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

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 $productId, Request $request, SalesChannelContext $context, Criteria $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 $productId, SalesChannelContext $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 $crossSelling, SalesChannelContext $context, Criteria $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 $crossSelling, SalesChannelContext $context, Criteria $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 $criteria, SalesChannelContext $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. }