<?php declare(strict_types=1);
namespace ThemeOkeOnline\Storefront\Product\Subscriber;
use Psr\Log\LoggerInterface;
use Shopware\Core\Framework\Context;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Content\Product\ProductCollection;
use Shopware\Core\Framework\Adapter\Cache\CacheCompressor;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntitySearchResultLoadedEvent;
class StorefrontProductSearchResultLoadedSubscriber implements EventSubscriberInterface
{
/**
* @var EntityRepository
*/
private $productRepository;
/**
* @var SystemConfigService
*/
private $systemConfigService;
/**
* @var TagAwareAdapterInterface
*/
private $cache;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var Context
*/
private $context;
/**
* @var string
*/
public $commonPropertyGroupId;
public function __construct(
EntityRepository $productRepository,
SystemConfigService $systemConfigService,
TagAwareAdapterInterface $cache,
LoggerInterface $logger
)
{
$this->productRepository = $productRepository;
$this->systemConfigService = $systemConfigService;
$this->cache = $cache;
$this->logger = $logger;
$this->commonPropertyGroupId = $this->systemConfigService->get('ThemeOkeOnline.config.parentProperty') ?? null;
}
public static function getSubscribedEvents(): array
{
return [
ProductListingCriteriaEvent::class => ['addPropertiesToProductListingCriteria'],
'sales_channel.product.search.result.loaded' => ['productListingLoaded']
];
}
public function addPropertiesToProductListingCriteria(ProductListingCriteriaEvent $event): void
{
if($this->commonPropertyGroupId)
{
$criteria = $event->getCriteria();
$criteria->addAssociation('properties');
}
}
public function productListingLoaded(EntitySearchResultLoadedEvent $event): void
{
// set context for later use
$this->context = $event->getContext();
// if there is no setting set, we're done
if( ! $this->commonPropertyGroupId)
return;
// collect a list with all used property-connections
$loadedProducts = $event->getResult()->getElements();
// loop trough all products
foreach($loadedProducts as $product)
{
// get the propertyGroupOptionIds based on the setting which defines the common-PropertyGroup
$commonPropertyGroupOptionIds = $this->getCommonPropertyGroupOptionIds($product, $this->commonPropertyGroupId);
// get the siblings, out of the cache or database
$siblings = $this->getSiblingsOfProduct($product, $commonPropertyGroupOptionIds, true);
// remove the product itself from that list
$siblings = $this->without($siblings, $product->getId());
// add an extension
$product->addExtension('siblings', $siblings);
}
}
private function getCommonPropertyGroupOptionIds(ProductEntity $product, $commonPropertyGroupId): array
{
// get the properties from the product, this can be null or an empty array, if so, we can return an empty array
$commonProductPropertyGroupOptions = $product->getProperties();
if( ! $commonProductPropertyGroupOptions)
return [];
$commonProductPropertyGroupOptionValues = $commonProductPropertyGroupOptions->filterByGroupId($commonPropertyGroupId);
if($commonProductPropertyGroupOptionValues->count() == 0)
return [];
// eventually, return the ids as an array
return $commonProductPropertyGroupOptionValues->getIds();
}
private function getSiblingsOfProduct(ProductEntity $product, array $commonPropertyGroupOptionIds): ProductCollection
{
// check if there are some in cache, if so, return ProductCollection from cache
// If not, get from database...
$cacheKey = sprintf('product-siblings-%s', $product->getId());
// try to find the key in the cache
$item = $this->cache->getItem($cacheKey);
try {
// if the tag is a hit, and has content
if ($item->isHit() && $item->get()) {
$this->logger->info('cache-hit: ' . $cacheKey);
// uncompress and return its value
return CacheCompressor::uncompress($item);
}
} catch (\Throwable $e) {
$this->logger->error($e->getMessage());
}
$this->logger->info('cache-miss: ' . $cacheKey);
// No result found in Cache, so;
// do what you should normally do without cache, and $item->save() it, so we have it the next time out of the cache.
// if $commonPropertyGroupOptionIds had no elements, we should not query, but we should save a empty cache item.
// set cache expire time to
$item->expiresAfter(86400);
if( ! count($commonPropertyGroupOptionIds) > 0)
{
$result = new ProductCollection();
$item = CacheCompressor::compress($item, $result);
$this->cache->save($item);
// and were done.
return $result;
}
// We have commonPropertyGroupOptionIds, so we should query them, to find any siblings
$criteria = new Criteria();
$criteria->setTitle('load-siblings');
$criteria->resetAssociations();
$criteria->addAssociation('cover');
// create a multi-filter, because the product we're checking could have more than one common-propertyGroupOptions (it is a multi-select in back-office)
$multiFilter = [];
foreach($commonPropertyGroupOptionIds as $commonPropertyGroupOptionId)
{
$multiFilter[] = new EqualsFilter('properties.id', $commonPropertyGroupOptionId);
}
$criteria->addFilter(
new MultiFilter(
MultiFilter::CONNECTION_OR,
$multiFilter
)
);
// query it!
$result = $this->productRepository->search($criteria, $this->getContext());
// if there are products, save them in the cache, and return them.
if($result->count() > 0)
{
$item = CacheCompressor::compress($item, $result->getEntities());
$this->cache->save($item);
return $result->getEntities();
}
// fallback, empty product Collection
return new ProductCollection();
}
private function without(ProductCollection $collection, $id): ProductCollection
{
$collection->remove($id);
return $collection;
}
private function getContext(): ?Context
{
return $this->context;
}
}