diff --git a/src/DependencyInjection/RegisterProductsSearchResultsProvidersCompilerPass.php b/src/DependencyInjection/RegisterProductsSearchResultsProvidersCompilerPass.php new file mode 100644 index 0000000000..9b2ab1c067 --- /dev/null +++ b/src/DependencyInjection/RegisterProductsSearchResultsProvidersCompilerPass.php @@ -0,0 +1,44 @@ +getDefinition(ProductSearchResultsProviderResolver::class); + $productSearchResultsProvidersDefinitions = $container->findTaggedServiceIds('shopsys.frontend_api.products_search_results_provider'); + + foreach ($productSearchResultsProvidersDefinitions as $serviceId => $tags) { + $priority = null; + + foreach ($tags as $tag) { + if (array_key_exists('priority', $tag)) { + $priority = $tag['priority']; + } + } + + if (!is_int($priority)) { + throw new ProductSearchResultsProviderPriorityNotSetException(sprintf('Service "%s" has not defined required tag priority or its type is not integer.', $serviceId)); + } + + $productSearchResultsProviderResolverDefinition->addMethodCall( + 'registerProductSearchResultsProvider', + [ + $serviceId, + $priority, + ], + ); + } + } +} diff --git a/src/Model/Product/ProductRepository.php b/src/Model/Product/ProductRepository.php index 4656521043..f898d4f618 100644 --- a/src/Model/Product/ProductRepository.php +++ b/src/Model/Product/ProductRepository.php @@ -26,11 +26,11 @@ public function __construct(protected readonly FrameworkProductRepository $produ */ public function getSellableByUuid(string $uuid, int $domainId, PricingGroup $pricingGroup): Product { - $qb = $this->productRepository->getAllSellableQueryBuilder($domainId, $pricingGroup); - $qb->andWhere('p.uuid = :uuid'); - $qb->setParameter('uuid', $uuid); + $queryBuilder = $this->productRepository->getAllSellableQueryBuilder($domainId, $pricingGroup); + $queryBuilder->andWhere('p.uuid = :uuid'); + $queryBuilder->setParameter('uuid', $uuid); - $product = $qb->getQuery()->getOneOrNullResult(); + $product = $queryBuilder->getQuery()->getOneOrNullResult(); if ($product === null) { throw new ProductNotFoundException( diff --git a/src/Model/Resolver/Products/Search/Exception/NoProductSearchResultsProviderEnabledOnDomainException.php b/src/Model/Resolver/Products/Search/Exception/NoProductSearchResultsProviderEnabledOnDomainException.php new file mode 100644 index 0000000000..f8004a3139 --- /dev/null +++ b/src/Model/Resolver/Products/Search/Exception/NoProductSearchResultsProviderEnabledOnDomainException.php @@ -0,0 +1,18 @@ +setDefaultFirstOffsetIfNecessary($argument); + + $productFilterData = $this->productFilterFacade->getValidatedProductFilterDataForAll( + $argument, + ); + + $productSearchResultsProvider = $this->productSearchResultsProviderResolver->getProductsSearchResultsProviderByDomainId($this->domain->getId()); + + return $productSearchResultsProvider->getProductsSearchResults($argument, $productFilterData); + } +} diff --git a/src/Model/Resolver/Products/ProductSearchQuery.php b/src/Model/Resolver/Products/Search/ProductSearchResultsProvider.php similarity index 56% rename from src/Model/Resolver/Products/ProductSearchQuery.php rename to src/Model/Resolver/Products/Search/ProductSearchResultsProvider.php index ae4175b64b..c888e95c27 100644 --- a/src/Model/Resolver/Products/ProductSearchQuery.php +++ b/src/Model/Resolver/Products/Search/ProductSearchResultsProvider.php @@ -2,42 +2,44 @@ declare(strict_types=1); -namespace Shopsys\FrontendApiBundle\Model\Resolver\Products; +namespace Shopsys\FrontendApiBundle\Model\Resolver\Products\Search; use Overblog\GraphQLBundle\Definition\Argument; +use Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData; +use Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterDataFactory; use Shopsys\FrameworkBundle\Model\Product\Listing\ProductListOrderingConfig; use Shopsys\FrontendApiBundle\Model\Product\Connection\ProductConnection; use Shopsys\FrontendApiBundle\Model\Product\Connection\ProductConnectionFactory; -use Shopsys\FrontendApiBundle\Model\Product\Filter\ProductFilterFacade; use Shopsys\FrontendApiBundle\Model\Product\ProductFacade; -use Shopsys\FrontendApiBundle\Model\Resolver\AbstractQuery; +use Shopsys\FrontendApiBundle\Model\Resolver\Products\ProductOrderingModeProvider; -class ProductSearchQuery extends AbstractQuery +class ProductSearchResultsProvider implements ProductSearchResultsProviderInterface { /** - * @param \Shopsys\FrontendApiBundle\Model\Product\Filter\ProductFilterFacade $productFilterFacade + * @param \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterDataFactory $productFilterDataFactory * @param \Shopsys\FrontendApiBundle\Model\Product\Connection\ProductConnectionFactory $productConnectionFactory * @param \Shopsys\FrontendApiBundle\Model\Product\ProductFacade $productFacade + * @param \Shopsys\FrontendApiBundle\Model\Resolver\Products\ProductOrderingModeProvider $productOrderingModeProvider */ public function __construct( - protected readonly ProductFilterFacade $productFilterFacade, + protected readonly ProductFilterDataFactory $productFilterDataFactory, protected readonly ProductConnectionFactory $productConnectionFactory, protected readonly ProductFacade $productFacade, + protected readonly ProductOrderingModeProvider $productOrderingModeProvider, ) { } /** * @param \Overblog\GraphQLBundle\Definition\Argument $argument + * @param \Shopsys\FrameworkBundle\Model\Product\Filter\ProductFilterData $productFilterData * @return \Shopsys\FrontendApiBundle\Model\Product\Connection\ProductConnection */ - public function productsSearchQuery(Argument $argument): ProductConnection - { + public function getProductsSearchResults( + Argument $argument, + ProductFilterData $productFilterData, + ): ProductConnection { $search = $argument['search'] ?? ''; - $productFilterData = $this->productFilterFacade->getValidatedProductFilterDataForAll( - $argument, - ); - return $this->productConnectionFactory->createConnectionForAll( function ($offset, $limit) use ($search, $productFilterData) { return $this->productFacade->getFilteredProductsOnCurrentDomain( @@ -51,7 +53,16 @@ function ($offset, $limit) use ($search, $productFilterData) { $this->productFacade->getFilteredProductsCountOnCurrentDomain($productFilterData, $search), $argument, $productFilterData, - ProductsQuery::getOrderingModeFromArgument($argument), + $this->productOrderingModeProvider->getOrderingModeFromArgument($argument), ); } + + /** + * @param int $domainId + * @return bool + */ + public function isEnabledOnDomain(int $domainId): bool + { + return true; + } } diff --git a/src/Model/Resolver/Products/Search/ProductSearchResultsProviderInterface.php b/src/Model/Resolver/Products/Search/ProductSearchResultsProviderInterface.php new file mode 100644 index 0000000000..27de7365d4 --- /dev/null +++ b/src/Model/Resolver/Products/Search/ProductSearchResultsProviderInterface.php @@ -0,0 +1,28 @@ + + */ + protected array $productSearchResultsProvidersServiceIdByPriority = []; + + /** + * @param \Shopsys\FrontendApiBundle\Model\Resolver\Products\Search\ProductSearchResultsProviderInterface[] $productSearchResultsProviders + */ + public function __construct( + protected readonly iterable $productSearchResultsProviders, + ) { + } + + /** + * @param int $domainId + * @return \Shopsys\FrontendApiBundle\Model\Resolver\Products\Search\ProductSearchResultsProviderInterface + */ + public function getProductsSearchResultsProviderByDomainId( + int $domainId, + ): ProductSearchResultsProviderInterface { + Assert::allIsInstanceOf($this->productSearchResultsProviders, ProductSearchResultsProviderInterface::class); + + foreach ($this->getProductsSearchResultsProvidersOrderedByPriority() as $productSearchResultsProvider) { + if ($productSearchResultsProvider->isEnabledOnDomain($domainId)) { + return $productSearchResultsProvider; + } + } + + throw new NoProductSearchResultsProviderEnabledOnDomainException($domainId); + } + + /** + * @return \Shopsys\FrontendApiBundle\Model\Resolver\Products\Search\ProductSearchResultsProviderInterface[] + */ + protected function getProductsSearchResultsProvidersOrderedByPriority(): array + { + krsort($this->productSearchResultsProvidersServiceIdByPriority, SORT_NUMERIC); + + $productSearchResultsProvidersOrderedByPriority = []; + + foreach ($this->productSearchResultsProvidersServiceIdByPriority as $serviceId) { + foreach ($this->productSearchResultsProviders as $productSearchResultsProvider) { + if ($productSearchResultsProvider instanceof $serviceId) { + $productSearchResultsProvidersOrderedByPriority[] = $productSearchResultsProvider; + } + } + } + + return $productSearchResultsProvidersOrderedByPriority; + } + + /** + * @param string $serviceId + * @param int $priority + */ + public function registerProductSearchResultsProvider(string $serviceId, int $priority): void + { + if (array_key_exists($priority, $this->productSearchResultsProvidersServiceIdByPriority)) { + throw new ProductSearchResultsProviderWithSamePriorityAlreadyExistsException($serviceId, $priority); + } + + $this->productSearchResultsProvidersServiceIdByPriority[$priority] = $serviceId; + } +} diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 5931672f5b..a46811c426 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -54,3 +54,11 @@ services: Shopsys\FrontendApiBundle\Component\ExpressionLanguage\DynamicPaginationComplexityExpressionFunction: tags: [ 'overblog_graphql.expression_function' ] + + Shopsys\FrontendApiBundle\Model\Resolver\Products\Search\ProductSearchResultsProviderResolver: + arguments: + $productSearchResultsProviders: !tagged 'shopsys.frontend_api.products_search_results_provider' + + Shopsys\FrontendApiBundle\Model\Resolver\Products\Search\ProductSearchResultsProvider: + tags: + - { name: 'shopsys.frontend_api.products_search_results_provider', priority: 1 } diff --git a/src/ShopsysFrontendApiBundle.php b/src/ShopsysFrontendApiBundle.php index 40b28146e0..b2481ac3db 100644 --- a/src/ShopsysFrontendApiBundle.php +++ b/src/ShopsysFrontendApiBundle.php @@ -4,8 +4,19 @@ namespace Shopsys\FrontendApiBundle; +use Shopsys\FrontendApiBundle\DependencyInjection\RegisterProductsSearchResultsProvidersCompilerPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class ShopsysFrontendApiBundle extends Bundle { + /** + * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container + */ + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container->addCompilerPass(new RegisterProductsSearchResultsProvidersCompilerPass()); + } }