vendor/shopware/core/Content/Product/SalesChannel/Detail/ProductDetailRoute.php line 73

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Product\SalesChannel\Detail;
  3. use Shopware\Core\Content\Category\Service\CategoryBreadcrumbBuilder;
  4. use Shopware\Core\Content\Cms\DataResolver\ResolverContext\EntityResolverContext;
  5. use Shopware\Core\Content\Cms\SalesChannel\SalesChannelCmsPageLoaderInterface;
  6. use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition;
  7. use Shopware\Core\Content\Product\Exception\ProductNotFoundException;
  8. use Shopware\Core\Content\Product\SalesChannel\AbstractProductCloseoutFilterFactory;
  9. use Shopware\Core\Content\Product\SalesChannel\ProductAvailableFilter;
  10. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductDefinition;
  11. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  17. use Shopware\Core\Framework\Log\Package;
  18. use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
  19. use Shopware\Core\Profiling\Profiler;
  20. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
  21. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  22. use Shopware\Core\System\SystemConfig\SystemConfigService;
  23. use Symfony\Component\HttpFoundation\Request;
  24. use Symfony\Component\Routing\Annotation\Route;
  25. #[Route(defaults: ['_routeScope' => ['store-api']])]
  26. #[Package('inventory')]
  27. class ProductDetailRoute extends AbstractProductDetailRoute
  28. {
  29.     /**
  30.      * @internal
  31.      */
  32.     public function __construct(
  33.         private readonly SalesChannelRepository $productRepository,
  34.         private readonly SystemConfigService $config,
  35.         private readonly ProductConfiguratorLoader $configuratorLoader,
  36.         private readonly CategoryBreadcrumbBuilder $breadcrumbBuilder,
  37.         private readonly SalesChannelCmsPageLoaderInterface $cmsPageLoader,
  38.         private readonly SalesChannelProductDefinition $productDefinition,
  39.         private readonly AbstractProductCloseoutFilterFactory $productCloseoutFilterFactory
  40.     ) {
  41.     }
  42.     public function getDecorated(): AbstractProductDetailRoute
  43.     {
  44.         throw new DecorationPatternException(self::class);
  45.     }
  46.     #[Route(path'/store-api/product/{productId}'name'store-api.product.detail'methods: ['POST'], defaults: ['_entity' => 'product'])]
  47.     public function load(string $productIdRequest $requestSalesChannelContext $contextCriteria $criteria): ProductDetailRouteResponse
  48.     {
  49.         return Profiler::trace('product-detail-route', function () use ($productId$request$context$criteria) {
  50.             $mainVariantId $this->checkVariantListingConfig($productId$context);
  51.             $productId $mainVariantId ?? $this->findBestVariant($productId$context);
  52.             $this->addFilters($context$criteria);
  53.             $criteria->setIds([$productId]);
  54.             $criteria->setTitle('product-detail-route');
  55.             $product $this->productRepository
  56.                 ->search($criteria$context)
  57.                 ->first();
  58.             if (!($product instanceof SalesChannelProductEntity)) {
  59.                 throw new ProductNotFoundException($productId);
  60.             }
  61.             $product->setSeoCategory(
  62.                 $this->breadcrumbBuilder->getProductSeoCategory($product$context)
  63.             );
  64.             $configurator $this->configuratorLoader->load($product$context);
  65.             $pageId $product->getCmsPageId();
  66.             if ($pageId) {
  67.                 // clone product to prevent recursion encoding (see NEXT-17603)
  68.                 $resolverContext = new EntityResolverContext($context$request$this->productDefinition, clone $product);
  69.                 $pages $this->cmsPageLoader->load(
  70.                     $request,
  71.                     $this->createCriteria($pageId$request),
  72.                     $context,
  73.                     $product->getTranslation('slotConfig'),
  74.                     $resolverContext
  75.                 );
  76.                 if ($page $pages->first()) {
  77.                     $product->setCmsPage($page);
  78.                 }
  79.             }
  80.             return new ProductDetailRouteResponse($product$configurator);
  81.         });
  82.     }
  83.     private function addFilters(SalesChannelContext $contextCriteria $criteria): void
  84.     {
  85.         $criteria->addFilter(
  86.             new ProductAvailableFilter($context->getSalesChannel()->getId(), ProductVisibilityDefinition::VISIBILITY_LINK)
  87.         );
  88.         $salesChannelId $context->getSalesChannel()->getId();
  89.         $hideCloseoutProductsWhenOutOfStock $this->config->get('core.listing.hideCloseoutProductsWhenOutOfStock'$salesChannelId);
  90.         if ($hideCloseoutProductsWhenOutOfStock) {
  91.             $filter $this->productCloseoutFilterFactory->create($context);
  92.             $filter->addQuery(new EqualsFilter('product.parentId'null));
  93.             $criteria->addFilter($filter);
  94.         }
  95.     }
  96.     /**
  97.      * @throws InconsistentCriteriaIdsException
  98.      */
  99.     private function checkVariantListingConfig(string $productIdSalesChannelContext $context): ?string
  100.     {
  101.         /** @var SalesChannelProductEntity|null $product */
  102.         $product $this->productRepository->search(new Criteria([$productId]), $context)->first();
  103.         if ($product === null || $product->getParentId() !== null) {
  104.             return null;
  105.         }
  106.         if (($listingConfig $product->getVariantListingConfig()) === null || $listingConfig->getDisplayParent() !== true) {
  107.             return null;
  108.         }
  109.         return $listingConfig->getMainVariantId();
  110.     }
  111.     /**
  112.      * @throws InconsistentCriteriaIdsException
  113.      */
  114.     private function findBestVariant(string $productIdSalesChannelContext $context): string
  115.     {
  116.         $criteria = (new Criteria())
  117.             ->addFilter(new EqualsFilter('product.parentId'$productId))
  118.             ->addSorting(new FieldSorting('product.price'))
  119.             ->addSorting(new FieldSorting('product.available'))
  120.             ->setLimit(1);
  121.         $criteria->setTitle('product-detail-route::find-best-variant');
  122.         $variantId $this->productRepository->searchIds($criteria$context);
  123.         return $variantId->firstId() ?? $productId;
  124.     }
  125.     private function createCriteria(string $pageIdRequest $request): Criteria
  126.     {
  127.         $criteria = new Criteria([$pageId]);
  128.         $criteria->setTitle('product::cms-page');
  129.         $slots $request->get('slots');
  130.         if (\is_string($slots)) {
  131.             $slots explode('|'$slots);
  132.         }
  133.         if (!empty($slots) && \is_array($slots)) {
  134.             $criteria
  135.                 ->getAssociation('sections.blocks')
  136.                 ->addFilter(new EqualsAnyFilter('slots.id'$slots));
  137.         }
  138.         return $criteria;
  139.     }
  140. }