<?php declare(strict_types=1);
namespace Hemo\CustomerGroupRestriction\Subscriber;
use Hemo\CustomerGroupRestriction\Service\ProductRestrictionLoader;
use Shopware\Core\Content\Category\Event\CategoryRouteCacheKeyEvent;
use Shopware\Core\Content\LandingPage\Event\LandingPageRouteCacheKeyEvent;
use Shopware\Core\Content\Product\Events\CrossSellingRouteCacheKeyEvent;
use Shopware\Core\Content\Product\Events\ProductDetailRouteCacheKeyEvent;
use Shopware\Core\Content\Product\Events\ProductListingRouteCacheKeyEvent;
use Shopware\Core\Content\Product\Events\ProductSearchRouteCacheKeyEvent;
use Shopware\Core\Content\Product\Events\ProductSuggestRouteCacheKeyEvent;
use Shopware\Core\Framework\Adapter\Cache\StoreApiRouteCacheKeyEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\System\SalesChannel\Event\SalesChannelProcessCriteriaEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Hides restricted products from every sales channel product read (listing, search, suggest,
* detail page, CMS sliders, cross selling, Store API) for customers outside the allowed groups.
*/
final class ProductVisibilitySubscriber implements EventSubscriberInterface
{
/**
* Criteria title used by Shopware's ProductGateway when loading cart products. The cart
* processor handles that case itself and reports a meaningful error to the customer.
*/
private const CART_CRITERIA_TITLE = 'cart::products';
private ProductRestrictionLoader $loader;
public function __construct(ProductRestrictionLoader $loader)
{
$this->loader = $loader;
}
public static function getSubscribedEvents(): array
{
return [
'sales_channel.product.process.criteria' => 'onProcessCriteria',
ProductListingRouteCacheKeyEvent::class => 'onCacheKey',
ProductSearchRouteCacheKeyEvent::class => 'onCacheKey',
ProductSuggestRouteCacheKeyEvent::class => 'onCacheKey',
ProductDetailRouteCacheKeyEvent::class => 'onCacheKey',
CrossSellingRouteCacheKeyEvent::class => 'onCacheKey',
CategoryRouteCacheKeyEvent::class => 'onCacheKey',
LandingPageRouteCacheKeyEvent::class => 'onCacheKey',
];
}
public function onProcessCriteria(SalesChannelProcessCriteriaEvent $event): void
{
$criteria = $event->getCriteria();
if ($criteria->getTitle() === self::CART_CRITERIA_TITLE) {
return;
}
$customerGroupId = $event->getSalesChannelContext()->getCurrentCustomerGroup()->getId();
$hiddenIds = $this->loader->getHiddenProductIds($customerGroupId);
if ($hiddenIds === []) {
return;
}
// A variant without a restriction of its own inherits the parent's restriction.
$parentVisibility = [
new EqualsFilter('product.parentId', null),
new NotFilter(NotFilter::CONNECTION_AND, [new EqualsAnyFilter('product.parentId', $hiddenIds)]),
];
$ownRestriction = $this->loader->getRestrictedProductIds();
if ($ownRestriction !== []) {
$parentVisibility[] = new EqualsAnyFilter('product.id', $ownRestriction);
}
$criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_AND, [
new NotFilter(NotFilter::CONNECTION_AND, [new EqualsAnyFilter('product.id', $hiddenIds)]),
new MultiFilter(MultiFilter::CONNECTION_OR, $parentVisibility),
]));
}
/**
* The cached Store API routes build their cache key before the criteria is processed, so the
* customer group has to become part of the key explicitly.
*/
public function onCacheKey(StoreApiRouteCacheKeyEvent $event): void
{
$event->addPart($event->getContext()->getCurrentCustomerGroup()->getId());
}
}