<?php declare(strict_types=1);
namespace SyseaSimilarProducts\Subscriber;
use Doctrine\DBAL\Connection;
use Shopware\Core\Content\Category\CategoryEntity;
use Shopware\Core\Content\Product\ProductCollection;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Content\Product\SalesChannel\Listing\Filter;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\TermsAggregation;
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\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Storefront\Page\Product\ProductPageCriteriaEvent;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class ProductSubscriber implements EventSubscriberInterface
{
/**
* @var SystemConfigService
*/
private $systemConfigService;
/**
* @var EntityRepository
*/
private $categoryRepository;
/**
* @var SalesChannelRepositoryInterface
*/
private $productRepository;
/**
* @var Connection
*/
private Connection $connection;
public function __construct(
EntityRepository $categoryRepository,
SalesChannelRepositoryInterface $productRepository,
SystemConfigService $systemConfigService,
Connection $connection
)
{
$this->categoryRepository = $categoryRepository;
$this->productRepository = $productRepository;
$this->systemConfigService = $systemConfigService;
$this->connection = $connection;
}
/**
* @inheritDoc
*/
public static function getSubscribedEvents()
{
return [
ProductPageCriteriaEvent::class => 'onProductPageCriteria',
ProductPageLoadedEvent::class => 'onProductPageLoaded'
];
}
public function onProductPageCriteria(ProductPageCriteriaEvent $event) {
$event->getCriteria()->addAssociation('categories');
}
public function onProductPageLoaded(ProductPageLoadedEvent $event) {
$page = $event->getPage();
$product = $page->getProduct();
$currentProductId = $product->getId();
$salesChannelContext = $event->getSalesChannelContext();
$categoryIds = $this->getProductCategories($product, $event->getContext());
if(!empty($categoryIds)) {
$similarProducts = $this->getSimilarProducts($categoryIds, $currentProductId, $salesChannelContext, $product, $salesChannelContext->getSalesChannelId());
if(!empty($similarProducts)) {
$apperanceStyle = 'block';
$apperance = $this->systemConfigService->get('SyseaSimilarProducts.config.apperanceStyle');
if($apperance) {
$apperanceStyle = 'slider';
}
$similarProductsConfig = [
'similarProducts' => $similarProducts,
'apperance' => $apperanceStyle
];
$similarProductsConfig['boxLayout'] = $this->systemConfigService->get('SyseaSimilarProducts.config.boxLayout');
$similarProductsConfig['displayMode'] = $this->systemConfigService->get('SyseaSimilarProducts.config.displayMode');
$similarProductsConfig['container'] = $this->systemConfigService->get('SyseaSimilarProducts.config.container');
$similarProductsConfig['asTitle'] = $this->systemConfigService->get('SyseaSimilarProducts.config.asTitle');
if($apperanceStyle == "slider") {
$similarProductsConfig['navigation'] = $this->systemConfigService->get('SyseaSimilarProducts.config.navigation');
$similarProductsConfig['elMinWidth'] = $this->systemConfigService->get('SyseaSimilarProducts.config.elMinWidth');
}
$page->assign([
'similarProductsConfig' => $similarProductsConfig
]);
}
}
}
/**
* @param ProductEntity $product
* @return array
*/
private function getProductCategories(ProductEntity $product, Context $context): array {
$categoryIds = [];
$categoryLevels = [];
$categories = $product->getCategories();
if(empty($categories->getIds())) {
return [];
}
/**
* @var CategoryEntity $category
*/
foreach($categories as $category) {
if(!in_array($category->getLevel(), $categoryLevels)) {
$categoryLevels[] = $category->getLevel();
}
}
$higehstLevel = max($categoryLevels);
//$tolerance = (int)$this->systemConfigService->get('SyseaSimilarProducts.config.categoryLevelTolerance');
$tolerance = 0;
if($tolerance < 0) {
$tolerance = 0;
}
foreach($categories as $category) {
if(!empty($category->getId())) {
if($category->getLevel() == $higehstLevel || $category->getLevel() >= ($higehstLevel - $tolerance)) {
if($category->getActive()) {
$categoryIds[] = $category->getId();
}
}
}
if($this->systemConfigService->get('SyseaSimilarProducts.config.includeParentCategory')) {
if(!empty($category->getParentId())) {
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('id', $category->getParentId()));
/** @var CategoryEntity $parentCategory */
$parentCategory = $this->categoryRepository->search($criteria, $context)->getEntities()->first();
if($parentCategory->getActive() && $parentCategory->getLevel() !== 1) {
$categoryIds[] = $category->getParentId();
}
}
}
}
return $categoryIds;
}
private function getSimilarProducts($categoryIds, $currentProductId, $context, $pageProduct, $salesChannelId): ProductCollection {
if($this->systemConfigService->get('SyseaSimilarProducts.config.randomSelectProducts')) {
shuffle($categoryIds);
}
$similarMax = (int)$this->systemConfigService->get('SyseaSimilarProducts.config.similarProductsLimit');
if($similarMax > 20) {
$similarMax = 20; // capped to 20 items to prevent memory limit exceed
}
$criteria = new Criteria();
$criteria->addAssociation('categories');
$criteria->addAssociations([
'categories', 'cover', 'manufacturer', 'prices'
]);
$criteria->addFilter(new NotFilter(
NotFilter::CONNECTION_AND,
[
new EqualsFilter('id', $currentProductId)
]
));
$criteria->addFilter(new EqualsAnyFilter('categoryTree', $categoryIds));
if(!$this->systemConfigService->get('SyseaSimilarProducts.config.randomSelectProducts')) {
$criteria->setLimit($similarMax);
} else {
$criteria->setLimit(20);
}
if($criteria->getLimit() > 20 || $criteria->getLimit() === null) {
$criteria->setLimit(20);
}
if($this->systemConfigService->get('SyseaSimilarProducts.config.properties')) {
$propertyFilter = $this->getPropertyFilter($pageProduct, $salesChannelId);
$filter = $propertyFilter->getFilter();
$criteria->addFilter($filter);
}
// only show active products
$criteria->addFilter(new EqualsFilter('active', true));
// hide products with stock <= 0 if active
if($this->systemConfigService->get('SyseaSimilarProducts.config.hideSoldoutProducts')) {
$criteria->addFilter(
new RangeFilter('stock', [
RangeFilter::GT => 0
])
);
}
// only show parent product if active
if($this->systemConfigService->get('SyseaSimilarProducts.config.onlyMainVariant')) {
$criteria->addFilter(new EqualsFilter('parentId', null));
}
$productSearchResult = $this->productRepository->search($criteria, $context);
$products = $productSearchResult->getEntities();
$productIds = [];
if(!$this->systemConfigService->get('SyseaSimilarProducts.config.onlyMainVariant')) {
if(!$this->systemConfigService->get('SyseaSimilarProducts.config.useStorefrontRepresentation')) {
$productIds = $products->getIds();
} else {
$propertyGroupIds = [];
/**
* @var SalesChannelProductEntity $product
*/
foreach($products as $product) {
if(!is_null($product->getVariantListingConfig())) {
$configuratorGroupConfig = $product->getVariantListingConfig();
if($configuratorGroupConfig->getDisplayParent()) {
if(!is_null($product->getParentId())) {
$productIds[] = $product->getParentId();
} else {
$productIds[] = $product->getId();
}
continue;
}
if(!$configuratorGroupConfig->getDisplayParent() && $configuratorGroupConfig->getMainVariantId()) {
$productIds[] = $configuratorGroupConfig->getMainVariantId();
continue;
}
foreach($configuratorGroupConfig->getConfiguratorGroupConfig() as $groupConfig) {
if($groupConfig['expressionForListings']) {
if(!in_array($groupConfig['id'], $propertyGroupIds)) {
$propertyGroupIds[] = $groupConfig['id'];
}
$productIds[] = $product->getId();
} else {
$productIds[] = $product->getId();
}
}
} else {
$productIds[] = $product->getId();
}
}
}
} else {
$productIds = $products->getIds();
}
$criteria->addFilter(new EqualsAnyFilter('id', $productIds));
$productSearchResult = $this->productRepository->search($criteria, $context);
if($this->systemConfigService->get('SyseaSimilarProducts.config.randomSelectProducts')) {
$i = 0;
$randomProductCollection = new ProductCollection();
/**
* @var ProductCollection $productCollection
*/
$productCollection = $productSearchResult->getEntities();
$productElements = $productCollection->getElements();
shuffle($productElements);
foreach($productElements as $productElement) {
$randomProductCollection->add($productElement);
$i++;
if($i == $similarMax) {
break;
}
}
return $randomProductCollection;
}
/**
* @var ProductCollection $similarProducts
*/
$similarProducts = $productSearchResult->getEntities();
return $similarProducts;
}
private function getPropertyFilter(SalesChannelProductEntity $product, $salesChannelId)
{
$properties = $product->getProperties();
$options = $product->getOptions();
$filterProperties = $this->systemConfigService->get('SyseaSimilarProducts.config.properties', $salesChannelId) ?: [];
$ids = [];
$filters = [];
foreach ($properties as $property) {
if (in_array($property->getGroupId(), $filterProperties, true)) {
$ids[] = $property->getId();
}
}
foreach ($options as $option) {
if (in_array($option->getGroupId(), $filterProperties, true)) {
$ids[] = $option->getId();
}
}
$grouped = $this->connection->fetchAllAssociative("
SELECT LOWER(HEX(property_group_id)) as property_group_id, LOWER(HEX(id)) as id
FROM property_group_option WHERE id IN (:ids)
",
['ids' => Uuid::fromHexToBytesList($ids)],
['ids' => Connection::PARAM_STR_ARRAY]
);
$grouped = FetchModeHelper::group($grouped);
foreach ($grouped as $options) {
$options = array_column($options, 'id');
$filters[] = new MultiFilter(
MultiFilter::CONNECTION_OR,
[
new EqualsAnyFilter('product.optionIds', $options),
new EqualsAnyFilter('product.propertyIds', $options),
]
);
}
$configOperator = $this->systemConfigService->get('SyseaSimilarProducts.config.propertyLogic', $salesChannelId) ?: 'or';
$operator = MultiFilter::CONNECTION_OR;
if (strtolower($configOperator) === 'and') {
$operator = MultiFilter::CONNECTION_AND;
}
$optionAggregation = new TermsAggregation('options', 'product.options.id');
$propertyAggregation = new TermsAggregation('properties', 'product.properties.id');
return new Filter(
'properties',
true,
[
$propertyAggregation,
$optionAggregation
],
new MultiFilter($operator, $filters),
$ids,
false
);
}
}