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

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