From 92a9a86e3dfa509c2f4aecb2866f5ce4804467a8 Mon Sep 17 00:00:00 2001 From: Lorenzo Ruozzi Date: Thu, 3 Oct 2024 12:17:51 +0200 Subject: [PATCH] Introduce new feed generation system --- composer.json | 7 +- config/services/command.php | 10 ++ config/services/generator.php | 42 +++++++ config/services/provider.php | 31 +++++ config/services/query_builder.php | 8 ++ src/Command/FeedGeneratorCommand.php | 3 + src/Command/V2FeedGeneratorCommand.php | 85 ++++++++++++++ .../Doctrine/ORM/Event/QueryBuilderEvent.php | 51 +++++++++ .../Doctrine/ORM/ProductsQueryBuilder.php | 78 +++++++++++++ src/DataSyncInfrastructure/Enum/Resource.php | 14 +++ .../Generator/FeedGeneratorInterface.php | 19 +++ .../Generator/ResourceFeedGenerator.php | 56 +++++++++ .../Model/QueryBuilderInterface.php | 27 +++++ .../Provider/QueryBuilderResourceProvider.php | 36 ++++++ .../Provider/ResourceProviderInterface.php | 24 ++++ .../ValueObject/Feed.php | 108 ++++++++++++++++++ src/DependencyInjection/Configuration.php | 3 + .../WebgriffeSyliusClerkExtension.php | 3 + symfony.lock | 21 ++++ tests/Application/config/bundles.php | 2 + .../packages/webgriffe_sylius_clerk.yaml | 5 - .../webgriffe_sylius_clerk_plugin.yaml | 6 + .../Doctrine/ORM/ProductsQueryBuilderTest.php | 46 ++++++++ .../testItQueriesProducts.yaml | 43 +++++++ .../Generator/ResourceFeedGeneratorTest.php | 80 +++++++++++++ .../QueryBuilderResourceProviderTest.php | 52 +++++++++ .../ValueObject/FeedTest.php | 93 +++++++++++++++ 27 files changed, 945 insertions(+), 8 deletions(-) create mode 100644 src/Command/V2FeedGeneratorCommand.php create mode 100644 src/DataSyncInfrastructure/Doctrine/ORM/Event/QueryBuilderEvent.php create mode 100644 src/DataSyncInfrastructure/Doctrine/ORM/ProductsQueryBuilder.php create mode 100644 src/DataSyncInfrastructure/Enum/Resource.php create mode 100644 src/DataSyncInfrastructure/Generator/FeedGeneratorInterface.php create mode 100644 src/DataSyncInfrastructure/Generator/ResourceFeedGenerator.php create mode 100644 src/DataSyncInfrastructure/Model/QueryBuilderInterface.php create mode 100644 src/DataSyncInfrastructure/Provider/QueryBuilderResourceProvider.php create mode 100644 src/DataSyncInfrastructure/Provider/ResourceProviderInterface.php create mode 100644 src/DataSyncInfrastructure/ValueObject/Feed.php delete mode 100644 tests/Application/config/packages/webgriffe_sylius_clerk.yaml create mode 100644 tests/Application/config/packages/webgriffe_sylius_clerk_plugin.yaml create mode 100644 tests/Integration/DataSyncInfrastructure/Doctrine/ORM/ProductsQueryBuilderTest.php create mode 100644 tests/Integration/DataSyncInfrastructure/Doctrine/ORM/fixtures/ProductsQueryBuilderTest/testItQueriesProducts.yaml create mode 100644 tests/Unit/DataSyncInfrastructure/Generator/ResourceFeedGeneratorTest.php create mode 100644 tests/Unit/DataSyncInfrastructure/Provider/QueryBuilderResourceProviderTest.php create mode 100644 tests/Unit/DataSyncInfrastructure/ValueObject/FeedTest.php diff --git a/composer.json b/composer.json index 2c3e16c..230dd44 100644 --- a/composer.json +++ b/composer.json @@ -9,11 +9,11 @@ ], "license": "MIT", "require": { - "php": "^8.1", + "php": "^8.2", "ext-json": "*", "sylius/sylius": "^1.12", - "symfony/serializer": "^5.0 || ^6.0", "symfony/lock": "^5.4 || ^6.0", + "symfony/serializer": "^5.0 || ^6.0", "symfony/webpack-encore-bundle": "^1.15" }, "require-dev": { @@ -49,7 +49,8 @@ "symfony/flex": "^2.2.2", "symfony/intl": "^5.4 || ^6.0", "symfony/web-profiler-bundle": "^5.4 || ^6.0", - "vimeo/psalm": "^4.27" + "theofidry/alice-data-fixtures": "^1.7", + "vimeo/psalm": "^5.26" }, "config": { "sort-packages": true, diff --git a/config/services/command.php b/config/services/command.php index 11a60ac..614b4eb 100644 --- a/config/services/command.php +++ b/config/services/command.php @@ -4,6 +4,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Webgriffe\SyliusClerkPlugin\Command\V2FeedGeneratorCommand; use Webgriffe\SyliusClerkPlugin\Command\FeedGeneratorCommand; use Webgriffe\SyliusClerkPlugin\Service\FeedGenerator; @@ -19,4 +20,13 @@ ]) ->tag('console.command') ; + + $services->set('webgriffe_sylius_clerk_plugin.command.feed_generator', V2FeedGeneratorCommand::class) + ->args([ + '$channelRepository' => service('sylius.repository.channel'), + '$productsFeedGenerator' => service('webgriffe_sylius_clerk_plugin.feed_generator.products'), + '$filesystem' => service('filesystem'), + ]) + ->tag('console.command') + ; }; diff --git a/config/services/generator.php b/config/services/generator.php index 31ecc8b..f7c9b11 100644 --- a/config/services/generator.php +++ b/config/services/generator.php @@ -4,6 +4,8 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Webgriffe\SyliusClerkPlugin\DataSyncInfrastructure\Enum\Resource; +use Webgriffe\SyliusClerkPlugin\DataSyncInfrastructure\Generator\ResourceFeedGenerator; use Webgriffe\SyliusClerkPlugin\QueryBuilder\CustomersQueryBuilderFactory; use Webgriffe\SyliusClerkPlugin\QueryBuilder\ProductsQueryBuilderFactory; use Webgriffe\SyliusClerkPlugin\QueryBuilder\TaxonsQueryBuilderFactory; @@ -24,4 +26,44 @@ service('serializer'), ]) ; + + $services->set('webgriffe_sylius_clerk_plugin.feed_generator.products', ResourceFeedGenerator::class) + ->args([ + service('webgriffe_sylius_clerk_plugin.provider.products'), + service('serializer'), + Resource::PRODUCTS, + ]) + ; + + $services->set('webgriffe_sylius_clerk_plugin.feed_generator.categories', ResourceFeedGenerator::class) + ->args([ + service('webgriffe_sylius_clerk_plugin.provider.categories'), + service('serializer'), + Resource::CATEGORIES, + ]) + ; + + $services->set('webgriffe_sylius_clerk_plugin.feed_generator.orders', ResourceFeedGenerator::class) + ->args([ + service('webgriffe_sylius_clerk_plugin.provider.orders'), + service('serializer'), + Resource::ORDERS, + ]) + ; + + $services->set('webgriffe_sylius_clerk_plugin.feed_generator.customers', ResourceFeedGenerator::class) + ->args([ + service('webgriffe_sylius_clerk_plugin.provider.customers'), + service('serializer'), + Resource::CUSTOMERS, + ]) + ; + + $services->set('webgriffe_sylius_clerk_plugin.feed_generator.pages', ResourceFeedGenerator::class) + ->args([ + service('webgriffe_sylius_clerk_plugin.provider.pages'), + service('serializer'), + Resource::PAGES, + ]) + ; }; diff --git a/config/services/provider.php b/config/services/provider.php index f123e4d..baaff0c 100644 --- a/config/services/provider.php +++ b/config/services/provider.php @@ -4,6 +4,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Webgriffe\SyliusClerkPlugin\DataSyncInfrastructure\Provider\QueryBuilderResourceProvider; use Webgriffe\SyliusClerkPlugin\Service\PrivateApiKeyProvider; use Webgriffe\SyliusClerkPlugin\Service\PublicApiKeyProvider; @@ -13,4 +14,34 @@ $services->set('webgriffe_sylius_clerk.provider.private_api_key', PrivateApiKeyProvider::class); $services->set('webgriffe_sylius_clerk.provider.public_api_key', PublicApiKeyProvider::class); + + $services->set('webgriffe_sylius_clerk_plugin.provider.products', QueryBuilderResourceProvider::class) + ->args([ + service('webgriffe_sylius_clerk_plugin.query_builder.products'), + ]) + ; + + $services->set('webgriffe_sylius_clerk_plugin.provider.categories', QueryBuilderResourceProvider::class) + ->args([ + service('webgriffe_sylius_clerk_plugin.query_builder.categories'), + ]) + ; + + $services->set('webgriffe_sylius_clerk_plugin.provider.orders', QueryBuilderResourceProvider::class) + ->args([ + service('webgriffe_sylius_clerk_plugin.query_builder.orders'), + ]) + ; + + $services->set('webgriffe_sylius_clerk_plugin.provider.customers', QueryBuilderResourceProvider::class) + ->args([ + service('webgriffe_sylius_clerk_plugin.query_builder.customers'), + ]) + ; + + $services->set('webgriffe_sylius_clerk_plugin.provider.pages', QueryBuilderResourceProvider::class) + ->args([ + service('webgriffe_sylius_clerk_plugin.query_builder.pages'), + ]) + ; }; diff --git a/config/services/query_builder.php b/config/services/query_builder.php index 0f9e649..5db8b58 100644 --- a/config/services/query_builder.php +++ b/config/services/query_builder.php @@ -4,6 +4,7 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator; +use Webgriffe\SyliusClerkPlugin\DataSyncInfrastructure\Doctrine\ORM\ProductsQueryBuilder; use Webgriffe\SyliusClerkPlugin\QueryBuilder\CustomersQueryBuilderFactory; use Webgriffe\SyliusClerkPlugin\QueryBuilder\OrdersQueryBuilderFactory; use Webgriffe\SyliusClerkPlugin\QueryBuilder\ProductsQueryBuilderFactory; @@ -35,4 +36,11 @@ service('sylius.repository.customer'), ]) ; + + $services->set('webgriffe_sylius_clerk_plugin.query_builder.products', ProductsQueryBuilder::class) + ->args([ + service('sylius.repository.product'), + service('event_dispatcher'), + ]) + ; }; diff --git a/src/Command/FeedGeneratorCommand.php b/src/Command/FeedGeneratorCommand.php index fa61a80..4a3f2b8 100644 --- a/src/Command/FeedGeneratorCommand.php +++ b/src/Command/FeedGeneratorCommand.php @@ -17,6 +17,9 @@ use Webgriffe\SyliusClerkPlugin\Service\FeedGeneratorInterface; use Webmozart\Assert\Assert; +/** + * @deprecated + */ final class FeedGeneratorCommand extends Command { use LockableTrait; diff --git a/src/Command/V2FeedGeneratorCommand.php b/src/Command/V2FeedGeneratorCommand.php new file mode 100644 index 0000000..1084d72 --- /dev/null +++ b/src/Command/V2FeedGeneratorCommand.php @@ -0,0 +1,85 @@ + $channelRepository + */ + public function __construct( + private readonly ChannelRepositoryInterface $channelRepository, + private readonly FeedGeneratorInterface $productsFeedGenerator, + private readonly Filesystem $filesystem, + private readonly string $feedsStorageDirectory, + ) { + parent::__construct(); + } + + protected function configure(): void + { + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->io = new SymfonyStyle($input, $output); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->io->writeln('Starting Clerk.io feed generation...'); + if (!$this->lock()) { + $this->io->error('The command is already running in another process, quitting.'); + + return Command::FAILURE; + } + + $this->filesystem->mkdir($this->feedsStorageDirectory); + /** @var ChannelInterface[] $channels */ + $channels = $this->channelRepository->findAll(); + foreach ($channels as $channel) { + foreach ($channel->getLocales() as $locale) { + $productsFeed = $this->productsFeedGenerator->generate($channel, (string) $locale->getCode()); + $feedFilePath = $this->getFeedFilePath($productsFeed); + + $this->io->writeln(sprintf('Writing feed to file: %s', $feedFilePath)); + $this->filesystem->dumpFile($feedFilePath, $productsFeed->getContent()); + } + } + + $this->io->success('Clerk.io feed generation completed successfully.'); + + return Command::SUCCESS; + } + + private function getFeedFilePath(Feed $productsFeed): string + { + return sprintf( + '%s/%s', + rtrim($this->feedsStorageDirectory, '/'), + ltrim($productsFeed->getFileName(), '/'), + ); + } +} diff --git a/src/DataSyncInfrastructure/Doctrine/ORM/Event/QueryBuilderEvent.php b/src/DataSyncInfrastructure/Doctrine/ORM/Event/QueryBuilderEvent.php new file mode 100644 index 0000000..8d8dce9 --- /dev/null +++ b/src/DataSyncInfrastructure/Doctrine/ORM/Event/QueryBuilderEvent.php @@ -0,0 +1,51 @@ +queryBuilder; + } + + public function getChannel(): ChannelInterface + { + return $this->channel; + } + + public function getLocaleCode(): string + { + return $this->localeCode; + } + + public function getModifiedAfter(): ?\DateTimeInterface + { + return $this->modifiedAfter; + } + + public function getLimit(): ?int + { + return $this->limit; + } + + public function getOffset(): ?int + { + return $this->offset; + } +} diff --git a/src/DataSyncInfrastructure/Doctrine/ORM/ProductsQueryBuilder.php b/src/DataSyncInfrastructure/Doctrine/ORM/ProductsQueryBuilder.php new file mode 100644 index 0000000..97de6a8 --- /dev/null +++ b/src/DataSyncInfrastructure/Doctrine/ORM/ProductsQueryBuilder.php @@ -0,0 +1,78 @@ + + */ +final readonly class ProductsQueryBuilder implements QueryBuilderInterface +{ + public function __construct( + private ProductRepository $productRepository, + private EventDispatcherInterface $eventDispatcher, + ) { + } + + public function getResource(): Resource + { + return Resource::PRODUCTS; + } + + public function getResult( + ChannelInterface $channel, + string $localeCode, + ?\DateTimeInterface $modifiedAfter = null, + ?int $limit = null, + ?int $offset = null, + ): array { + $queryBuilder = $this->productRepository->createQueryBuilder('p'); + + $queryBuilder + ->andWhere(':channel MEMBER OF p.channels') + ->setParameter('channel', $channel) + ; + $queryBuilder + ->leftJoin('p.translations', 't', 'WITH', 't.locale = :localeCode') + ->setParameter('localeCode', $localeCode) + ; + + if ($modifiedAfter !== null) { + $queryBuilder + ->andWhere('p.updatedAt > :modifiedAfter') + ->setParameter('modifiedAfter', $modifiedAfter) + ; + } + + if ($limit !== null) { + $queryBuilder->setMaxResults($limit); + } + + if ($offset !== null) { + $queryBuilder->setFirstResult($offset); + } + + $this->eventDispatcher->dispatch(new QueryBuilderEvent( + $queryBuilder, + $channel, + $localeCode, + $modifiedAfter, + $limit, + $offset, + )); + + /** @var ProductInterface[] $result */ + $result = $queryBuilder->getQuery()->getResult(); + + return $result; + } +} diff --git a/src/DataSyncInfrastructure/Enum/Resource.php b/src/DataSyncInfrastructure/Enum/Resource.php new file mode 100644 index 0000000..234ebf6 --- /dev/null +++ b/src/DataSyncInfrastructure/Enum/Resource.php @@ -0,0 +1,14 @@ +resourceProvider->provide($channel, $localeCode, $modifiedAfter, $limit, $offset); + + $payload = []; + foreach ($resources as $resource) { + $payload[] = $this->serializer->normalize( + $resource, + FeedGenerator::NORMALIZATION_FORMAT, + [ + 'channel' => $channel, + 'localeCode' => $localeCode, + ], + ); + } + + return new Feed( + resource: $this->resource, + content: $this->serializer->encode($payload, 'json'), + channel: $channel, + localeCode: $localeCode, + modifiedAfter: $modifiedAfter, + limit: $limit, + offset: $offset, + ); + } +} diff --git a/src/DataSyncInfrastructure/Model/QueryBuilderInterface.php b/src/DataSyncInfrastructure/Model/QueryBuilderInterface.php new file mode 100644 index 0000000..5ce2e39 --- /dev/null +++ b/src/DataSyncInfrastructure/Model/QueryBuilderInterface.php @@ -0,0 +1,27 @@ + + */ +final readonly class QueryBuilderResourceProvider implements ResourceProviderInterface +{ + public function __construct( + private QueryBuilderInterface $queryBuilder, + ) { + } + + public function provide( + ChannelInterface $channel, + string $localeCode, + ?\DateTimeInterface $modifiedAfter = null, + ?int $limit = null, + ?int $offset = null, + ): array { + return $this->queryBuilder->getResult( + $channel, + $localeCode, + $modifiedAfter, + $limit, + $offset, + ); + } +} diff --git a/src/DataSyncInfrastructure/Provider/ResourceProviderInterface.php b/src/DataSyncInfrastructure/Provider/ResourceProviderInterface.php new file mode 100644 index 0000000..92530d3 --- /dev/null +++ b/src/DataSyncInfrastructure/Provider/ResourceProviderInterface.php @@ -0,0 +1,24 @@ +resource; + } + + public function getContent(): string + { + return $this->content; + } + + public function getChannel(): ChannelInterface + { + return $this->channel; + } + + public function getLocaleCode(): string + { + return $this->localeCode; + } + + public function getModifiedAfter(): ?\DateTimeInterface + { + return $this->modifiedAfter; + } + + public function getLimit(): ?int + { + return $this->limit; + } + + public function getOffset(): ?int + { + return $this->offset; + } + + public function getFileName(): string + { + $channelCode = $this->getChannel()->getCode(); + Assert::stringNotEmpty($channelCode, 'Channel code must be set.'); + + $dir = sprintf( + '%s/%s/%s/', + $channelCode, + $this->getLocaleCode(), + strtolower($this->getResource()->name), + ); + + if ($this->getModifiedAfter() === null && $this->getLimit() === null && $this->getOffset() === null) { + return $dir . 'all.json'; + } + + $fileName = ''; + if ($this->getModifiedAfter() !== null) { + $fileName .= 'modified_after_' . $this->getModifiedAfter()->format(\DateTime::ATOM); + } + if ($this->getLimit() !== null) { + if ($this->getOffset() !== null) { + $fileName .= sprintf( + '%sfrom_%s_to_%s', + $fileName === '' ? '' : '_', + $this->getOffset(), + $this->getOffset() + $this->getLimit(), + ); + } else { + $fileName .= sprintf( + '%sto_%s', + $fileName === '' ? '' : '_', + $this->getLimit(), + ); + } + } elseif ($this->getOffset() !== null) { + $fileName .= sprintf( + '%sfrom_%s', + $fileName === '' ? '' : '_', + $this->getOffset(), + ); + } + + return sprintf( + '%s%s.json', + $dir, + $fileName, + ); + } +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 638f2c8..2799742 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -9,6 +9,9 @@ final class Configuration implements ConfigurationInterface { + /** + * @psalm-suppress UndefinedMethod + */ public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('webgriffe_sylius_clerk'); diff --git a/src/DependencyInjection/WebgriffeSyliusClerkExtension.php b/src/DependencyInjection/WebgriffeSyliusClerkExtension.php index 3f7ec71..27ce41c 100644 --- a/src/DependencyInjection/WebgriffeSyliusClerkExtension.php +++ b/src/DependencyInjection/WebgriffeSyliusClerkExtension.php @@ -26,6 +26,9 @@ public function load(array $configs, ContainerBuilder $container): void $generateFeedCommand = $container->getDefinition('webgriffe_sylius_clerk.command.generate_feed'); $generateFeedCommand->setArgument('$storagePath', $config['storage_feed_path']); + + $newGenerateFeedCommand = $container->getDefinition('webgriffe_sylius_clerk_plugin.command.feed_generator'); + $newGenerateFeedCommand->setArgument('$feedsStorageDirectory', $config['storage_feed_path']); } public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface diff --git a/symfony.lock b/symfony.lock index 4c7ba77..7fb5a6d 100644 --- a/symfony.lock +++ b/symfony.lock @@ -135,6 +135,18 @@ "config/routes/liip_imagine.yaml" ] }, + "nelmio/alice": { + "version": "3.13", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "3.3", + "ref": "42b52d2065dc3fde27912d502c18ca1926e35ae2" + }, + "files": [ + "config/packages/nelmio_alice.yaml" + ] + }, "nyholm/psr7": { "version": "1.8", "recipe": { @@ -484,6 +496,15 @@ "config/packages/workflow.yaml" ] }, + "theofidry/alice-data-fixtures": { + "version": "1.7", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "fe5a50faf580eb58f08ada2abe8afbd2d4941e05" + } + }, "willdurand/hateoas-bundle": { "version": "2.6", "recipe": { diff --git a/tests/Application/config/bundles.php b/tests/Application/config/bundles.php index cbf9132..f8ab89e 100644 --- a/tests/Application/config/bundles.php +++ b/tests/Application/config/bundles.php @@ -58,6 +58,8 @@ Sylius\Calendar\SyliusCalendarBundle::class => ['all' => true], Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], League\FlysystemBundle\FlysystemBundle::class => ['all' => true], + Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['test' => true, 'test_cached' => true], + Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['test' => true, 'test_cached' => true], ]; if (class_exists(Sylius\Abstraction\StateMachine\SyliusStateMachineAbstractionBundle::class)) { $bundles[Sylius\Abstraction\StateMachine\SyliusStateMachineAbstractionBundle::class] = ['all' => true]; diff --git a/tests/Application/config/packages/webgriffe_sylius_clerk.yaml b/tests/Application/config/packages/webgriffe_sylius_clerk.yaml deleted file mode 100644 index 4d79ade..0000000 --- a/tests/Application/config/packages/webgriffe_sylius_clerk.yaml +++ /dev/null @@ -1,5 +0,0 @@ -webgriffe_sylius_clerk: - stores: - - channel_code: WEB-US - public_api_key: public-key - private_api_key: 123abc diff --git a/tests/Application/config/packages/webgriffe_sylius_clerk_plugin.yaml b/tests/Application/config/packages/webgriffe_sylius_clerk_plugin.yaml new file mode 100644 index 0000000..d5f09fa --- /dev/null +++ b/tests/Application/config/packages/webgriffe_sylius_clerk_plugin.yaml @@ -0,0 +1,6 @@ +webgriffe_sylius_clerk: + storage_feed_path: '%kernel.project_dir%/public/feed' + stores: + - channel_code: WEB-US + public_api_key: public-key + private_api_key: 123abc diff --git a/tests/Integration/DataSyncInfrastructure/Doctrine/ORM/ProductsQueryBuilderTest.php b/tests/Integration/DataSyncInfrastructure/Doctrine/ORM/ProductsQueryBuilderTest.php new file mode 100644 index 0000000..b3d1bb6 --- /dev/null +++ b/tests/Integration/DataSyncInfrastructure/Doctrine/ORM/ProductsQueryBuilderTest.php @@ -0,0 +1,46 @@ +queryBuilder = self::getContainer()->get('webgriffe_sylius_clerk_plugin.query_builder.products'); + $this->channelRepository = self::getContainer()->get('sylius.repository.channel'); + /** @var PurgerLoader $fixtureLoader */ + $fixtureLoader = self::getContainer()->get('fidry_alice_data_fixtures.loader.doctrine'); + $ORMResourceFixturePath = __DIR__ . '/fixtures/ProductsQueryBuilderTest/' . $this->getName() . '.yaml'; + $fixtureLoader->load( + [$ORMResourceFixturePath], + [], + [], + PurgeMode::createDeleteMode(), + ); + } + + public function testItQueriesProducts(): void + { + $channel = $this->channelRepository->findOneByCode('DEFAULT'); + $products = $this->queryBuilder->getResult($channel, 'en_US'); + + $this->assertIsArray($products); + $this->assertCount(1, $products); + $product = $products[0]; + $this->assertEquals('STAR_WARS_TSHIRT_M', $product->getCode()); + } +} diff --git a/tests/Integration/DataSyncInfrastructure/Doctrine/ORM/fixtures/ProductsQueryBuilderTest/testItQueriesProducts.yaml b/tests/Integration/DataSyncInfrastructure/Doctrine/ORM/fixtures/ProductsQueryBuilderTest/testItQueriesProducts.yaml new file mode 100644 index 0000000..55cd0f5 --- /dev/null +++ b/tests/Integration/DataSyncInfrastructure/Doctrine/ORM/fixtures/ProductsQueryBuilderTest/testItQueriesProducts.yaml @@ -0,0 +1,43 @@ +Sylius\Component\Currency\Model\Currency: + USD: + code: 'USD' + +Sylius\Component\Locale\Model\Locale: + en_US: + code: "en_US" + it_IT: + code: "it_IT" + +Sylius\Component\Core\Model\Channel: + default: + code: 'DEFAULT' + name: 'United States' + enabled: true + description: 'United States' + defaultLocale: '@en_US' + locales: [ '@en_US' ] + baseCurrency: '@USD' + currencies: [ '@USD' ] + taxCalculationStrategy: 'order_items_based' + +Sylius\Component\Core\Model\ProductTranslation: + product-translation: + translatable: '@product' + locale: 'en_US' + name: 'Star Wars T-Shirt M' + slug: 'star-wars-t-shirt-m' + description: 'Star Wars T-Shirt M' + shortDescription: 'Star Wars T-Shirt M' + metaKeywords: 'Star Wars T-Shirt M' + metaDescription: 'Star Wars T-Shirt M' + +Sylius\Component\Core\Model\Product: + product: + fallbackLocale: "en_US" + currentLocale: "en_US" + code: "STAR_WARS_TSHIRT_M" + translations: + - '@product-translation' + channels: + - '@default' + diff --git a/tests/Unit/DataSyncInfrastructure/Generator/ResourceFeedGeneratorTest.php b/tests/Unit/DataSyncInfrastructure/Generator/ResourceFeedGeneratorTest.php new file mode 100644 index 0000000..6cafd10 --- /dev/null +++ b/tests/Unit/DataSyncInfrastructure/Generator/ResourceFeedGeneratorTest.php @@ -0,0 +1,80 @@ +setCode('PRODUCT_CODE'); + $resourceProvider = new InMemoryResourceProvider([$product]); + $this->generator = new ResourceFeedGenerator( + $resourceProvider, + new Serializer([new TestProductNormalizer()], [new JsonEncoder()]), + Resource::PRODUCTS, + ); + } + + public function testItGeneratesFeed(): void + { + $channel = new Channel(); + $feed = $this->generator->generate($channel, 'en_US'); + $this->assertSame(Resource::PRODUCTS, $feed->getResource()); + $this->assertEquals( + '[{"code":"PRODUCT_CODE"}]', + $feed->getContent(), + ); + $this->assertEquals($channel, $feed->getChannel()); + $this->assertEquals('en_US', $feed->getLocaleCode()); + } +} + +final readonly class InMemoryResourceProvider implements ResourceProviderInterface +{ + public function __construct( + private array $resources = [], + ) { + } + + public function provide( + ChannelInterface $channel, + string $localeCode, + ?\DateTimeInterface $modifiedAfter = null, + ?int $limit = null, + ?int $offset = null, + ): array { + return $this->resources; + } +} + +final readonly class TestProductNormalizer implements NormalizerInterface +{ + public function normalize($object, string $format = null, array $context = []): array + { + return [ + 'code' => $object->getCode(), + ]; + } + + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof Product; + } +} diff --git a/tests/Unit/DataSyncInfrastructure/Provider/QueryBuilderResourceProviderTest.php b/tests/Unit/DataSyncInfrastructure/Provider/QueryBuilderResourceProviderTest.php new file mode 100644 index 0000000..8da664e --- /dev/null +++ b/tests/Unit/DataSyncInfrastructure/Provider/QueryBuilderResourceProviderTest.php @@ -0,0 +1,52 @@ +provider = new QueryBuilderResourceProvider(new TestQueryBuilder()); + } + + public function testItProvidesDataFromQueryBuilder(): void + { + $result = $this->provider->provide(new Channel(), 'en_US'); + + $this->assertIsArray($result); + $this->assertEquals(['test1', 'test2'], $result); + } +} + +final class TestQueryBuilder implements QueryBuilderInterface +{ + public function getResult( + ChannelInterface $channel, + string $localeCode, + ?\DateTimeInterface $modifiedAfter = null, + ?int $limit = null, + ?int $offset = null + ): array { + return [ + 'test1', + 'test2', + ]; + } + + public function getResource(): Resource + { + return Resource::PRODUCTS; + } +} diff --git a/tests/Unit/DataSyncInfrastructure/ValueObject/FeedTest.php b/tests/Unit/DataSyncInfrastructure/ValueObject/FeedTest.php new file mode 100644 index 0000000..678d175 --- /dev/null +++ b/tests/Unit/DataSyncInfrastructure/ValueObject/FeedTest.php @@ -0,0 +1,93 @@ +channel = new Channel(); + $this->channel->setCode('CHANNEL_CODE'); + } + + public function testItFailsIfChannelCodeIsNotSetWhileGeneratingFileName(): void + { + $this->channel->setCode(null); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Channel code must be set.'); + + $feed = new Feed(Resource::PRODUCTS, 'content', $this->channel, 'en_US'); + $feed->getFileName(); + } + + public function testItGeneratesFileNameWithoutAnyOptionalParameter(): void + { + $feed = new Feed(Resource::PRODUCTS, 'content', $this->channel, 'en_US'); + + $this->assertSame('CHANNEL_CODE/en_US/products/all.json', $feed->getFileName()); + } + + public function testItGeneratesFileNameWithModifiedAfter(): void + { + $modifiedAfter = new \DateTime('2024-10-03 15:50:00', new \DateTimeZone('Europe/Rome')); + $feed = new Feed(Resource::PRODUCTS, 'content', $this->channel, 'en_US', $modifiedAfter); + + $this->assertSame('CHANNEL_CODE/en_US/products/modified_after_2024-10-03T15:50:00+02:00.json', $feed->getFileName()); + } + + public function testItGeneratesFileNameWithLimit(): void + { + $feed = new Feed(Resource::PRODUCTS, 'content', $this->channel, 'en_US', null, 10); + + $this->assertSame('CHANNEL_CODE/en_US/products/to_10.json', $feed->getFileName()); + } + + public function testItGeneratesFileNameWithOffset(): void + { + $feed = new Feed(Resource::PRODUCTS, 'content', $this->channel, 'en_US', null, null, 10); + + $this->assertSame('CHANNEL_CODE/en_US/products/from_10.json', $feed->getFileName()); + } + + public function testItGeneratesFileNameWithModifiedAfterAndLimit(): void + { + $modifiedAfter = new \DateTime('2024-10-03 15:50:00', new \DateTimeZone('Europe/Rome')); + $feed = new Feed(Resource::PRODUCTS, 'content', $this->channel, 'en_US', $modifiedAfter, 10); + + $this->assertSame('CHANNEL_CODE/en_US/products/modified_after_2024-10-03T15:50:00+02:00_to_10.json', $feed->getFileName()); + } + + public function testItGeneratesFileNameWithModifiedAfterAndOffset(): void + { + $modifiedAfter = new \DateTime('2024-10-03 15:50:00', new \DateTimeZone('Europe/Rome')); + $feed = new Feed(Resource::PRODUCTS, 'content', $this->channel, 'en_US', $modifiedAfter, null, 10); + + $this->assertSame('CHANNEL_CODE/en_US/products/modified_after_2024-10-03T15:50:00+02:00_from_10.json', $feed->getFileName()); + } + + public function testItGeneratesFileNameWithLimitAndOffset(): void + { + $feed = new Feed(Resource::PRODUCTS, 'content', $this->channel, 'en_US', null, 10, 10); + + $this->assertSame('CHANNEL_CODE/en_US/products/from_10_to_20.json', $feed->getFileName()); + } + + public function testItGeneratesFileNameWithModifiedAfterLimitAndOffset(): void + { + $modifiedAfter = new \DateTime('2024-10-03 15:50:00', new \DateTimeZone('Europe/Rome')); + $feed = new Feed(Resource::PRODUCTS, 'content', $this->channel, 'en_US', $modifiedAfter, 10, 10); + + $this->assertSame('CHANNEL_CODE/en_US/products/modified_after_2024-10-03T15:50:00+02:00_from_10_to_20.json', $feed->getFileName()); + } +}