<?php declare(strict_types=1);
namespace Shopware\Core\Content\Product\SalesChannel\CrossSelling;
use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingCollection;
use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingDefinition;
use Shopware\Core\Content\Product\Aggregate\ProductCrossSelling\ProductCrossSellingEntity;
use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition;
use Shopware\Core\Content\Product\Events\ProductCrossSellingCriteriaLoadEvent;
use Shopware\Core\Content\Product\Events\ProductCrossSellingIdsCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductCrossSellingsLoadedEvent;
use Shopware\Core\Content\Product\Events\ProductCrossSellingStreamCriteriaEvent;
use Shopware\Core\Content\Product\ProductCollection;
use Shopware\Core\Content\Product\SalesChannel\AbstractProductCloseoutFilterFactory;
use Shopware\Core\Content\Product\SalesChannel\Listing\ProductListingLoader;
use Shopware\Core\Content\Product\SalesChannel\ProductAvailableFilter;
use Shopware\Core\Content\ProductStream\Service\ProductStreamBuilderInterface;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
use Shopware\Core\Framework\Routing\Annotation\Entity;
use Shopware\Core\Framework\Routing\Annotation\Since;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* @Route(defaults={"_routeScope"={"store-api"}})
*/
#[Package('inventory')]
class ProductCrossSellingRoute extends AbstractProductCrossSellingRoute
{
private EventDispatcherInterface $eventDispatcher;
private EntityRepositoryInterface $crossSellingRepository;
private ProductStreamBuilderInterface $productStreamBuilder;
private SalesChannelRepositoryInterface $productRepository;
private SystemConfigService $systemConfigService;
private ProductListingLoader $listingLoader;
private AbstractProductCloseoutFilterFactory $productCloseoutFilterFactory;
/**
* @internal
*/
public function __construct(
EntityRepositoryInterface $crossSellingRepository,
EventDispatcherInterface $eventDispatcher,
ProductStreamBuilderInterface $productStreamBuilder,
SalesChannelRepositoryInterface $productRepository,
SystemConfigService $systemConfigService,
ProductListingLoader $listingLoader,
AbstractProductCloseoutFilterFactory $productCloseoutFilterFactory
) {
$this->eventDispatcher = $eventDispatcher;
$this->crossSellingRepository = $crossSellingRepository;
$this->productStreamBuilder = $productStreamBuilder;
$this->productRepository = $productRepository;
$this->systemConfigService = $systemConfigService;
$this->listingLoader = $listingLoader;
$this->productCloseoutFilterFactory = $productCloseoutFilterFactory;
}
public function getDecorated(): AbstractProductCrossSellingRoute
{
throw new DecorationPatternException(self::class);
}
/**
* @Since("6.3.2.0")
* @Entity("product")
* @Route("/store-api/product/{productId}/cross-selling", name="store-api.product.cross-selling", methods={"POST"})
*/
public function load(string $productId, Request $request, SalesChannelContext $context, Criteria $criteria): ProductCrossSellingRouteResponse
{
$crossSellings = $this->loadCrossSellings($productId, $context);
$elements = new CrossSellingElementCollection();
foreach ($crossSellings as $crossSelling) {
$clone = clone $criteria;
if ($this->useProductStream($crossSelling)) {
$element = $this->loadByStream($crossSelling, $context, $clone);
} else {
$element = $this->loadByIds($crossSelling, $context, $clone);
}
$elements->add($element);
}
$this->eventDispatcher->dispatch(new ProductCrossSellingsLoadedEvent($elements, $context));
return new ProductCrossSellingRouteResponse($elements);
}
private function loadCrossSellings(string $productId, SalesChannelContext $context): ProductCrossSellingCollection
{
$criteria = new Criteria();
$criteria->setTitle('product-cross-selling-route');
$criteria
->addAssociation('assignedProducts')
->addFilter(new EqualsFilter('product.id', $productId))
->addFilter(new EqualsFilter('active', 1))
->addSorting(new FieldSorting('position', FieldSorting::ASCENDING));
$this->eventDispatcher->dispatch(
new ProductCrossSellingCriteriaLoadEvent($criteria, $context)
);
/** @var ProductCrossSellingCollection $crossSellings */
$crossSellings = $this->crossSellingRepository
->search($criteria, $context->getContext())
->getEntities();
return $crossSellings;
}
private function loadByStream(ProductCrossSellingEntity $crossSelling, SalesChannelContext $context, Criteria $criteria): CrossSellingElement
{
/** @var string $productStreamId */
$productStreamId = $crossSelling->getProductStreamId();
$filters = $this->productStreamBuilder->buildFilters(
$productStreamId,
$context->getContext()
);
$criteria->addFilter(...$filters)
->setOffset(0)
->setLimit($crossSelling->getLimit())
->addSorting($crossSelling->getSorting());
$criteria = $this->handleAvailableStock($criteria, $context);
$this->eventDispatcher->dispatch(
new ProductCrossSellingStreamCriteriaEvent($crossSelling, $criteria, $context)
);
$searchResult = $this->listingLoader->load($criteria, $context);
/** @var ProductCollection $products */
$products = $searchResult->getEntities();
$element = new CrossSellingElement();
$element->setCrossSelling($crossSelling);
$element->setProducts($products);
$element->setStreamId($crossSelling->getProductStreamId());
$element->setTotal($products->count());
return $element;
}
private function loadByIds(ProductCrossSellingEntity $crossSelling, SalesChannelContext $context, Criteria $criteria): CrossSellingElement
{
$element = new CrossSellingElement();
$element->setCrossSelling($crossSelling);
$element->setProducts(new ProductCollection());
$element->setTotal(0);
if (!$crossSelling->getAssignedProducts()) {
return $element;
}
$crossSelling->getAssignedProducts()->sortByPosition();
$ids = array_values($crossSelling->getAssignedProducts()->getProductIds());
$filter = new ProductAvailableFilter(
$context->getSalesChannel()->getId(),
ProductVisibilityDefinition::VISIBILITY_LINK
);
if (!\count($ids)) {
return $element;
}
$criteria->setIds($ids);
$criteria->addFilter($filter);
$criteria->addAssociation('options.group');
$criteria = $this->handleAvailableStock($criteria, $context);
$this->eventDispatcher->dispatch(
new ProductCrossSellingIdsCriteriaEvent($crossSelling, $criteria, $context)
);
$result = $this->productRepository
->search($criteria, $context);
/** @var ProductCollection $products */
$products = $result->getEntities();
$products->sortByIdArray($ids);
$element->setProducts($products);
$element->setTotal(\count($products));
return $element;
}
private function handleAvailableStock(Criteria $criteria, SalesChannelContext $context): Criteria
{
$salesChannelId = $context->getSalesChannel()->getId();
$hide = $this->systemConfigService->get('core.listing.hideCloseoutProductsWhenOutOfStock', $salesChannelId);
if (!$hide) {
return $criteria;
}
$closeoutFilter = $this->productCloseoutFilterFactory->create($context);
$criteria->addFilter($closeoutFilter);
return $criteria;
}
private function useProductStream(ProductCrossSellingEntity $crossSelling): bool
{
return $crossSelling->getType() === ProductCrossSellingDefinition::TYPE_PRODUCT_STREAM
&& $crossSelling->getProductStreamId() !== null;
}
}