From 7526e38152bd062819d4dfad453531c4592bfd89 Mon Sep 17 00:00:00 2001 From: sni10 <9530163+sni10@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:38:41 +0200 Subject: [PATCH 1/6] docs: add design patterns section to README --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 03d0ff7..9f7bf73 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,18 @@ ![architecture-Keepa_API_Data_Extractor___Architecture.png](architecture-Keepa_API_Data_Extractor___Architecture.png) +### Применяемые паттерны проектирования + +В проекте активно используются следующие паттерны: + +- **DTO (Data Transfer Object)**: Для передачи типизированных данных между слоями (`KeepaInputDto`, `KeepaOutputDto`). +- **Facade (Фасад)**: `KeepaService` скрывает сложность взаимодействия с внешней библиотекой Keepa API и управления токенами. +- **Service Layer (Сервисный слой)**: Разделение бизнес-логики (`FinderProductsService`) и логики работы с API (`KeepaService`). +- **Orchestrator (Оркестратор)**: `FinderProductsService` управляет потоком: валидация -> поиск -> отправка. +- **Command Handler**: Обработка сообщений через `KeepaInputMessageHandler` (CQRS-style в Symfony Messenger). +- **Iterator / Generator**: Использование `yield` для экономии памяти при переборе больших списков товаров. +- **Dependency Injection**: Внедрение зависимостей для обеспечения тестируемости и гибкости. + ## Требования - PHP 8.3+ @@ -389,5 +401,4 @@ CREATE TABLE find_products ( ## License -MIT License — see [LICENSE](LICENSE) for details -ываыва \ No newline at end of file +MIT License — see [LICENSE](LICENSE) for details \ No newline at end of file From ad6cae1b0303912d583ddc7c22910d059c2e450f Mon Sep 17 00:00:00 2001 From: sni10 <9530163+sni10@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:50:50 +0200 Subject: [PATCH 2/6] test: add KeepaOutputDto unit tests --- tests/Unit/Dto/KeepaOutputDtoTest.php | 149 ++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 tests/Unit/Dto/KeepaOutputDtoTest.php diff --git a/tests/Unit/Dto/KeepaOutputDtoTest.php b/tests/Unit/Dto/KeepaOutputDtoTest.php new file mode 100644 index 0000000..88ba88d --- /dev/null +++ b/tests/Unit/Dto/KeepaOutputDtoTest.php @@ -0,0 +1,149 @@ + 'B08N5WRWNW', + 'brand' => 'Adidas', + 'title' => 'Adidas Ultraboost 21', + 'upc_list' => ['123456789012', '234567890123'], + 'ean_list' => ['1234567890123', '2345678901234'], + 'search_request_id' => 42, + 'domain_id' => 1, + 'json_data' => ['key' => 'value'], + 'time_from' => '2025-01-01', + 'time_to' => '2025-01-31', + 'version' => 1, + ]; + + $dto = KeepaOutputDto::fromArray($data); + + self::assertSame('B08N5WRWNW', $dto->asin); + self::assertSame('Adidas', $dto->brand); + self::assertSame('Adidas Ultraboost 21', $dto->title); + self::assertSame(['123456789012', '234567890123'], $dto->upc_list); + self::assertSame(['1234567890123', '2345678901234'], $dto->ean_list); + self::assertSame(42, $dto->search_request_id); + self::assertSame(1, $dto->domain_id); + self::assertSame(['key' => 'value'], $dto->json_data); + self::assertSame('2025-01-01', $dto->time_from); + self::assertSame('2025-01-31', $dto->time_to); + self::assertSame(1, $dto->version); + } + + public function testFromArrayWithMissingFields(): void + { + $data = [ + 'asin' => 'B08N5WRWNW', + 'brand' => 'Adidas', + ]; + + $dto = KeepaOutputDto::fromArray($data); + + self::assertSame('B08N5WRWNW', $dto->asin); + self::assertSame('Adidas', $dto->brand); + self::assertNull($dto->title); + self::assertNull($dto->upc_list); + self::assertNull($dto->ean_list); + self::assertNull($dto->search_request_id); + self::assertNull($dto->domain_id); + self::assertNull($dto->json_data); + self::assertNull($dto->time_from); + self::assertNull($dto->time_to); + self::assertNull($dto->version); + } + + public function testFromArrayCastsIntegerFields(): void + { + $data = [ + 'asin' => 'B08N5WRWNW', + 'search_request_id' => '42', + 'domain_id' => '1', + 'version' => '5', + ]; + + $dto = KeepaOutputDto::fromArray($data); + + self::assertSame(42, $dto->search_request_id); + self::assertSame(1, $dto->domain_id); + self::assertSame(5, $dto->version); + } + + public function testToArrayReturnsAllProperties(): void + { + $dto = new KeepaOutputDto(); + $dto->asin = 'B08N5WRWNW'; + $dto->brand = 'Nike'; + $dto->title = 'Nike Air Max'; + $dto->upc_list = ['111111111111']; + $dto->ean_list = ['2222222222222']; + $dto->search_request_id = 100; + $dto->domain_id = 2; + $dto->json_data = ['test' => 'data']; + $dto->time_from = '2025-02-01'; + $dto->time_to = '2025-02-28'; + $dto->version = 2; + + $array = $dto->toArray(); + + self::assertSame('B08N5WRWNW', $array['asin']); + self::assertSame('Nike', $array['brand']); + self::assertSame('Nike Air Max', $array['title']); + self::assertSame(['111111111111'], $array['upc_list']); + self::assertSame(['2222222222222'], $array['ean_list']); + self::assertSame(100, $array['search_request_id']); + self::assertSame(2, $array['domain_id']); + self::assertSame(['test' => 'data'], $array['json_data']); + self::assertSame('2025-02-01', $array['time_from']); + self::assertSame('2025-02-28', $array['time_to']); + self::assertSame(2, $array['version']); + } + + public function testToArrayWithNullProperties(): void + { + $dto = new KeepaOutputDto(); + + $array = $dto->toArray(); + + self::assertNull($array['asin']); + self::assertNull($array['brand']); + self::assertNull($array['title']); + self::assertNull($array['upc_list']); + self::assertNull($array['ean_list']); + self::assertNull($array['search_request_id']); + self::assertNull($array['domain_id']); + self::assertNull($array['json_data']); + self::assertNull($array['time_from']); + self::assertNull($array['time_to']); + self::assertNull($array['version']); + } + + public function testRoundTripConversion(): void + { + $originalData = [ + 'asin' => 'B08N5WRWNW', + 'brand' => 'Puma', + 'title' => 'Puma Suede Classic', + 'upc_list' => ['333333333333'], + 'ean_list' => ['4444444444444'], + 'search_request_id' => 999, + 'domain_id' => 3, + 'json_data' => ['nested' => ['data' => 'here']], + 'time_from' => '2025-03-01', + 'time_to' => '2025-03-31', + 'version' => 10, + ]; + + $dto = KeepaOutputDto::fromArray($originalData); + $resultArray = $dto->toArray(); + + self::assertSame($originalData, $resultArray); + } +} From ebf17a0e762b2b42d0057a8a4bb5ba1077289fee Mon Sep 17 00:00:00 2001 From: sni10 <9530163+sni10@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:06:46 +0200 Subject: [PATCH 3/6] test: add unit test for KeepaInputDto brand normalization --- tests/Unit/Dto/KeepaInputDtoTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Unit/Dto/KeepaInputDtoTest.php b/tests/Unit/Dto/KeepaInputDtoTest.php index 38c5c4c..d071043 100644 --- a/tests/Unit/Dto/KeepaInputDtoTest.php +++ b/tests/Unit/Dto/KeepaInputDtoTest.php @@ -45,4 +45,20 @@ public function testFromArrayNormalizesAndCastsFields(): void 'step' => 3, ], $dto->toArray()); } + + public function testGetBrandReturnsNormalizedBrand(): void + { + $dto = KeepaInputDto::fromArray([ + 'id' => '10', + 'domain_id' => '3', + 'brand' => " Nike ", + 'time_from' => '2025-02-01', + 'time_to' => '2025-02-28', + 'version' => '1', + 'status' => ' IN_PROGRESS ', + 'step' => '1', + ]); + + self::assertSame('Nike', $dto->getBrand()); + } } From a5b17679b7cae5afdad59028342fb18898267d4b Mon Sep 17 00:00:00 2001 From: sni10 <9530163+sni10@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:07:27 +0200 Subject: [PATCH 4/6] test: add unit test for FinderProductsService invalid input handling --- .../Service/FinderProductsServiceTest.php | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 tests/Unit/Service/FinderProductsServiceTest.php diff --git a/tests/Unit/Service/FinderProductsServiceTest.php b/tests/Unit/Service/FinderProductsServiceTest.php new file mode 100644 index 0000000..31e2ff4 --- /dev/null +++ b/tests/Unit/Service/FinderProductsServiceTest.php @@ -0,0 +1,92 @@ +logger = $this->createMock(LoggerInterface::class); + $this->keepaService = $this->createMock(KeepaService::class); + $this->messageValidator = $this->createMock(MessageValidator::class); + $this->bus = $this->createMock(MessageBusInterface::class); + } + + public function testExecuteReturnsDefaultStatsWhenInputIsInvalid(): void + { + $dto = KeepaInputDto::fromArray([ + 'id' => 1, + 'domain_id' => 1, + 'brand' => 'adidas', + 'time_from' => '2025-01-01', + 'time_to' => '2025-01-31', + 'version' => 1, + 'status' => 'PENDING', + 'step' => 0, + ]); + + $this->messageValidator + ->expects(self::once()) + ->method('validateInput') + ->with($dto) + ->willReturn(null); + + $this->logger + ->expects(self::once()) + ->method('warning') + ->with( + self::stringContains('Invalid Input Kafka message'), + self::anything(), + ); + + $this->keepaService + ->expects(self::never()) + ->method('execute'); + + $this->bus + ->expects(self::never()) + ->method('dispatch'); + + $service = new FinderProductsService( + $this->logger, + $this->keepaService, + $this->messageValidator, + $this->bus, + ); + + $result = $service->execute($dto); + + self::assertSame( + [ + 'asinsFound' => 0, + 'tokensUsed' => 0, + 'pages' => 0, + 'sleepSeconds' => 0, + ], + $result, + ); + } +} From 807c37ef8113c1d8ee440aa5f0c27939c1ce4e30 Mon Sep 17 00:00:00 2001 From: sni10 <9530163+sni10@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:08:49 +0200 Subject: [PATCH 5/6] test: add unit test for FinderProductsService execute method --- .../Service/FinderProductsServiceTest.php | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/tests/Unit/Service/FinderProductsServiceTest.php b/tests/Unit/Service/FinderProductsServiceTest.php index 31e2ff4..c27e368 100644 --- a/tests/Unit/Service/FinderProductsServiceTest.php +++ b/tests/Unit/Service/FinderProductsServiceTest.php @@ -3,6 +3,7 @@ namespace App\Tests\Unit\Service; use App\Dto\KeepaInputDto; +use App\Dto\KeepaOutputDto; use App\Service\FinderProductsService; use App\Service\KeepaService; use App\Service\Validator\MessageValidator; @@ -89,4 +90,83 @@ public function testExecuteReturnsDefaultStatsWhenInputIsInvalid(): void $result, ); } + + public function testExecuteDispatchesValidOutputsAndReturnsMappedStats(): void + { + $inputDto = KeepaInputDto::fromArray([ + 'id' => 2, + 'domain_id' => 3, + 'brand' => 'nike', + 'time_from' => '2025-02-01', + 'time_to' => '2025-02-28', + 'version' => 1, + 'status' => 'IN_PROGRESS', + 'step' => 1, + ]); + + $validatedInput = clone $inputDto; + + $this->messageValidator + ->expects(self::once()) + ->method('validateInput') + ->with($inputDto) + ->willReturn($validatedInput); + + $product = new KeepaOutputDto(); + $product->asin = 'B000TEST00'; + + $generator = (function () use ($product) { + yield $product; + })(); + + $this->keepaService + ->expects(self::once()) + ->method('execute') + ->with($validatedInput) + ->willReturn($generator); + + $validatedOutput = clone $product; + + $this->messageValidator + ->expects(self::once()) + ->method('validateOutput') + ->with($product) + ->willReturn($validatedOutput); + + $this->bus + ->expects(self::once()) + ->method('dispatch') + ->with($validatedOutput); + + $this->keepaService + ->expects(self::once()) + ->method('getStats') + ->willReturn([ + 'asinsFound' => 5, + 'tokensUsed' => 42, + 'pagesProcessed' => 3, + 'sleepSeconds' => 7, + 'tokensLeft' => 100, + ]); + + $service = new FinderProductsService( + $this->logger, + $this->keepaService, + $this->messageValidator, + $this->bus, + ); + + $result = $service->execute($inputDto); + + self::assertSame( + [ + 'asinsFound' => 5, + 'tokensUsed' => 42, + 'pages' => 3, + 'sleepSeconds' => 7, + 'tokensLeft' => 100, + ], + $result, + ); + } } From bb946886013c50af6717ea1e6adef89cb4184bbd Mon Sep 17 00:00:00 2001 From: sni10 <9530163+sni10@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:10:39 +0200 Subject: [PATCH 6/6] test: add unit test for FinderProductsService getKeepaService method and update dispatch mock --- tests/Unit/Service/FinderProductsServiceTest.php | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/Unit/Service/FinderProductsServiceTest.php b/tests/Unit/Service/FinderProductsServiceTest.php index c27e368..2af0a06 100644 --- a/tests/Unit/Service/FinderProductsServiceTest.php +++ b/tests/Unit/Service/FinderProductsServiceTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Envelope; class FinderProductsServiceTest extends TestCase { @@ -136,7 +137,8 @@ public function testExecuteDispatchesValidOutputsAndReturnsMappedStats(): void $this->bus ->expects(self::once()) ->method('dispatch') - ->with($validatedOutput); + ->with($validatedOutput) + ->willReturn(new Envelope($validatedOutput)); $this->keepaService ->expects(self::once()) @@ -169,4 +171,16 @@ public function testExecuteDispatchesValidOutputsAndReturnsMappedStats(): void $result, ); } + + public function testGetKeepaServiceReturnsInjectedInstance(): void + { + $service = new FinderProductsService( + $this->logger, + $this->keepaService, + $this->messageValidator, + $this->bus, + ); + + self::assertSame($this->keepaService, $service->getKeepaService()); + } }