Parcourir la source

refactored code for SOLID, DRY, KISS and YAGNI principles compartibility

master
Evgeniy Ierusalimov il y a 2 jours
Parent
révision
7adde57e80

+ 5
- 1
config/services.yaml Voir le fichier

@@ -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 Voir le fichier

@@ -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');

+ 4
- 67
src/Presentation/PortfolioPresenter.php Voir le fichier

@@ -6,54 +6,17 @@ 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)
18
+    public function __construct(private SecurityTitleResolver $titleResolver)
28 19
     {
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
36
-    {
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 22
     public function toText(ParsedPortfolio $parsedPortfolio): string
@@ -77,7 +40,7 @@ class PortfolioPresenter implements PortfolioPresenterInterface
77 40
 
78 41
         foreach ($parsedPortfolio->details as $detail) {
79 42
             $table->addRow([
80
-                $this->getPrintableSecurityTitle($detail->security, $detail->issuer),
43
+                $this->titleResolver->resolve($detail->security, $detail->issuer),
81 44
                 $this->formatQuantity($detail->quantityStart, $detail->quantityEnd),
82 45
                 $detail->priceStart,
83 46
                 $detail->priceEnd,
@@ -97,7 +60,7 @@ class PortfolioPresenter implements PortfolioPresenterInterface
97 60
 
98 61
         foreach ($parsedPortfolio->movements as $movement) {
99 62
             $table->addRow([
100
-                $this->getPrintableSecurityTitle($movement->security),
63
+                $this->titleResolver->resolve($movement->security),
101 64
                 $movement->quantityStart,
102 65
                 $movement->quantityIncome > 0 ? '+' . $movement->quantityIncome : $movement->quantityIncome,
103 66
                 $movement->quantityOutcome > 0 ? '-' . $movement->quantityOutcome : $movement->quantityOutcome,
@@ -123,32 +86,6 @@ class PortfolioPresenter implements PortfolioPresenterInterface
123 86
         return $total;
124 87
     }
125 88
 
126
-    private function getPrintableSecurityTitle(string $security, ?string $issuer = null): string
127
-    {
128
-        $security = str_replace(
129
-            $this->securityMapPrintable[0],
130
-            $this->securityMapPrintable[1],
131
-            $security,
132
-        );
133
-
134
-        if (!str_starts_with($security, 'RU00')) {
135
-            return $security;
136
-        }
137
-
138
-        if (isset($this->mapSecurityTitle[$security])) {
139
-            return $this->mapSecurityTitle[$security];
140
-        }
141
-
142
-        if ($issuer === null) {
143
-            return $security;
144
-        }
145
-
146
-        $title = str_replace(['МКПАО', 'ПАО', '"', ' ', '(', ')'], '', $issuer);
147
-        $this->mapSecurityTitle[$security] = $title;
148
-
149
-        return $title;
150
-    }
151
-
152 89
     private function formatQuantity(int $quantityStart, int $quantityEnd): string
153 90
     {
154 91
         if ($quantityStart === $quantityEnd) {

+ 14
- 12
src/Service/MailFetcher.php Voir le fichier

@@ -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 Voir le fichier

@@ -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 = explode("\n", $this->presenter->toText($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
+}

+ 0
- 2
src/Service/PortfolioPresenterInterface.php Voir le fichier

@@ -8,7 +8,5 @@ use App\Domain\Entity\ParsedPortfolio;
8 8
 
9 9
 interface PortfolioPresenterInterface
10 10
 {
11
-    public function toImage(ParsedPortfolio $parsedPortfolio, string $filename): void;
12
-
13 11
     public function toText(ParsedPortfolio $parsedPortfolio): string;
14 12
 }

+ 37
- 0
src/Service/ProxyChecker.php Voir le fichier

@@ -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
+}

+ 49
- 0
src/Service/SecurityTitleResolver.php Voir le fichier

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

+ 1
- 1
src/Service/TelegramNotifier.php Voir le fichier

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

+ 1
- 1
src/Service/XmlParser.php Voir le fichier

@@ -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       = 'ШапкаПериод';

+ 12
- 0
src/Service/XmlParserInterface.php Voir le fichier

@@ -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
+}

Chargement…
Annuler
Enregistrer