2 коммитов

Автор SHA1 Сообщение Дата
  Evgeniy Ierusalimov 7972cf4fed code beautifying 1 месяц назад
  Evgeniy Ierusalimov 7adde57e80 refactored code for SOLID, DRY, KISS and YAGNI principles compartibility 1 месяц назад

+ 5
- 1
config/services.yaml Просмотреть файл

@@ -34,12 +34,16 @@ services:
34 34
         arguments:
35 35
             - proxy: '%env(TELEGRAM_PROXY)%'
36 36
 
37
-    App\Presentation\PortfolioPresenter:
37
+    App\Service\SecurityTitleResolver:
38 38
         arguments:
39 39
             $securitiesMap: '%securitiesMap%'
40
+
41
+    App\Service\PortfolioImageRenderer:
42
+        arguments:
40 43
             $fontDir: '%kernel.project_dir%'
41 44
 
42 45
     App\Service\MailFetcher:
43 46
         arguments:
47
+            $proxy: '%env(TELEGRAM_PROXY)%'
44 48
             $validEmailSubject: '%env(VALID_EMAIL_SUBJECT)%'
45 49
             $validEmailSender: '%env(VALID_EMAIL_SENDER)%'

+ 9
- 7
src/Command/ImportFromXmlCommand.php Просмотреть файл

@@ -4,9 +4,10 @@ declare(strict_types=1);
4 4
 
5 5
 namespace App\Command;
6 6
 
7
-use App\Presentation\PortfolioPresenter;
8
-use App\Service\TelegramNotifier;
9
-use App\Service\XmlParser;
7
+use App\Service\PortfolioImageRenderer;
8
+use App\Service\TelegramNotifierInterface;
9
+use App\Service\XmlParserInterface;
10
+use App\Service\PortfolioPresenterInterface;
10 11
 use Symfony\Component\Console\Attribute\AsCommand;
11 12
 use Symfony\Component\Console\Command\Command;
12 13
 use Symfony\Component\Console\Input\InputInterface;
@@ -21,9 +22,10 @@ use Symfony\Component\Console\Style\SymfonyStyle;
21 22
 class ImportFromXmlCommand extends Command
22 23
 {
23 24
     public function __construct(
24
-        private readonly XmlParser          $xmlParser,
25
-        private readonly TelegramNotifier   $telegramNotifier,
26
-        private readonly PortfolioPresenter $portfolioPresenter,
25
+        private readonly XmlParserInterface          $xmlParser,
26
+        private readonly TelegramNotifierInterface   $telegramNotifier,
27
+        private readonly PortfolioPresenterInterface $portfolioPresenter,
28
+        private readonly PortfolioImageRenderer      $imageRenderer,
27 29
     )
28 30
     {
29 31
         parent::__construct();
@@ -71,7 +73,7 @@ class ImportFromXmlCommand extends Command
71 73
 
72 74
         $output->writeln($this->portfolioPresenter->toText($parsedPortfolio));
73 75
 
74
-        $this->portfolioPresenter->toImage($parsedPortfolio, 'portfolio.png');
76
+        $this->imageRenderer->toImage($parsedPortfolio, 'portfolio.png');
75 77
 
76 78
         if ($input->getOption('notify')) {
77 79
             $this->telegramNotifier->notify('', 'portfolio.png');

+ 1
- 2
src/Entity/PortfolioDetail.php Просмотреть файл

@@ -4,11 +4,10 @@ declare(strict_types=1);
4 4
 
5 5
 namespace App\Entity;
6 6
 
7
-use App\Repository\PortfolioDetailRepository;
8 7
 use Doctrine\DBAL\Types\Types;
9 8
 use Doctrine\ORM\Mapping as ORM;
10 9
 
11
-#[ORM\Entity(repositoryClass: PortfolioDetailRepository::class)]
10
+#[ORM\Entity]
12 11
 class PortfolioDetail
13 12
 {
14 13
     #[ORM\Id]

+ 1
- 2
src/Entity/PortfolioMovement.php Просмотреть файл

@@ -4,10 +4,9 @@ declare(strict_types=1);
4 4
 
5 5
 namespace App\Entity;
6 6
 
7
-use App\Repository\PortfolioMovementRepository;
8 7
 use Doctrine\ORM\Mapping as ORM;
9 8
 
10
-#[ORM\Entity(repositoryClass: PortfolioMovementRepository::class)]
9
+#[ORM\Entity]
11 10
 class PortfolioMovement
12 11
 {
13 12
     #[ORM\Id]

+ 65
- 92
src/Presentation/PortfolioPresenter.php Просмотреть файл

@@ -6,57 +6,21 @@ namespace App\Presentation;
6 6
 
7 7
 use App\Domain\Entity\ParsedPortfolio;
8 8
 use App\Service\PortfolioPresenterInterface;
9
+use App\Service\SecurityTitleResolver;
9 10
 use Symfony\Component\Console\Helper\Table;
10 11
 use Symfony\Component\Console\Output\BufferedOutput;
11 12
 use Symfony\Component\Console\Helper\TableStyle;
12 13
 
13 14
 class PortfolioPresenter implements PortfolioPresenterInterface
14 15
 {
15
-    private const FONT_SIZE = 12;
16
-    private const FONT_NAME = 'droid_sans_mono.ttf';
17 16
     private const COPYRIGHT_MARK = ' ©ЕИ';
18 17
 
19
-    /** @var array{array<string>, array<string>} */
20
-    private array $securityMapPrintable = [['(в пути)'], ['±']];
21
-
22
-    /** @var array<string, string> */
23
-    private array $mapSecurityTitle = [];
24
-
25
-    private string $fontPath;
26
-
27
-    public function __construct(array $securitiesMap, string $fontDir)
28
-    {
29
-        array_unshift($this->securityMapPrintable[0], ...array_keys($securitiesMap));
30
-        array_unshift($this->securityMapPrintable[1], ...array_values($securitiesMap));
31
-
32
-        $this->fontPath = $fontDir . '/' . self::FONT_NAME;
33
-    }
34
-
35
-    public function toImage(ParsedPortfolio $parsedPortfolio, string $filename): void
18
+    public function __construct(private SecurityTitleResolver $titleResolver)
36 19
     {
37
-        $im = imagecreatetruecolor(1000, 1000);
38
-        $whitey = imagecolorallocate($im, 240, 240, 240);
39
-        $grey = imagecolorallocate($im, 48, 48, 48);
40
-        imagefill($im, 0, 0, $grey);
41
-
42
-        $lines = explode("\n", $this->toText($parsedPortfolio));
43
-        $height = self::FONT_SIZE;
44
-        foreach ($lines as $line) {
45
-            imagefttext($im, self::FONT_SIZE, 0, 0, $height, $whitey, $this->fontPath, $line);
46
-            $height += self::FONT_SIZE + (int) round(0.5 * self::FONT_SIZE);
47
-        }
48
-
49
-        $cropped = imagecropauto($im, IMG_CROP_SIDES);
50
-        if ($cropped !== false) {
51
-            imagedestroy($im);
52
-            $im = $cropped;
53
-        }
54
-
55
-        imagepng($im, $filename);
56
-        imagedestroy($im);
57 20
     }
58 21
 
59
-    public function toText(ParsedPortfolio $parsedPortfolio): string
22
+    /** @return array<int, string> */
23
+    public function toLines(ParsedPortfolio $parsedPortfolio): array
60 24
     {
61 25
         $header = $parsedPortfolio->header;
62 26
         $periodTitle = $header->startDate->format('d.m.y') . ' - ' . $header->endDate->format('d.m.y');
@@ -64,89 +28,98 @@ class PortfolioPresenter implements PortfolioPresenterInterface
64 28
         $total = $this->calcTotalSum($parsedPortfolio);
65 29
 
66 30
         $output = new BufferedOutput();
31
+        $cellStyle = $this->createCellStyle();
67 32
 
68
-        $tableStyle = new TableStyle();
69
-        $tableStyle->setPadType(STR_PAD_LEFT);
70
-
71
-        $cellStyle = new TableStyle();
72
-        $cellStyle->setPadType(STR_PAD_RIGHT);
73
-
74
-        $table = new Table($output);
75
-        $table->setHeaderTitle('ПОРТФЕЛЬ: ' . $periodTitle);
76
-        $table->setHeaders(['ЦБ', 'Кол-во', 'Цн', 'Цк', 'Сумм', '%']);
77
-
33
+        $portfolioRows = [];
78 34
         foreach ($parsedPortfolio->details as $detail) {
79
-            $table->addRow([
80
-                $this->getPrintableSecurityTitle($detail->security, $detail->issuer),
35
+            $portfolioRows[] = [
36
+                $this->titleResolver->resolve($detail->security, $detail->issuer),
81 37
                 $this->formatQuantity($detail->quantityStart, $detail->quantityEnd),
82 38
                 $detail->priceStart,
83 39
                 $detail->priceEnd,
84 40
                 round((float) $detail->sumTotal),
85 41
                 round(100 * (float) $detail->sumTotal / $total, 2),
86
-            ]);
42
+            ];
87 43
         }
44
+        $lines = $this->renderTable($output, 'ПОРТФЕЛЬ: ' . $periodTitle,
45
+            ['ЦБ', 'Кол-во', 'Цн', 'Цк', 'Сумм', '%'],
46
+            $portfolioRows, $cellStyle,
47
+        );
88 48
 
89
-        $table->setColumnStyle(0, $cellStyle);
90
-        $table->setStyle($tableStyle)->render();
91
-
92
-        $txt = $output->fetch();
93
-
94
-        $table = new Table($output);
95
-        $table->setHeaderTitle('ДВИЖ: ' . $periodTitle);
96
-        $table->setHeaders(['ЦБ', 'Кн', 'In', 'Out', 'Кк']);
97
-
49
+        $movementRows = [];
98 50
         foreach ($parsedPortfolio->movements as $movement) {
99
-            $table->addRow([
100
-                $this->getPrintableSecurityTitle($movement->security),
51
+            $movementRows[] = [
52
+                $this->titleResolver->resolve($movement->security),
101 53
                 $movement->quantityStart,
102 54
                 $movement->quantityIncome > 0 ? '+' . $movement->quantityIncome : $movement->quantityIncome,
103 55
                 $movement->quantityOutcome > 0 ? '-' . $movement->quantityOutcome : $movement->quantityOutcome,
104 56
                 $movement->quantityEnd,
105
-            ]);
57
+            ];
106 58
         }
59
+        $mvLines = $this->renderTable($output, 'ДВИЖ: ' . $periodTitle,
60
+            ['ЦБ', 'Кн', 'In', 'Out', 'Кк'],
61
+            $movementRows, $cellStyle,
62
+        );
107 63
 
108
-        $table->setColumnStyle(0, $cellStyle);
109
-        $table->setStyle($tableStyle)->render();
110
-        $txt .= PHP_EOL . rtrim($output->fetch()) . self::COPYRIGHT_MARK;
64
+        $lines[] = '';
65
+        foreach ($mvLines as $line) {
66
+            $lines[] = $line;
67
+        }
68
+        $lines[count($lines) - 1] = rtrim($lines[count($lines) - 1]) . self::COPYRIGHT_MARK;
111 69
 
112
-        return $txt;
70
+        return $lines;
113 71
     }
114 72
 
115
-    private function calcTotalSum(ParsedPortfolio $parsedPortfolio): float
73
+    public function toText(ParsedPortfolio $parsedPortfolio): string
116 74
     {
117
-        $total = 0.0;
75
+        return implode("\n", $this->toLines($parsedPortfolio));
76
+    }
118 77
 
119
-        foreach ($parsedPortfolio->details as $detail) {
120
-            $total += (float) $detail->sumTotal;
78
+    /**
79
+     * @param array<int, array<string|int>> $rows
80
+     * @return array<int, string>
81
+     */
82
+    private function renderTable(
83
+        BufferedOutput $output,
84
+        string $title,
85
+        array $headers,
86
+        array $rows,
87
+        TableStyle $cellStyle,
88
+    ): array {
89
+        $table = new Table($output);
90
+        $table->setHeaderTitle($title);
91
+        $table->setHeaders($headers);
92
+
93
+        foreach ($rows as $row) {
94
+            $table->addRow($row);
121 95
         }
122 96
 
123
-        return $total;
97
+        $table->setColumnStyle(0, $cellStyle);
98
+
99
+        $padStyle = new TableStyle();
100
+        $padStyle->setPadType(STR_PAD_LEFT);
101
+        $table->setStyle($padStyle)->render();
102
+
103
+        return explode("\n", rtrim($output->fetch()));
124 104
     }
125 105
 
126
-    private function getPrintableSecurityTitle(string $security, ?string $issuer = null): string
106
+    private function createCellStyle(): TableStyle
127 107
     {
128
-        $security = str_replace(
129
-            $this->securityMapPrintable[0],
130
-            $this->securityMapPrintable[1],
131
-            $security,
132
-        );
108
+        $style = new TableStyle();
109
+        $style->setPadType(STR_PAD_RIGHT);
133 110
 
134
-        if (!str_starts_with($security, 'RU00')) {
135
-            return $security;
136
-        }
111
+        return $style;
112
+    }
137 113
 
138
-        if (isset($this->mapSecurityTitle[$security])) {
139
-            return $this->mapSecurityTitle[$security];
140
-        }
114
+    private function calcTotalSum(ParsedPortfolio $parsedPortfolio): float
115
+    {
116
+        $total = 0.0;
141 117
 
142
-        if ($issuer === null) {
143
-            return $security;
118
+        foreach ($parsedPortfolio->details as $detail) {
119
+            $total += (float) $detail->sumTotal;
144 120
         }
145 121
 
146
-        $title = str_replace(['МКПАО', 'ПАО', '"', ' ', '(', ')'], '', $issuer);
147
-        $this->mapSecurityTitle[$security] = $title;
148
-
149
-        return $title;
122
+        return $total;
150 123
     }
151 124
 
152 125
     private function formatQuantity(int $quantityStart, int $quantityEnd): string

+ 0
- 20
src/Repository/PortfolioDetailRepository.php Просмотреть файл

@@ -1,20 +0,0 @@
1
-<?php
2
-
3
-declare(strict_types=1);
4
-
5
-namespace App\Repository;
6
-
7
-use App\Entity\PortfolioDetail;
8
-use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
9
-use Doctrine\Persistence\ManagerRegistry;
10
-
11
-/**
12
- * @extends ServiceEntityRepository<PortfolioDetail>
13
- */
14
-class PortfolioDetailRepository extends ServiceEntityRepository
15
-{
16
-    public function __construct(ManagerRegistry $registry)
17
-    {
18
-        parent::__construct($registry, PortfolioDetail::class);
19
-    }
20
-}

+ 0
- 20
src/Repository/PortfolioMovementRepository.php Просмотреть файл

@@ -1,20 +0,0 @@
1
-<?php
2
-
3
-declare(strict_types=1);
4
-
5
-namespace App\Repository;
6
-
7
-use App\Entity\PortfolioMovement;
8
-use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
9
-use Doctrine\Persistence\ManagerRegistry;
10
-
11
-/**
12
- * @extends ServiceEntityRepository<PortfolioMovement>
13
- */
14
-class PortfolioMovementRepository extends ServiceEntityRepository
15
-{
16
-    public function __construct(ManagerRegistry $registry)
17
-    {
18
-        parent::__construct($registry, PortfolioMovement::class);
19
-    }
20
-}

+ 14
- 12
src/Service/MailFetcher.php Просмотреть файл

@@ -4,8 +4,6 @@ declare(strict_types=1);
4 4
 
5 5
 namespace App\Service;
6 6
 
7
-use App\Presentation\PortfolioPresenter;
8
-use App\Service\TelegramNotifier;
9 7
 use PhpImap\Exceptions\ConnectionException;
10 8
 use PhpImap\IncomingMail;
11 9
 use Psr\Log\LoggerInterface;
@@ -16,21 +14,25 @@ readonly class MailFetcher
16 14
 {
17 15
     public function __construct(
18 16
         #[Target('finfollowConnection')]
19
-        private ConnectionInterface $connection,
20
-        private XmlParser           $xmlParser,
21
-        private PortfolioManager    $portfolioManager,
22
-        private LoggerInterface     $logger,
23
-        private TelegramNotifier    $telegramNotifier,
24
-        private PortfolioPresenter  $portfolioPresenter,
25
-        private AttachmentProcessor $attachmentProcessor,
26
-        private string              $validEmailSubject,
27
-        private string              $validEmailSender,
17
+        private ConnectionInterface      $connection,
18
+        private XmlParserInterface       $xmlParser,
19
+        private PortfolioManager         $portfolioManager,
20
+        private LoggerInterface          $logger,
21
+        private TelegramNotifierInterface $telegramNotifier,
22
+        private PortfolioImageRenderer   $imageRenderer,
23
+        private AttachmentProcessor      $attachmentProcessor,
24
+        private ProxyChecker             $proxyChecker,
25
+        private string                   $proxy,
26
+        private string                   $validEmailSubject,
27
+        private string                   $validEmailSender,
28 28
     )
29 29
     {
30 30
     }
31 31
 
32 32
     public function fetchNewEmails(): void
33 33
     {
34
+        $this->proxyChecker->check($this->proxy);
35
+
34 36
         $mailbox = $this->connection->getMailbox();
35 37
         try {
36 38
             $mailsIds = $mailbox->searchMailbox('ALL');
@@ -101,7 +103,7 @@ readonly class MailFetcher
101 103
 
102 104
             if ($this->portfolioManager->updatePortfolio($parsedPortfolio, $xmlString)) {
103 105
                 $pngFilename = tempnam(sys_get_temp_dir(), 'finfollow-portfolio-');
104
-                $this->portfolioPresenter->toImage($parsedPortfolio, $pngFilename);
106
+                $this->imageRenderer->toImage($parsedPortfolio, $pngFilename);
105 107
                 $this->telegramNotifier->notify('', $pngFilename);
106 108
             }
107 109
         }

+ 47
- 0
src/Service/PortfolioImageRenderer.php Просмотреть файл

@@ -0,0 +1,47 @@
1
+<?php
2
+
3
+declare(strict_types=1);
4
+
5
+namespace App\Service;
6
+
7
+use App\Domain\Entity\ParsedPortfolio;
8
+
9
+readonly class PortfolioImageRenderer
10
+{
11
+    private const FONT_SIZE = 12;
12
+    private const FONT_NAME = 'droid_sans_mono.ttf';
13
+
14
+    private string $fontPath;
15
+
16
+    public function __construct(
17
+        private PortfolioPresenterInterface $presenter,
18
+        string $fontDir,
19
+    ) {
20
+        $this->fontPath = $fontDir . '/' . self::FONT_NAME;
21
+    }
22
+
23
+    public function toImage(ParsedPortfolio $parsedPortfolio, string $filename): void
24
+    {
25
+        $im = imagecreatetruecolor(1000, 1000);
26
+        $whitey = imagecolorallocate($im, 240, 240, 240);
27
+        $grey = imagecolorallocate($im, 48, 48, 48);
28
+        imagefill($im, 0, 0, $grey);
29
+
30
+        $lines = $this->presenter->toLines($parsedPortfolio);
31
+
32
+        $height = self::FONT_SIZE;
33
+        foreach ($lines as $line) {
34
+            imagefttext($im, self::FONT_SIZE, 0, 0, $height, $whitey, $this->fontPath, $line);
35
+            $height += self::FONT_SIZE + (int) round(0.5 * self::FONT_SIZE);
36
+        }
37
+
38
+        $cropped = imagecropauto($im, IMG_CROP_SIDES);
39
+        if ($cropped !== false) {
40
+            imagedestroy($im);
41
+            $im = $cropped;
42
+        }
43
+
44
+        imagepng($im, $filename);
45
+        imagedestroy($im);
46
+    }
47
+}

+ 2
- 1
src/Service/PortfolioPresenterInterface.php Просмотреть файл

@@ -8,7 +8,8 @@ use App\Domain\Entity\ParsedPortfolio;
8 8
 
9 9
 interface PortfolioPresenterInterface
10 10
 {
11
-    public function toImage(ParsedPortfolio $parsedPortfolio, string $filename): void;
11
+    /** @return array<int, string> */
12
+    public function toLines(ParsedPortfolio $parsedPortfolio): array;
12 13
 
13 14
     public function toText(ParsedPortfolio $parsedPortfolio): string;
14 15
 }

+ 37
- 0
src/Service/ProxyChecker.php Просмотреть файл

@@ -0,0 +1,37 @@
1
+<?php
2
+
3
+declare(strict_types=1);
4
+
5
+namespace App\Service;
6
+
7
+readonly class ProxyChecker
8
+{
9
+    public function check(string $proxy): void
10
+    {
11
+        if ($proxy === '') {
12
+            return;
13
+        }
14
+
15
+        $parsed = parse_url($proxy);
16
+
17
+        if ($parsed === false || !isset($parsed['host'])) {
18
+            throw new \RuntimeException('TELEGRAM_PROXY: invalid URL format');
19
+        }
20
+
21
+        $host = $parsed['host'];
22
+        $port = $parsed['port'] ?? 1080;
23
+
24
+        $errno = 0;
25
+        $errstr = '';
26
+
27
+        $fp = @fsockopen($host, $port, $errno, $errstr, 5);
28
+
29
+        if ($fp === false) {
30
+            throw new \RuntimeException(
31
+                sprintf('TELEGRAM_PROXY is unreachable: %s:%d (%s)', $host, $port, $errstr),
32
+            );
33
+        }
34
+
35
+        fclose($fp);
36
+    }
37
+}

+ 45
- 0
src/Service/SecurityTitleResolver.php Просмотреть файл

@@ -0,0 +1,45 @@
1
+<?php
2
+
3
+declare(strict_types=1);
4
+
5
+namespace App\Service;
6
+
7
+final class SecurityTitleResolver
8
+{
9
+    /** @var array<string, string> */
10
+    private readonly array $replacements;
11
+
12
+    /** @var array<string, string> */
13
+    private array $cache;
14
+
15
+    public function __construct(array $securitiesMap)
16
+    {
17
+        $this->cache = [];
18
+        $this->replacements = array_merge(
19
+            ['(в пути)' => '±'],
20
+            $securitiesMap,
21
+        );
22
+    }
23
+
24
+    public function resolve(string $security, ?string $issuer = null): string
25
+    {
26
+        $security = strtr($security, $this->replacements);
27
+
28
+        if (!str_starts_with($security, 'RU00')) {
29
+            return $security;
30
+        }
31
+
32
+        if (isset($this->cache[$security])) {
33
+            return $this->cache[$security];
34
+        }
35
+
36
+        if ($issuer === null) {
37
+            return $security;
38
+        }
39
+
40
+        $title = str_replace(['МКПАО', 'ПАО', '"', ' ', '(', ')'], '', $issuer);
41
+        $this->cache[$security] = $title;
42
+
43
+        return $title;
44
+    }
45
+}

+ 1
- 1
src/Service/TelegramNotifier.php Просмотреть файл

@@ -48,4 +48,4 @@ readonly class TelegramNotifier implements TelegramNotifierInterface
48 48
             ]);
49 49
         }
50 50
     }
51
-}
51
+}

+ 30
- 38
src/Service/XmlParser.php Просмотреть файл

@@ -9,7 +9,7 @@ use App\Domain\Entity\PortfolioDetailItem;
9 9
 use App\Domain\Entity\PortfolioHeader;
10 10
 use App\Domain\Entity\PortfolioMovementItem;
11 11
 
12
-class XmlParser
12
+class XmlParser implements XmlParserInterface
13 13
 {
14 14
     private const XML_HEADER_AGRMT        = 'ШапкаГенСог';
15 15
     private const XML_HEADER_PERIOD       = 'ШапкаПериод';
@@ -95,33 +95,18 @@ class XmlParser
95 95
     {
96 96
         $items = [];
97 97
 
98
-        foreach (get_object_vars($xml) as $key => $value) {
99
-            if (mb_strpos($key, self::PREFIX_DETAILS) === false) {
100
-                continue;
101
-            }
102
-
103
-            if (!is_array($value)) {
104
-                $value = [$value];
105
-            }
106
-
107
-            foreach ($value as $line) {
108
-                $attr = [];
109
-                foreach ($line->attributes() as $attrName => $attrValue) {
110
-                    $attr[$attrName] = get_object_vars($attrValue)[0];
111
-                }
112
-
113
-                $items[] = new PortfolioDetailItem(
114
-                    issuer:        $attr[self::ATTR_ISSUER],
115
-                    security:      $attr[self::ATTR_SECURITY],
116
-                    priceStart:    $attr[self::ATTR_PRICE_START],
117
-                    priceEnd:      $attr[self::ATTR_PRICE_END],
118
-                    quantityStart: (int) $attr[self::ATTR_QTY_START],
119
-                    quantityEnd:   (int) $attr[self::ATTR_QTY_END],
120
-                    sumStart:      $attr[self::ATTR_SUM_START],
121
-                    sumEnd:        $attr[self::ATTR_SUM_END],
122
-                    sumTotal:      $attr[self::ATTR_SUM_TOTAL],
123
-                );
124
-            }
98
+        foreach ($this->iterateDetailElements($xml) as $attr) {
99
+            $items[] = new PortfolioDetailItem(
100
+                issuer:        $attr[self::ATTR_ISSUER],
101
+                security:      $attr[self::ATTR_SECURITY],
102
+                priceStart:    $attr[self::ATTR_PRICE_START],
103
+                priceEnd:      $attr[self::ATTR_PRICE_END],
104
+                quantityStart: (int) $attr[self::ATTR_QTY_START],
105
+                quantityEnd:   (int) $attr[self::ATTR_QTY_END],
106
+                sumStart:      $attr[self::ATTR_SUM_START],
107
+                sumEnd:        $attr[self::ATTR_SUM_END],
108
+                sumTotal:      $attr[self::ATTR_SUM_TOTAL],
109
+            );
125 110
         }
126 111
 
127 112
         return $items;
@@ -131,6 +116,22 @@ class XmlParser
131 116
     {
132 117
         $items = [];
133 118
 
119
+        foreach ($this->iterateDetailElements($xml) as $attr) {
120
+            $items[] = new PortfolioMovementItem(
121
+                security:       $attr[self::ATTR_SECURITY],
122
+                period:         new \DateTimeImmutable($attr[self::ATTR_PERIOD]),
123
+                quantityStart:  (int) $attr[self::ATTR_QTY_START_MV],
124
+                quantityEnd:    (int) $attr[self::ATTR_QTY_END_MV],
125
+                quantityIncome: (int) $attr[self::ATTR_QTY_INCOME],
126
+                quantityOutcome:(int) $attr[self::ATTR_QTY_OUTCOME],
127
+            );
128
+        }
129
+
130
+        return $items;
131
+    }
132
+
133
+    private function iterateDetailElements(\SimpleXMLElement $xml): iterable
134
+    {
134 135
         foreach (get_object_vars($xml) as $key => $value) {
135 136
             if (mb_strpos($key, self::PREFIX_DETAILS) === false) {
136 137
                 continue;
@@ -146,17 +147,8 @@ class XmlParser
146 147
                     $attr[$attrName] = get_object_vars($attrValue)[0];
147 148
                 }
148 149
 
149
-                $items[] = new PortfolioMovementItem(
150
-                    security:       $attr[self::ATTR_SECURITY],
151
-                    period:         new \DateTimeImmutable($attr[self::ATTR_PERIOD]),
152
-                    quantityStart:  (int) $attr[self::ATTR_QTY_START_MV],
153
-                    quantityEnd:    (int) $attr[self::ATTR_QTY_END_MV],
154
-                    quantityIncome: (int) $attr[self::ATTR_QTY_INCOME],
155
-                    quantityOutcome:(int) $attr[self::ATTR_QTY_OUTCOME],
156
-                );
150
+                yield $attr;
157 151
             }
158 152
         }
159
-
160
-        return $items;
161 153
     }
162 154
 }

+ 12
- 0
src/Service/XmlParserInterface.php Просмотреть файл

@@ -0,0 +1,12 @@
1
+<?php
2
+
3
+declare(strict_types=1);
4
+
5
+namespace App\Service;
6
+
7
+use App\Domain\Entity\ParsedPortfolio;
8
+
9
+interface XmlParserInterface
10
+{
11
+    public function processXml(\SimpleXMLElement $xml): ParsedPortfolio;
12
+}

Загрузка…
Отмена
Сохранить