diff --git a/spec/DataGenerator/Factory/Entity/ProductFactorySpec.php b/spec/DataGenerator/Factory/Entity/ProductFactorySpec.php index cf0d17b9..a8fb88ed 100644 --- a/spec/DataGenerator/Factory/Entity/ProductFactorySpec.php +++ b/spec/DataGenerator/Factory/Entity/ProductFactorySpec.php @@ -17,6 +17,7 @@ use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; +use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Resource\Factory\FactoryInterface; final class ProductFactorySpec extends ObjectBehavior @@ -36,6 +37,7 @@ public function it_creates_product( ProductVariantInterface $variant, ChannelInterface $channel, ProductInterface $product, + TaxonInterface $taxon, ): void { $productFactory->createNew()->willReturn($product); @@ -48,6 +50,7 @@ public function it_creates_product( $variant, $channel, new DateTime(), + $taxon, ) ->shouldReturn($product); } diff --git a/spec/DataGenerator/Generator/Bulk/Collection/ProductTaxonCollectionBulkGeneratorSpec.php b/spec/DataGenerator/Generator/Bulk/Collection/ProductTaxonCollectionBulkGeneratorSpec.php index a5fa718d..70160f79 100644 --- a/spec/DataGenerator/Generator/Bulk/Collection/ProductTaxonCollectionBulkGeneratorSpec.php +++ b/spec/DataGenerator/Generator/Bulk/Collection/ProductTaxonCollectionBulkGeneratorSpec.php @@ -47,6 +47,7 @@ public function it_generates_product_taxon_collection( $offset = 0; $taxons = [$taxon1, $taxon2]; + $context->getQuantity()->willReturn(100); $context->getIO()->willReturn($io); $taxonRepository->getEntityCount()->willReturn($entityCount); @@ -70,6 +71,7 @@ public function it_does_nothing_if_no_taxons_found( $limit = 100; $offset = 0; + $context->getQuantity()->willReturn(100); $context->getIO()->willReturn($io); $taxonRepository->getEntityCount()->willReturn($entityCount); @@ -78,6 +80,13 @@ public function it_does_nothing_if_no_taxons_found( $this->generate($context); } + public function it_does_nothing_if_quantity_equals_to_zero(ProductTaxonGeneratorContextInterface $context,): void + { + $context->getQuantity()->willReturn(0)->shouldBeCalled(); + + $this->generate($context); + } + public function it_throws_exception_on_invalid_context(ContextInterface $context): void { $this->shouldThrow(InvalidContextException::class) diff --git a/spec/DataGenerator/Generator/Bulk/Collection/WishlistProductCollectionBulkGeneratorSpec.php b/spec/DataGenerator/Generator/Bulk/Collection/WishlistProductCollectionBulkGeneratorSpec.php index 0970ceb7..87dc1e93 100644 --- a/spec/DataGenerator/Generator/Bulk/Collection/WishlistProductCollectionBulkGeneratorSpec.php +++ b/spec/DataGenerator/Generator/Bulk/Collection/WishlistProductCollectionBulkGeneratorSpec.php @@ -49,6 +49,7 @@ public function it_generates_wishlist_product_collection( $offset = 0; $wishlists = [$wishlist1, $wishlist2]; + $context->getQuantity()->willReturn(100); $context->getIO()->willReturn($io); $context->getChannel()->willReturn($channel); $wishlistRepository->getEntityCount($channel)->willReturn($entityCount); @@ -74,6 +75,7 @@ public function it_does_nothing_if_no_wishlists_found( $limit = 100; $offset = 0; + $context->getQuantity()->willReturn(100); $context->getIO()->willReturn($io); $context->getChannel()->willReturn($channel); $wishlistRepository->getEntityCount($channel)->willReturn($entityCount); @@ -83,6 +85,13 @@ public function it_does_nothing_if_no_wishlists_found( $this->generate($context); } + public function it_does_nothing_if_quantity_equals_to_zero(WishlistProductGeneratorContextInterface $context): void + { + $context->getQuantity()->willReturn(0)->shouldBeCalled(); + + $this->generate($context); + } + public function it_throws_exception_on_invalid_context(ContextInterface $context): void { $this->shouldThrow(InvalidContextException::class) diff --git a/spec/DataGenerator/Generator/Entity/ProductGeneratorSpec.php b/spec/DataGenerator/Generator/Entity/ProductGeneratorSpec.php index 5e1a152a..6ec4afbd 100644 --- a/spec/DataGenerator/Generator/Entity/ProductGeneratorSpec.php +++ b/spec/DataGenerator/Generator/Entity/ProductGeneratorSpec.php @@ -12,6 +12,7 @@ use BitBag\SyliusVueStorefront2Plugin\DataGenerator\ContextModel\Generator\GeneratorContextInterface; use BitBag\SyliusVueStorefront2Plugin\DataGenerator\ContextModel\Generator\ProductGeneratorContextInterface; +use BitBag\SyliusVueStorefront2Plugin\DataGenerator\Doctrine\Repository\TaxonRepositoryInterface; use BitBag\SyliusVueStorefront2Plugin\DataGenerator\Exception\InvalidContextException; use BitBag\SyliusVueStorefront2Plugin\DataGenerator\Factory\Entity\ChannelPricingFactoryInterface; use BitBag\SyliusVueStorefront2Plugin\DataGenerator\Factory\Entity\ProductFactoryInterface; @@ -24,6 +25,7 @@ use Sylius\Component\Core\Model\ChannelPricingInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; +use Sylius\Component\Core\Model\TaxonInterface; final class ProductGeneratorSpec extends ObjectBehavior { @@ -31,8 +33,9 @@ public function let( ProductFactoryInterface $productFactory, ProductVariantFactoryInterface $productVariantFactory, ChannelPricingFactoryInterface $channelPricingFactory, + TaxonRepositoryInterface $taxonRepository, ): void { - $this->beConstructedWith($productFactory, $productVariantFactory, $channelPricingFactory); + $this->beConstructedWith($productFactory, $productVariantFactory, $channelPricingFactory, $taxonRepository); } public function it_is_initializable(): void @@ -49,6 +52,8 @@ public function it_generates_product( ProductVariantInterface $productVariant, ProductGeneratorContextInterface $context, ProductInterface $product, + TaxonRepositoryInterface $taxonRepository, + TaxonInterface $taxon, ): void { $context->getChannel()->willReturn($channel); $channel->getCode()->willReturn(Argument::type('string')); @@ -61,11 +66,12 @@ public function it_generates_product( ->create( Argument::type('string'), Argument::type('string'), - $channelPricing->getWrappedObject() + $channelPricing ) ->willReturn($productVariant); - $context->getChannel()->willReturn($channel->getWrappedObject()); + $context->getChannel()->willReturn($channel); + $taxonRepository->getRandomTaxon()->willReturn($taxon); $productFactory ->create( @@ -73,9 +79,10 @@ public function it_generates_product( Argument::type('string'), Argument::type('string'), Argument::type('string'), - $productVariant->getWrappedObject(), - $channel->getWrappedObject(), + $productVariant, + $channel, Argument::type(DateTime::class), + $taxon, ) ->willReturn($product); diff --git a/src/DataGenerator/ConsoleCommand/BulkDataGeneratorInterface.php b/src/DataGenerator/ConsoleCommand/BulkDataGeneratorInterface.php index 16df4b03..e492953e 100644 --- a/src/DataGenerator/ConsoleCommand/BulkDataGeneratorInterface.php +++ b/src/DataGenerator/ConsoleCommand/BulkDataGeneratorInterface.php @@ -14,7 +14,7 @@ interface BulkDataGeneratorInterface { const DEFAULT_TAXONS_QTY = 5000; - const DEFAULT_MAX_TAXON_LEVEL = 20; + const DEFAULT_MAX_TAXON_LEVEL = 14; const DEFAULT_MAX_CHILDREN_PER_TAXON_LEVEL = 5; diff --git a/src/DataGenerator/Doctrine/Repository/TaxonRepository.php b/src/DataGenerator/Doctrine/Repository/TaxonRepository.php index 1f5e8828..e654625b 100644 --- a/src/DataGenerator/Doctrine/Repository/TaxonRepository.php +++ b/src/DataGenerator/Doctrine/Repository/TaxonRepository.php @@ -10,6 +10,7 @@ namespace BitBag\SyliusVueStorefront2Plugin\DataGenerator\Doctrine\Repository; +use BitBag\SyliusVueStorefront2Plugin\DataGenerator\Exception\NoTaxonFoundException; use Sylius\Bundle\TaxonomyBundle\Doctrine\ORM\TaxonRepository as BaseTaxonRepository; use Sylius\Component\Core\Model\TaxonInterface; @@ -63,8 +64,27 @@ public function findBatch( public function getEntityCount(): int { $queryBuilder = $this->createQueryBuilder('taxon') + ->andWhere('taxon.enabled = true') ->select('COUNT(taxon)'); return (int)$queryBuilder->getQuery()->getSingleScalarResult(); } + + public function getRandomTaxon(): TaxonInterface + { + $randomOffset = max(0, rand(0, $this->getEntityCount() - 1)); + + $result = $this->createQueryBuilder('taxon') + ->where('taxon.enabled = true') + ->setFirstResult($randomOffset) + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + + if ($result instanceof TaxonInterface) { + return $result; + } + + throw new NoTaxonFoundException(); + } } diff --git a/src/DataGenerator/Doctrine/Repository/TaxonRepositoryInterface.php b/src/DataGenerator/Doctrine/Repository/TaxonRepositoryInterface.php index bcb87e59..c89294f0 100644 --- a/src/DataGenerator/Doctrine/Repository/TaxonRepositoryInterface.php +++ b/src/DataGenerator/Doctrine/Repository/TaxonRepositoryInterface.php @@ -36,4 +36,6 @@ public function findBatch( ): array; public function getEntityCount(): int; + + public function getRandomTaxon(): TaxonInterface; } diff --git a/src/DataGenerator/Exception/NoTaxonFoundException.php b/src/DataGenerator/Exception/NoTaxonFoundException.php new file mode 100644 index 00000000..577d62e4 --- /dev/null +++ b/src/DataGenerator/Exception/NoTaxonFoundException.php @@ -0,0 +1,17 @@ +getIO(), $commandContext->getProductsQty(), @@ -63,7 +67,7 @@ private function productGeneratorContext( private function taxonGeneratorContext( DataGeneratorCommandContextInterface $commandContext, - ): TaxonGeneratorContext { + ): TaxonGeneratorContextInterface { return new TaxonGeneratorContext( $commandContext->getIO(), $commandContext->getTaxonsQty(), @@ -84,7 +88,7 @@ private function wishlistGeneratorContext( private function productTaxonGeneratorContext( DataGeneratorCommandContextInterface $commandContext, - ): ProductTaxonGeneratorContext { + ): ProductTaxonGeneratorContextInterface { return new ProductTaxonGeneratorContext( $commandContext->getIO(), $commandContext->getProductsPerTaxonQty(), @@ -95,7 +99,7 @@ private function productTaxonGeneratorContext( private function wishlistProductGeneratorContext( DataGeneratorCommandContextInterface $commandContext, - ): WishlistProductGeneratorContext { + ): WishlistProductGeneratorContextInterface { return new WishlistProductGeneratorContext( $commandContext->getIO(), $commandContext->getProductsPerWishlistQty(), diff --git a/src/DataGenerator/Factory/Entity/ProductFactory.php b/src/DataGenerator/Factory/Entity/ProductFactory.php index b5cba541..e2b3d23f 100644 --- a/src/DataGenerator/Factory/Entity/ProductFactory.php +++ b/src/DataGenerator/Factory/Entity/ProductFactory.php @@ -15,6 +15,7 @@ use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; +use Sylius\Component\Core\Model\TaxonInterface; use Sylius\Component\Resource\Factory\FactoryInterface; final class ProductFactory implements ProductFactoryInterface @@ -34,6 +35,7 @@ public function create( ProductVariantInterface $variant, ChannelInterface $channel, DateTimeInterface $createdAt, + TaxonInterface $mainTaxon, ): ProductInterface { /** @var ProductInterface $product */ $product = $this->productFactory->createNew(); @@ -46,6 +48,7 @@ public function create( $product->setCreatedAt($createdAt); $product->addChannel($channel); $product->addVariant($variant); + $product->setMainTaxon($mainTaxon); return $product; } diff --git a/src/DataGenerator/Factory/Entity/ProductFactoryInterface.php b/src/DataGenerator/Factory/Entity/ProductFactoryInterface.php index ed10713e..3bc118e1 100644 --- a/src/DataGenerator/Factory/Entity/ProductFactoryInterface.php +++ b/src/DataGenerator/Factory/Entity/ProductFactoryInterface.php @@ -14,6 +14,7 @@ use Sylius\Component\Core\Model\ChannelInterface; use Sylius\Component\Core\Model\ProductInterface; use Sylius\Component\Core\Model\ProductVariantInterface; +use Sylius\Component\Core\Model\TaxonInterface; interface ProductFactoryInterface { @@ -25,5 +26,6 @@ public function create( ProductVariantInterface $variant, ChannelInterface $channel, DateTimeInterface $createdAt, + TaxonInterface $mainTaxon, ): ProductInterface; } diff --git a/src/DataGenerator/Generator/Bulk/Collection/ProductTaxonCollectionBulkGenerator.php b/src/DataGenerator/Generator/Bulk/Collection/ProductTaxonCollectionBulkGenerator.php index 555d1d07..4c4ea037 100644 --- a/src/DataGenerator/Generator/Bulk/Collection/ProductTaxonCollectionBulkGenerator.php +++ b/src/DataGenerator/Generator/Bulk/Collection/ProductTaxonCollectionBulkGenerator.php @@ -37,6 +37,10 @@ public function generate(ContextInterface $context): void throw new InvalidContextException(); } + if ($context->getQuantity() === 0) { + return; + } + $io = $context->getIO(); $io->info(sprintf( diff --git a/src/DataGenerator/Generator/Bulk/Collection/WishlistProductCollectionBulkGenerator.php b/src/DataGenerator/Generator/Bulk/Collection/WishlistProductCollectionBulkGenerator.php index e5c17c36..b0c9119d 100644 --- a/src/DataGenerator/Generator/Bulk/Collection/WishlistProductCollectionBulkGenerator.php +++ b/src/DataGenerator/Generator/Bulk/Collection/WishlistProductCollectionBulkGenerator.php @@ -37,6 +37,10 @@ public function generate(ContextInterface $context): void throw new InvalidContextException(); } + if ($context->getQuantity() === 0) { + return; + } + $io = $context->getIO(); $io->info(sprintf( diff --git a/src/DataGenerator/Generator/Entity/ProductGenerator.php b/src/DataGenerator/Generator/Entity/ProductGenerator.php index e52457e1..77d44bf3 100644 --- a/src/DataGenerator/Generator/Entity/ProductGenerator.php +++ b/src/DataGenerator/Generator/Entity/ProductGenerator.php @@ -12,6 +12,7 @@ use BitBag\SyliusVueStorefront2Plugin\DataGenerator\ContextModel\Generator\GeneratorContextInterface; use BitBag\SyliusVueStorefront2Plugin\DataGenerator\ContextModel\Generator\ProductGeneratorContextInterface; +use BitBag\SyliusVueStorefront2Plugin\DataGenerator\Doctrine\Repository\TaxonRepositoryInterface; use BitBag\SyliusVueStorefront2Plugin\DataGenerator\Exception\InvalidContextException; use BitBag\SyliusVueStorefront2Plugin\DataGenerator\Factory\Entity\ChannelPricingFactoryInterface; use BitBag\SyliusVueStorefront2Plugin\DataGenerator\Factory\Entity\ProductFactoryInterface; @@ -28,16 +29,20 @@ final class ProductGenerator implements GeneratorInterface private ChannelPricingFactoryInterface $channelPricingFactory; + private TaxonRepositoryInterface $taxonRepository; + private Generator $faker; public function __construct( ProductFactoryInterface $productFactory, ProductVariantFactoryInterface $productVariantFactory, ChannelPricingFactoryInterface $channelPricingFactory, + TaxonRepositoryInterface $taxonRepository, ) { $this->productFactory = $productFactory; $this->productVariantFactory = $productVariantFactory; $this->channelPricingFactory = $channelPricingFactory; + $this->taxonRepository = $taxonRepository; $this->faker = Factory::create(); } @@ -47,9 +52,10 @@ public function generate(GeneratorContextInterface $context): ProductInterface throw new InvalidContextException(); } + $channel = $context->getChannel(); $channelPricing = $this->channelPricingFactory->create( $this->faker->randomNumber(), - $context->getChannel(), + $channel, ); $uuid = $this->faker->uuid; @@ -65,8 +71,9 @@ public function generate(GeneratorContextInterface $context): ProductInterface $this->faker->sentence(15), $this->faker->sentence(), $variant, - $context->getChannel(), - $this->faker->dateTimeBetween('-1 year') + $channel, + $this->faker->dateTimeBetween('-1 year'), + $this->taxonRepository->getRandomTaxon(), ); } } diff --git a/src/DataGenerator/Generator/Entity/TaxonGenerator.php b/src/DataGenerator/Generator/Entity/TaxonGenerator.php index e9826276..c9ca0c12 100644 --- a/src/DataGenerator/Generator/Entity/TaxonGenerator.php +++ b/src/DataGenerator/Generator/Entity/TaxonGenerator.php @@ -44,7 +44,7 @@ public function generate(GeneratorContextInterface $context): TaxonInterface } $translation = TaxonTranslationFactory::create( - $this->faker->sentence(3), + sprintf('%s %s', $this->faker->uuid, $this->faker->sentence(3)), $context::DEFAULT_LOCALE, ); diff --git a/src/DataGenerator/Generator/SimpleType/Integer/IntegerGenerator.php b/src/DataGenerator/Generator/SimpleType/Integer/IntegerGenerator.php index cefb4425..66de2e80 100644 --- a/src/DataGenerator/Generator/SimpleType/Integer/IntegerGenerator.php +++ b/src/DataGenerator/Generator/SimpleType/Integer/IntegerGenerator.php @@ -37,6 +37,6 @@ public function generateBiased( return $this->rand->rand($topValuesThreshold, $max); } - return $this->rand->rand($min, $topValuesThreshold - 1); + return $this->rand->rand($min, max(0, $topValuesThreshold - 1)); } } diff --git a/src/DataGenerator/Resources/services/generators.xml b/src/DataGenerator/Resources/services/generators.xml index e866107b..af9b117a 100644 --- a/src/DataGenerator/Resources/services/generators.xml +++ b/src/DataGenerator/Resources/services/generators.xml @@ -18,6 +18,7 @@ + compositePreFetcher = $preFetchedDataProvider; + } + + public function getCollection( + string $resourceClass, + string $operationName = null, + array $context = [], + ): iterable { + $collection = parent::getCollection($resourceClass, $operationName, $context); + + $ids = []; + /** @var ResourceInterface $item */ + foreach ($collection as $item) { + $ids[] = $item->getId(); + } + + $this->compositePreFetcher->preFetchData($ids, $context); + + return $collection; + } + + public function getCachedData( + string $identifier, + array $context, + ): array { + return $this->compositePreFetcher->getPreFetchedData($identifier, $context); + } +} diff --git a/src/DataProvider/CachedCollectionDataProviderInterface.php b/src/DataProvider/CachedCollectionDataProviderInterface.php new file mode 100644 index 00000000..2f4374a3 --- /dev/null +++ b/src/DataProvider/CachedCollectionDataProviderInterface.php @@ -0,0 +1,22 @@ +preFetchers = $preFetchers; + } + + public function preFetchData( + array $parentIds, + array $context, + ): void { + $attributes = $context['attributes'] ?? null; + if ($attributes === null) { + return; + } + + $attributes = $this->gatherAttributesToPreFetch($attributes); + + foreach (array_keys($attributes) as $attribute) { + /** @var RestrictedPreFetcherInterface $preFetcher */ + foreach ($this->preFetchers as $preFetcher) { + if ($preFetcher->supports($context, $attribute)) { + $preFetcher->preFetchData($parentIds, $context); + + break; + } + } + } + } + + public function getPreFetchedData( + string $identifier, + array $context, + ): array { + foreach ($this->preFetchers as $preFetcher) { + if ($preFetcher->supports($context)) { + return $preFetcher->getPreFetchedData($identifier, $context); + } + } + + return []; + } + + private function gatherAttributesToPreFetch(array $attributes): array + { + $filteredAttributes = $this->filterAttributes($attributes); + + foreach ($filteredAttributes as $attribute => $fields) { + $isCollection = is_array($fields['collection'] ?? null); + + if ($isCollection) { + $nestedAttributes = $this->gatherAttributesToPreFetch($fields['collection']); + } else { + $nestedAttributes = $this->gatherAttributesToPreFetch($fields['edges']['node']); + } + + $filteredAttributes = array_merge($filteredAttributes, $nestedAttributes); + } + + return $filteredAttributes; + } + + private function filterAttributes(array $attributes): array + { + return array_filter( + $attributes, + static fn($attr) => is_array($attr['collection'] ?? $attr['edges'] ?? null) + ); + } +} diff --git a/src/DataProvider/PreFetcher/PreFetcherInterface.php b/src/DataProvider/PreFetcher/PreFetcherInterface.php new file mode 100644 index 00000000..0731b966 --- /dev/null +++ b/src/DataProvider/PreFetcher/PreFetcherInterface.php @@ -0,0 +1,24 @@ +repository = $repository; + } + + public function preFetchData( + array $parentIds, + array $context, + ): void { + /** @var ChannelPricingInterface $result */ + foreach ($this->repository->findByProductIds($parentIds, $context) as $result) { + $this->preFetchedData[$result->getProductVariant()?->getCode()][] = $result; + } + } + + public function getPreFetchedData( + string $identifier, + ?array $context = [], + ): array { + return $this->preFetchedData[$identifier] ?? []; + } + + public function supports( + array $context, + ?string $attribute = null, + ): bool { + $resourceClass = $context['resource_class']; + + return is_a($resourceClass, ChannelPricingInterface::class, true) + || ($attribute === 'channelPricings' && is_a($resourceClass, ProductInterface::class, true)); + } +} diff --git a/src/DataProvider/PreFetcher/Product/ProductAttributePreFetcher.php b/src/DataProvider/PreFetcher/Product/ProductAttributePreFetcher.php new file mode 100644 index 00000000..ac4edbdb --- /dev/null +++ b/src/DataProvider/PreFetcher/Product/ProductAttributePreFetcher.php @@ -0,0 +1,55 @@ +repository = $repository; + } + + public function preFetchData( + array $parentIds, + array $context, + ): void { + /** @var ProductAttributeValueInterface $result */ + foreach ($this->repository->findByProductIds($parentIds, $context) as $result) { + $this->preFetchedData[$result->getProduct()?->getCode()][] = $result; + } + } + + public function getPreFetchedData( + string $identifier, + ?array $context = [], + ): array { + return $this->preFetchedData[$identifier] ?? []; + } + + public function supports( + array $context, + ?string $attribute = null, + ): bool { + $resourceClass = $context['resource_class']; + + return is_a($resourceClass, ProductAttributeValueInterface::class, true) + || ($attribute === 'attributes' && is_a($resourceClass, ProductInterface::class, true)); + } +} diff --git a/src/DataProvider/PreFetcher/Product/ProductImagePreFetcher.php b/src/DataProvider/PreFetcher/Product/ProductImagePreFetcher.php new file mode 100644 index 00000000..24c5c4ac --- /dev/null +++ b/src/DataProvider/PreFetcher/Product/ProductImagePreFetcher.php @@ -0,0 +1,54 @@ +repository = $repository; + } + + public function preFetchData( + array $parentIds, + array $context, + ): void { + foreach ($this->repository->findByProductIds($parentIds, $context) as $result) { + $this->preFetchedData[$result['code']][] = $result[0]; + } + } + + public function getPreFetchedData( + string $identifier, + ?array $context = [], + ): array { + return $this->preFetchedData[$identifier] ?? []; + } + + public function supports( + array $context, + ?string $attribute = null, + ): bool { + $resourceClass = $context['resource_class']; + + return is_a($resourceClass, ProductImageInterface::class, true) + || ($attribute === 'images' && is_a($resourceClass, ProductInterface::class, true)); + } +} diff --git a/src/DataProvider/PreFetcher/Product/ProductOptionsPreFetcherInterface.php b/src/DataProvider/PreFetcher/Product/ProductOptionsPreFetcherInterface.php new file mode 100644 index 00000000..2f8c3592 --- /dev/null +++ b/src/DataProvider/PreFetcher/Product/ProductOptionsPreFetcherInterface.php @@ -0,0 +1,26 @@ +repository = $repository; + } + + public function preFetchData( + array $parentIds, + array $context, + ): void { + if ($this->isPrefetched === true) { + return; + } + + $result = $this->repository->findOptionsByProductIds($parentIds, $context); + + /** @var ProductVariantInterface $result */ + foreach ($result as $variant) { + $this->prepareProductOptions($variant); + $this->prepareOptionValues($variant); + $this->prepareVariantOptionValues($variant); + } + + $this->isPrefetched = true; + } + + public function getPreFetchedData( + string $identifier, + array $context, + ): array { + return match ($context['property']) { + self::ELIGIBLE_ATTR_PRODUCT_OPTIONS => $this->productOptions[$identifier] ?? [], + self::ELIGIBLE_ATTR_PRODUCT_OPTION_VALUES => $this->optionValues[$identifier] ?? [], + self::ELIGIBLE_ATTR_VARIANT_OPTION_VALUES => $this->variantOptionValues[$identifier] ?? [], + default => [], + }; + } + + public function supports( + array $context, + ?string $attribute = null, + ): bool { + $resourceClass = $context['resource_class']; + + return is_a($resourceClass, ProductOptionInterface::class, true) + || is_a($resourceClass, ProductOptionValueInterface::class, true) + || (in_array($attribute, self::ELIGIBLE_ATTRIBUTES, true) + && is_a($resourceClass, ProductInterface::class, true)); + } + + private function prepareProductOptions(ProductVariantInterface $variant): void + { + foreach ($variant->getOptionValues() as $optionValue) { + $option = $optionValue->getOption(); + $this->productOptions[$variant->getProduct()?->getCode()][$option?->getCode()] = $option; + } + } + + private function prepareOptionValues(ProductVariantInterface $variant): void + { + foreach ($variant->getOptionValues() as $optionValue) { + $this->optionValues[$optionValue->getOption()?->getCode()][$optionValue->getCode()] = $optionValue; + } + } + + private function prepareVariantOptionValues(ProductVariantInterface $variant): void + { + $this->variantOptionValues[$variant->getCode()] = $variant->getOptionValues()->toArray(); + } +} diff --git a/src/DataProvider/PreFetcher/Product/ProductVariantPreFetcher.php b/src/DataProvider/PreFetcher/Product/ProductVariantPreFetcher.php new file mode 100644 index 00000000..6be0165c --- /dev/null +++ b/src/DataProvider/PreFetcher/Product/ProductVariantPreFetcher.php @@ -0,0 +1,55 @@ +repository = $repository; + } + + public function preFetchData( + array $parentIds, + array $context, + ): void { + /** @var ProductVariantInterface $result */ + foreach ($this->repository->findByProductIds($parentIds, $context) as $result) { + $this->preFetchedData[$result->getProduct()?->getCode()][] = $result; + } + } + + public function getPreFetchedData( + string $identifier, + ?array $context = [], + ): array { + return $this->preFetchedData[$identifier] ?? []; + } + + public function supports( + array $context, + ?string $attribute = null, + ): bool { + $resourceClass = $context['resource_class']; + + return is_a($resourceClass, ProductVariantInterface::class, true) + || ($attribute === 'variants' && is_a($resourceClass, ProductInterface::class, true)); + } +} diff --git a/src/DataProvider/PreFetcher/RestrictedPreFetcherInterface.php b/src/DataProvider/PreFetcher/RestrictedPreFetcherInterface.php new file mode 100644 index 00000000..34116a91 --- /dev/null +++ b/src/DataProvider/PreFetcher/RestrictedPreFetcherInterface.php @@ -0,0 +1,19 @@ +supports($resourceClass)) { + Assert::keyExists($identifiers, 'code'); + + /** @var ProductInterface[] $data */ + $data = $this->cachedCollectionDataProvider->getCachedData($identifiers['code'], $context); + + return new ArrayPaginator($data, 0, count($data)); + } + + return $this->decoratedSubresourceProvider->getSubresource($resourceClass, $identifiers, $context, $operationName); + } + + private function supports(string $resourceClass): bool + { + foreach (self::ELIGIBLE_ENTITIES as $entity) { + if (is_a($resourceClass, $entity, true)) { + return true; + } + } + + return false; + } +} diff --git a/src/DataProvider/SubresourceDataProviderInterface.php b/src/DataProvider/SubresourceDataProviderInterface.php new file mode 100644 index 00000000..b4100a81 --- /dev/null +++ b/src/DataProvider/SubresourceDataProviderInterface.php @@ -0,0 +1,31 @@ +addSelect('ovs') + ->join('o.variants', 'ovs'); + } + } +} diff --git a/src/Doctrine/Repository/Product/ChannelPricingRepository.php b/src/Doctrine/Repository/Product/ChannelPricingRepository.php new file mode 100644 index 00000000..3c8c0f5b --- /dev/null +++ b/src/Doctrine/Repository/Product/ChannelPricingRepository.php @@ -0,0 +1,49 @@ +entityManager = $entityManager; + } + + public function findByProductIds( + array $productIds, + array $context, + ): array { + $channel = $context[ContextKeys::CHANNEL]; + Assert::isInstanceOf($channel, ChannelInterface::class); + + return $this->entityManager->createQueryBuilder() + ->from(ChannelPricingInterface::class, 'channelPricing') + ->leftJoin('channelPricing.productVariant', 'variant') + ->leftJoin('variant.product', 'product') + ->leftJoin(ChannelInterface::class, 'channel', Join::WITH, 'channel.code = channelPricing.channelCode') + ->addSelect('channelPricing') + ->andWhere('product.id IN (:productIds)') + ->andWhere('channel = :channel') + ->setParameter('productIds', $productIds) + ->setParameter('channel', $channel) + ->getQuery() + ->getResult(); + } +} diff --git a/src/Doctrine/Repository/Product/ChannelPricingRepositoryInterface.php b/src/Doctrine/Repository/Product/ChannelPricingRepositoryInterface.php new file mode 100644 index 00000000..acd199a1 --- /dev/null +++ b/src/Doctrine/Repository/Product/ChannelPricingRepositoryInterface.php @@ -0,0 +1,19 @@ +getEntityManager(), $decoratedRepository->getClassMetadata()); + $this->decoratedRepository = $decoratedRepository; + } + + public function findByProductIds( + array $productIds, + array $context, + ): array { + $locale = $context[ContextKeys::LOCALE_CODE] ?? 'en_US'; + + return $this->createQueryBuilder('value') + ->leftJoin('value.attribute', 'attribute') + ->leftJoin('attribute.translations', 'translation') + ->leftJoin('value.subject', 'product') + ->addSelect('attribute') + ->addSelect('translation') + ->andWhere('product.id IN (:productIds)') + ->andWhere('translation.locale = :locale') + ->andWhere('value.localeCode = :locale') + ->setParameter('productIds', $productIds) + ->setParameter('locale', $locale) + ->getQuery() + ->getResult(); + } + + public function findByJsonChoiceKey(string $choiceKey): array + { + return $this->decoratedRepository->findByJsonChoiceKey($choiceKey); + } +} diff --git a/src/Doctrine/Repository/Product/ProductAttributeValueRepositoryInterface.php b/src/Doctrine/Repository/Product/ProductAttributeValueRepositoryInterface.php new file mode 100644 index 00000000..ec418854 --- /dev/null +++ b/src/Doctrine/Repository/Product/ProductAttributeValueRepositoryInterface.php @@ -0,0 +1,21 @@ +entityManager = $entityManager; + } + + public function findByProductIds( + array $productIds, + array $context, + ): array { + return $this->entityManager->createQueryBuilder() + ->from(ProductImageInterface::class, 'image') + ->join('image.owner', 'product') + ->select('image') + ->addSelect('product.code') + ->andWhere('product.id IN (:productIds)') + ->setParameter('productIds', $productIds) + ->getQuery() + ->getResult(); + } +} diff --git a/src/Doctrine/Repository/Product/ProductImageRepositoryInterface.php b/src/Doctrine/Repository/Product/ProductImageRepositoryInterface.php new file mode 100644 index 00000000..f4a6772f --- /dev/null +++ b/src/Doctrine/Repository/Product/ProductImageRepositoryInterface.php @@ -0,0 +1,19 @@ +createQueryBuilder('variant') + ->leftJoin('variant.product', 'product') + ->leftJoin('variant.channelPricings', 'channelPricing') + ->leftJoin(ChannelInterface::class, 'channel', Join::WITH, 'channel.code = channelPricing.channelCode') + ->leftJoin('channelPricing.appliedPromotions', 'appliedPromotion') + ->leftJoin('variant.translations', 'translation') + ->addSelect('translation') + ->addSelect('channelPricing') + ->addSelect('appliedPromotion') + ->andWhere('product.id IN (:productIds)') + ->andWhere('translation.locale = :locale') + ->andWhere('channel = :channel') + ->setParameter('productIds', $productIds) + ->setParameter('locale', $locale) + ->setParameter('channel', $channel) + ->getQuery() + ->getResult(); + } + + public function findOptionsByProductIds( + array $productIds, + array $context, + ): array { + $locale = $context[ContextKeys::LOCALE_CODE] ?? 'en_US'; + $channel = $context[ContextKeys::CHANNEL]; + Assert::isInstanceOf($channel, ChannelInterface::class); + + return $this->createQueryBuilder('variant') + ->leftJoin('variant.product', 'product') + ->leftJoin('product.options', 'productOption') + ->leftJoin('variant.optionValues', 'optionValue') + ->leftJoin('optionValue.translations', 'optionValueTranslation') + ->leftJoin('optionValue.option', 'option') + ->leftJoin('option.translations', 'optionTranslation') + ->addSelect('product') + ->addSelect('productOption') + ->addSelect('optionValue') + ->addSelect('optionValueTranslation') + ->addSelect('option') + ->addSelect('optionTranslation') + ->andWhere('product.id IN (:productIds)') + ->andWhere('optionValueTranslation.locale = :locale') + ->andWhere(':channel MEMBER OF product.channels') + ->setParameter('productIds', $productIds) + ->setParameter('locale', $locale) + ->setParameter('channel', $channel) + ->getQuery() + ->getResult(); + } +} diff --git a/src/Doctrine/Repository/Product/ProductVariantRepositoryInterface.php b/src/Doctrine/Repository/Product/ProductVariantRepositoryInterface.php new file mode 100644 index 00000000..db4ee3d4 --- /dev/null +++ b/src/Doctrine/Repository/Product/ProductVariantRepositoryInterface.php @@ -0,0 +1,24 @@ +refreshTokenRepository->findOneBy(['refreshToken' => $refreshTokenString]); - Assert::notNull($refreshToken); - - /** @var RefreshTokenInterface $refreshToken */ $this->validateRefreshToken($refreshToken, $refreshTokenString); /** @var ShopUserInterface $user */ diff --git a/src/Resources/services/data_providers.xml b/src/Resources/services/data_providers.xml index 55471bda..78544db6 100644 --- a/src/Resources/services/data_providers.xml +++ b/src/Resources/services/data_providers.xml @@ -40,5 +40,71 @@ We are hiring developers from all over the world. Join us and start your new, ex + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/services/doctrine_orm.xml b/src/Resources/services/doctrine_orm.xml index 53849a30..64975c29 100644 --- a/src/Resources/services/doctrine_orm.xml +++ b/src/Resources/services/doctrine_orm.xml @@ -21,6 +21,7 @@ We are hiring developers from all over the world. Join us and start your new, ex + @@ -30,9 +31,37 @@ We are hiring developers from all over the world. Join us and start your new, ex + + + + + + + + + + + + + + + + + + + + diff --git a/src/Resources/services/extension.xml b/src/Resources/services/extension.xml index 18226f83..1344b27b 100644 --- a/src/Resources/services/extension.xml +++ b/src/Resources/services/extension.xml @@ -18,5 +18,13 @@ We are hiring developers from all over the world. Join us and start your new, ex > + + + + diff --git a/tests/Application/config/packages/doctrine_migrations.yaml b/tests/Application/config/packages/doctrine_migrations.yaml index 7ffde0f7..1d0a6140 100644 --- a/tests/Application/config/packages/doctrine_migrations.yaml +++ b/tests/Application/config/packages/doctrine_migrations.yaml @@ -4,4 +4,4 @@ doctrine_migrations: table_name: sylius_migrations migrations_paths: - 'migrations': '%kernel.project_dir%/../../migrations' + 'App\Migrations': '%kernel.project_dir%/../../migrations' diff --git a/tests/Integration/DataGenerator/TaxonRepositoryTest.php b/tests/Integration/DataGenerator/TaxonRepositoryTest.php index 043b85b4..25b6012f 100644 --- a/tests/Integration/DataGenerator/TaxonRepositoryTest.php +++ b/tests/Integration/DataGenerator/TaxonRepositoryTest.php @@ -12,6 +12,7 @@ use ApiTestCase\JsonApiTestCase; use BitBag\SyliusVueStorefront2Plugin\DataGenerator\Doctrine\Repository\TaxonRepositoryInterface; +use Sylius\Component\Core\Model\TaxonInterface; final class TaxonRepositoryTest extends JsonApiTestCase { @@ -136,6 +137,17 @@ public function test_find_batch_with_offset_exceeded(): void $this->assertCount(0, $taxons); } + public function test_getting_random_shop_user(): void + { + $this->loadFixtures(); + + $repository = $this->getContainer() + ->get('bitbag.sylius_vue_storefront2_plugin.data_generator.repository.taxon_repository'); + + $taxon = $repository->getRandomTaxon(); + $this->assertInstanceOf(TaxonInterface::class, $taxon); + } + public function test_getting_entity_count(): void { $this->loadFixtures();