<?php declare(strict_types=1);
/**
* @copyright 2022 Crehler Sp. z o. o.
* @link https://crehler.com/
* @support support@crehler.com
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Crehler\Tools\Product;
use Crehler\Tools\Enum\LoaderFlags;
use Crehler\Tools\Product\Exception\SalesChannelContextException;
use Shopware\Core\Content\Category\Service\CategoryBreadcrumbBuilder;
use Shopware\Core\Content\Cms\DataResolver\ResolverContext\EntityResolverContext;
use Shopware\Core\Content\Cms\SalesChannel\SalesChannelCmsPageLoaderInterface;
use Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition;
use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Content\Product\SalesChannel\AbstractProductCloseoutFilterFactory;
use Shopware\Core\Content\Product\SalesChannel\ProductAvailableFilter;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductCollection;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Cache\EntityCacheKeyGenerator;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\PlatformRequest;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
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\HttpFoundation\RequestStack;
class ProductLoader implements ProductLoaderInterface {
private array $cache = [];
public function __construct(private readonly SalesChannelRepository|SalesChannelRepositoryInterface $productRepository,
private readonly CategoryBreadcrumbBuilder $breadcrumbBuilder,
private readonly SalesChannelCmsPageLoaderInterface $cmsPageLoader,
private readonly RequestStack $requestStack,
private readonly SystemConfigService $config,
private readonly ProductDefinition $productDefinition,
private readonly EntityCacheKeyGenerator $generator,
private readonly AbstractProductCloseoutFilterFactory $productCloseoutFilterFactory
) {}
public function load(string|array $ids, SalesChannelContext|null $context, ...$flags): SalesChannelProductCollection
{
if (is_string($ids)) {
$ids = [$ids];
}
if (empty($ids)) {
return new SalesChannelProductCollection();
}
if ($context === null) {
$context = $this->requestStack->getMasterRequest()->get(PlatformRequest::ATTRIBUTE_SALES_CHANNEL_CONTEXT_OBJECT, null);
if ($context === null) {
throw new SalesChannelContextException();
}
}
$stringFlags = array_map(fn ($flag) => $flag->name, $flags);
$cacheKey = md5(json_encode([$this->generator->getSalesChannelContextHash($context), $stringFlags]));
if (!isset($this->cache[$cacheKey])) {
$this->cache[$cacheKey] = [];
}
$requestedIds = $ids;
$ids = array_filter($ids, fn($id) => !isset($this->cache[$cacheKey][$id]));
if (!empty($ids)) {
$criteria = new Criteria($ids);
$criteria->setTitle('crehler-product-loader');
$this->addFilters($context, $criteria, in_array(LoaderFlags::IgnoreHideCloseoutProductsWhenOutOfStock, $flags));
$products = $this->productRepository
->search($criteria, $context);
$collection = $products->getEntities();
} else {
$collection = new SalesChannelProductCollection();
}
/** @var SalesChannelProductEntity $product */
foreach ($collection as $product) {
if (!in_array(LoaderFlags::SkipSeoCategory, $flags) && $product->getSeoCategory()){
$product->setSeoCategory($this->breadcrumbBuilder->build($product->getSeoCategory()));
}
if (in_array(LoaderFlags::SkipPageLoading, $flags)) continue;
$pageId = $product->getCmsPageId();
if ($pageId) {
// clone product to prevent recursion encoding (see NEXT-17603)
$resolverContext = new EntityResolverContext($context, $this->requestStack->getMainRequest(), $this->productDefinition, clone $product);
$pages = $this->cmsPageLoader->load(
$this->requestStack->getMainRequest(),
$this->createCriteria($pageId, $this->requestStack->getMainRequest()),
$context,
$product->getTranslation('slotConfig'),
$resolverContext
);
if ($page = $pages->first()) {
$product->setCmsPage($page);
}
}
}
foreach ($requestedIds as $id) {
if (isset($this->cache[$cacheKey][$id])) {
$collection->add($this->cache[$cacheKey][$id]);
}
}
foreach ($collection as $product) {
$this->cache[$cacheKey][$product->getId()] = $product;
}
return $collection;
}
private function addFilters(SalesChannelContext $context, Criteria $criteria, bool $ignoreHideCloseoutProductsWhenOutOfStock): void
{
$criteria->addFilter(
new ProductAvailableFilter($context->getSalesChannel()->getId(), ProductVisibilityDefinition::VISIBILITY_LINK)
);
if ($ignoreHideCloseoutProductsWhenOutOfStock) return;
$salesChannelId = $context->getSalesChannel()->getId();
$hideCloseoutProductsWhenOutOfStock = $this->config->get('core.listing.hideCloseoutProductsWhenOutOfStock', $salesChannelId);
if ($hideCloseoutProductsWhenOutOfStock) {
$filter = $this->productCloseoutFilterFactory->create($context);
$filter->addQuery(new EqualsFilter('product.parentId', null));
$criteria->addFilter($filter);
}
}
private function createCriteria(string $pageId, Request $request): Criteria
{
$criteria = new Criteria([$pageId]);
$criteria->setTitle('product::cms-page');
$slots = $request->get('slots');
if (\is_string($slots)) {
$slots = explode('|', $slots);
}
if (!empty($slots) && \is_array($slots)) {
$criteria
->getAssociation('sections.blocks')
->addFilter(new EqualsAnyFilter('slots.id', $slots));
}
return $criteria;
}
}