vendor/shopware/core/Content/Product/SalesChannel/CrossSelling/ProductCrossSellingRoute.php line 164

Open in your IDE?
  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\EntityRepositoryInterface;
  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\Framework\Routing\Annotation\Entity;
  23. use Shopware\Core\Framework\Routing\Annotation\Since;
  24. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  25. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  26. use Shopware\Core\System\SystemConfig\SystemConfigService;
  27. use Symfony\Component\HttpFoundation\Request;
  28. use Symfony\Component\Routing\Annotation\Route;
  29. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  30. /**
  31.  * @Route(defaults={"_routeScope"={"store-api"}})
  32.  */
  33. #[Package('inventory')]
  34. class ProductCrossSellingRoute extends AbstractProductCrossSellingRoute
  35. {
  36.     private EventDispatcherInterface $eventDispatcher;
  37.     private EntityRepositoryInterface $crossSellingRepository;
  38.     private ProductStreamBuilderInterface $productStreamBuilder;
  39.     private SalesChannelRepositoryInterface $productRepository;
  40.     private SystemConfigService $systemConfigService;
  41.     private ProductListingLoader $listingLoader;
  42.     private AbstractProductCloseoutFilterFactory $productCloseoutFilterFactory;
  43.     /**
  44.      * @internal
  45.      */
  46.     public function __construct(
  47.         EntityRepositoryInterface $crossSellingRepository,
  48.         EventDispatcherInterface $eventDispatcher,
  49.         ProductStreamBuilderInterface $productStreamBuilder,
  50.         SalesChannelRepositoryInterface $productRepository,
  51.         SystemConfigService $systemConfigService,
  52.         ProductListingLoader $listingLoader,
  53.         AbstractProductCloseoutFilterFactory $productCloseoutFilterFactory
  54.     ) {
  55.         $this->eventDispatcher $eventDispatcher;
  56.         $this->crossSellingRepository $crossSellingRepository;
  57.         $this->productStreamBuilder $productStreamBuilder;
  58.         $this->productRepository $productRepository;
  59.         $this->systemConfigService $systemConfigService;
  60.         $this->listingLoader $listingLoader;
  61.         $this->productCloseoutFilterFactory $productCloseoutFilterFactory;
  62.     }
  63.     public function getDecorated(): AbstractProductCrossSellingRoute
  64.     {
  65.         throw new DecorationPatternException(self::class);
  66.     }
  67.     /**
  68.      * @Since("6.3.2.0")
  69.      * @Entity("product")
  70.      * @Route("/store-api/product/{productId}/cross-selling", name="store-api.product.cross-selling", methods={"POST"})
  71.      */
  72.     public function load(string $productIdRequest $requestSalesChannelContext $contextCriteria $criteria): ProductCrossSellingRouteResponse
  73.     {
  74.         $crossSellings $this->loadCrossSellings($productId$context);
  75.         $elements = new CrossSellingElementCollection();
  76.         foreach ($crossSellings as $crossSelling) {
  77.             $clone = clone $criteria;
  78.             if ($this->useProductStream($crossSelling)) {
  79.                 $element $this->loadByStream($crossSelling$context$clone);
  80.             } else {
  81.                 $element $this->loadByIds($crossSelling$context$clone);
  82.             }
  83.             $elements->add($element);
  84.         }
  85.         $this->eventDispatcher->dispatch(new ProductCrossSellingsLoadedEvent($elements$context));
  86.         return new ProductCrossSellingRouteResponse($elements);
  87.     }
  88.     private function loadCrossSellings(string $productIdSalesChannelContext $context): ProductCrossSellingCollection
  89.     {
  90.         $criteria = new Criteria();
  91.         $criteria->setTitle('product-cross-selling-route');
  92.         $criteria
  93.             ->addAssociation('assignedProducts')
  94.             ->addFilter(new EqualsFilter('product.id'$productId))
  95.             ->addFilter(new EqualsFilter('active'1))
  96.             ->addSorting(new FieldSorting('position'FieldSorting::ASCENDING));
  97.         $this->eventDispatcher->dispatch(
  98.             new ProductCrossSellingCriteriaLoadEvent($criteria$context)
  99.         );
  100.         /** @var ProductCrossSellingCollection $crossSellings */
  101.         $crossSellings $this->crossSellingRepository
  102.             ->search($criteria$context->getContext())
  103.             ->getEntities();
  104.         return $crossSellings;
  105.     }
  106.     private function loadByStream(ProductCrossSellingEntity $crossSellingSalesChannelContext $contextCriteria $criteria): CrossSellingElement
  107.     {
  108.         /** @var string $productStreamId */
  109.         $productStreamId $crossSelling->getProductStreamId();
  110.         $filters $this->productStreamBuilder->buildFilters(
  111.             $productStreamId,
  112.             $context->getContext()
  113.         );
  114.         $criteria->addFilter(...$filters)
  115.             ->setOffset(0)
  116.             ->setLimit($crossSelling->getLimit())
  117.             ->addSorting($crossSelling->getSorting());
  118.         $criteria $this->handleAvailableStock($criteria$context);
  119.         $this->eventDispatcher->dispatch(
  120.             new ProductCrossSellingStreamCriteriaEvent($crossSelling$criteria$context)
  121.         );
  122.         $searchResult $this->listingLoader->load($criteria$context);
  123.         /** @var ProductCollection $products */
  124.         $products $searchResult->getEntities();
  125.         $element = new CrossSellingElement();
  126.         $element->setCrossSelling($crossSelling);
  127.         $element->setProducts($products);
  128.         $element->setStreamId($crossSelling->getProductStreamId());
  129.         $element->setTotal($products->count());
  130.         return $element;
  131.     }
  132.     private function loadByIds(ProductCrossSellingEntity $crossSellingSalesChannelContext $contextCriteria $criteria): CrossSellingElement
  133.     {
  134.         $element = new CrossSellingElement();
  135.         $element->setCrossSelling($crossSelling);
  136.         $element->setProducts(new ProductCollection());
  137.         $element->setTotal(0);
  138.         if (!$crossSelling->getAssignedProducts()) {
  139.             return $element;
  140.         }
  141.         $crossSelling->getAssignedProducts()->sortByPosition();
  142.         $ids array_values($crossSelling->getAssignedProducts()->getProductIds());
  143.         $filter = new ProductAvailableFilter(
  144.             $context->getSalesChannel()->getId(),
  145.             ProductVisibilityDefinition::VISIBILITY_LINK
  146.         );
  147.         if (!\count($ids)) {
  148.             return $element;
  149.         }
  150.         $criteria->setIds($ids);
  151.         $criteria->addFilter($filter);
  152.         $criteria->addAssociation('options.group');
  153.         $criteria $this->handleAvailableStock($criteria$context);
  154.         $this->eventDispatcher->dispatch(
  155.             new ProductCrossSellingIdsCriteriaEvent($crossSelling$criteria$context)
  156.         );
  157.         $result $this->productRepository
  158.             ->search($criteria$context);
  159.         /** @var ProductCollection $products */
  160.         $products $result->getEntities();
  161.         $products->sortByIdArray($ids);
  162.         $element->setProducts($products);
  163.         $element->setTotal(\count($products));
  164.         return $element;
  165.     }
  166.     private function handleAvailableStock(Criteria $criteriaSalesChannelContext $context): Criteria
  167.     {
  168.         $salesChannelId $context->getSalesChannel()->getId();
  169.         $hide $this->systemConfigService->get('core.listing.hideCloseoutProductsWhenOutOfStock'$salesChannelId);
  170.         if (!$hide) {
  171.             return $criteria;
  172.         }
  173.         $closeoutFilter $this->productCloseoutFilterFactory->create($context);
  174.         $criteria->addFilter($closeoutFilter);
  175.         return $criteria;
  176.     }
  177.     private function useProductStream(ProductCrossSellingEntity $crossSelling): bool
  178.     {
  179.         return $crossSelling->getType() === ProductCrossSellingDefinition::TYPE_PRODUCT_STREAM
  180.             && $crossSelling->getProductStreamId() !== null;
  181.     }
  182. }