src/Core/Content/Product/SalesChannel/CrossSelling/ProductCrossSellingRoute.php line 50

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\Product\SalesChannel\CrossSelling;
  3. use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingCollection;
  4. use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingDefinition;
  5. use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingEntity;
  6. use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition;
  7. use Shopware\Core\Content\Product\Events\ProductCrossSellingCriteriaLoadEvent;
  8. use Shopware\Core\Content\Product\Events\ProductCrossSellingIdsCriteriaEvent;
  9. use Shopware\Core\Content\Product\Events\ProductCrossSellingsLoadedEvent;
  10. use Shopware\Core\Content\Product\Events\ProductCrossSellingStreamCriteriaEvent;
  11. use Shopware\Core\Content\Product\ProductCollection;
  12. use Shopware\Core\Content\Product\SalesChannel\AbstractProductCloseoutFilterFactory;
  13. use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingLoader;
  14. use Shopware\Core\Content\Product\SalesChannel\ProductAvailableFilter;
  15. use Shopware\Core\Content\ProductStream\Service\ProductStreamBuilderInterface;
  16. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  20. use Shopware\Core\Framework\Log\Package;
  21. use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
  22. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
  23. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  24. use Shopware\Core\System\SystemConfig\SystemConfigService;
  25. use Symfony\Component\HttpFoundation\Request;
  26. use Symfony\Component\Routing\Annotation\Route;
  27. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  28. #[Route(defaults: ['_routeScope' => ['store-api']])]
  29. #[Package('inventory')]
  30. class ProductCrossSellingRoute extends AbstractProductCrossSellingRoute
  31. {
  32.     /**
  33.      * @internal
  34.      */
  35.     public function __construct(private readonly EntityRepository $crossSellingRepository, private readonly EventDispatcherInterface $eventDispatcher, private readonly ProductStreamBuilderInterface $productStreamBuilder, private readonly SalesChannelRepository $productRepository, private readonly SystemConfigService $systemConfigService, private readonly ProductListingLoader $listingLoader, private readonly AbstractProductCloseoutFilterFactory $productCloseoutFilterFactory)
  36.     {
  37.     }
  38.     public function getDecorated(): AbstractProductCrossSellingRoute
  39.     {
  40.         throw new DecorationPatternException(self::class);
  41.     }
  42.     #[Route(path'/store-api/product/{productId}/cross-selling'name'store-api.product.cross-selling'methods: ['POST'], defaults: ['_entity' => 'product'])]
  43.     public function load(string $productIdRequest $requestSalesChannelContext $contextCriteria $criteria): ProductCrossSellingRouteResponse
  44.     {
  45.         $crossSellings $this->loadCrossSellings($productId$context);
  46.         $elements = new CrossSellingElementCollection();
  47.         foreach ($crossSellings as $crossSelling) {
  48.             $clone = clone $criteria;
  49.             if ($this->useProductStream($crossSelling)) {
  50.                 $element $this->loadByStream($crossSelling$context$clone);
  51.             } else {
  52.                 $element $this->loadByIds($crossSelling$context$clone);
  53.             }
  54.             $elements->add($element);
  55.         }
  56.         $this->eventDispatcher->dispatch(new ProductCrossSellingsLoadedEvent($elements$context));
  57.         return new ProductCrossSellingRouteResponse($elements);
  58.     }
  59.     private function loadCrossSellings(string $productIdSalesChannelContext $context): ProductCrossSellingCollection
  60.     {
  61.         $criteria = new Criteria();
  62.         $criteria->setTitle('product-cross-selling-route');
  63.         $criteria
  64.             ->addAssociation('assignedProducts')
  65.             ->addFilter(new EqualsFilter('product.id'$productId))
  66.             ->addFilter(new EqualsFilter('active'1))
  67.             ->addSorting(new FieldSorting('position'FieldSorting::ASCENDING));
  68.         $this->eventDispatcher->dispatch(
  69.             new ProductCrossSellingCriteriaLoadEvent($criteria$context)
  70.         );
  71.         /** @var ProductCrossSellingCollection $crossSellings */
  72.         $crossSellings $this->crossSellingRepository
  73.             ->search($criteria$context->getContext())
  74.             ->getEntities();
  75.         return $crossSellings;
  76.     }
  77.     private function loadByStream(ProductCrossSellingEntity $crossSellingSalesChannelContext $contextCriteria $criteria): CrossSellingElement
  78.     {
  79.         /** @var string $productStreamId */
  80.         $productStreamId $crossSelling->getProductStreamId();
  81.         $filters $this->productStreamBuilder->buildFilters(
  82.             $productStreamId,
  83.             $context->getContext()
  84.         );
  85.         $criteria->addFilter(...$filters)
  86.             ->setOffset(0)
  87.             ->setLimit($crossSelling->getLimit())
  88.             ->addSorting($crossSelling->getSorting());
  89.         $criteria $this->handleAvailableStock($criteria$context);
  90.         $this->eventDispatcher->dispatch(
  91.             new ProductCrossSellingStreamCriteriaEvent($crossSelling$criteria$context)
  92.         );
  93.         $searchResult $this->listingLoader->load($criteria$context);
  94.         /** @var ProductCollection $products */
  95.         $products $searchResult->getEntities();
  96.         $element = new CrossSellingElement();
  97.         $element->setCrossSelling($crossSelling);
  98.         $element->setProducts($products);
  99.         $element->setStreamId($crossSelling->getProductStreamId());
  100.         $element->setTotal($products->count());
  101.         return $element;
  102.     }
  103.     private function loadByIds(ProductCrossSellingEntity $crossSellingSalesChannelContext $contextCriteria $criteria): CrossSellingElement
  104.     {
  105.         $element = new CrossSellingElement();
  106.         $element->setCrossSelling($crossSelling);
  107.         $element->setProducts(new ProductCollection());
  108.         $element->setTotal(0);
  109.         if (!$crossSelling->getAssignedProducts()) {
  110.             return $element;
  111.         }
  112.         $crossSelling->getAssignedProducts()->sortByPosition();
  113.         $ids array_values($crossSelling->getAssignedProducts()->getProductIds());
  114.         $filter = new ProductAvailableFilter(
  115.             $context->getSalesChannel()->getId(),
  116.             ProductVisibilityDefinition::VISIBILITY_LINK
  117.         );
  118.         if (!\count($ids)) {
  119.             return $element;
  120.         }
  121.         $criteria->setIds($ids);
  122.         $criteria->addFilter($filter);
  123.         $criteria->addAssociation('options.group');
  124.         $criteria $this->handleAvailableStock($criteria$context);
  125.         $this->eventDispatcher->dispatch(
  126.             new ProductCrossSellingIdsCriteriaEvent($crossSelling$criteria$context)
  127.         );
  128.         $result $this->productRepository
  129.             ->search($criteria$context);
  130.         /** @var ProductCollection $products */
  131.         $products $result->getEntities();
  132.         $products->sortByIdArray($ids);
  133.         $element->setProducts($products);
  134.         $element->setTotal(\count($products));
  135.         return $element;
  136.     }
  137.     private function handleAvailableStock(Criteria $criteriaSalesChannelContext $context): Criteria
  138.     {
  139.         $salesChannelId $context->getSalesChannel()->getId();
  140.         $hide $this->systemConfigService->get('core.listing.hideCloseoutProductsWhenOutOfStock'$salesChannelId);
  141.         if (!$hide) {
  142.             return $criteria;
  143.         }
  144.         $closeoutFilter $this->productCloseoutFilterFactory->create($context);
  145.         $criteria->addFilter($closeoutFilter);
  146.         return $criteria;
  147.     }
  148.     private function useProductStream(ProductCrossSellingEntity $crossSelling): bool
  149.     {
  150.         return $crossSelling->getType() === ProductCrossSellingDefinition::TYPE_PRODUCT_STREAM
  151.             && $crossSelling->getProductStreamId() !== null;
  152.     }
  153. }