<?php
declare(strict_types=1);
namespace Rhiem\RhiemUserProducts\Subscriber;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\Content\Product\ProductEntity;
use Rhiem\RhiemUserProducts\Services\ProductGroupService;
use Shopware\Storefront\Page\Product\ProductPageLoadedEvent;
use Shopware\Storefront\Page\Product\ProductPageCriteriaEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Shopware\Core\Content\Product\Events\ProductSearchCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductSuggestCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductCrossSellingsLoadedEvent;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntitySearchedEvent;
use Shopware\Core\Content\Product\Events\ProductCrossSellingIdsCriteriaEvent;
use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Storefront\Page\Product\QuickView\MinimalQuickViewPageCriteriaEvent;
use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionCollection;
class ProductCriteriaEvents implements EventSubscriberInterface
{
const GROUP_MODE_WHITE = 0;
const GROUP_MODE_BLACK = 1;
/**
* @var SalesChannelRepositoryInterface
*/
private SalesChannelRepositoryInterface $productRepository;
/**
* @var ProductGroupService
*/
private ProductGroupService $productGroupService;
/**
* @var Connection
*/
private $connection;
public function __construct(
SalesChannelRepositoryInterface $productRepository,
ProductGroupService $productGroupService,
Connection $connection
) {
$this->productRepository = $productRepository;
$this->productGroupService = $productGroupService;
$this->connection = $connection;
}
public static function getSubscribedEvents()
{
return [
ProductListingCriteriaEvent::class => 'productGroupCriteria',
ProductPageCriteriaEvent::class => 'productGroupCriteria',
MinimalQuickViewPageCriteriaEvent::class => 'productGroupCriteria',
ProductSearchCriteriaEvent::class => 'productGroupCriteria',
ProductSuggestCriteriaEvent::class => 'productGroupCriteria',
ProductCrossSellingIdsCriteriaEvent::class => 'productGroupCriteria',
ProductCrossSellingsLoadedEvent::class => 'onCrossSellingLoadedEvent',
ProductPageLoadedEvent::class => 'onProductPageLoadedEvent'
];
}
/**
* @param ProductListingCriteriaEvent $event
*/
public function productGroupCriteria($event): void
{
$context = $event->getContext();
if(method_exists($event,'getRequest')) {
$navigationId = $event->getRequest()->get('navigationId');
$productGroup = $this->productGroupService->getProductGroup($event->getSalesChannelContext(), $navigationId);
} else {
$productGroup = $this->productGroupService->getProductGroup($event->getSalesChannelContext());
}
$productIds = $this->productGroupService->getProductIds($productGroup);
$criteria = $event->getCriteria();
$this->filterCriteria($criteria, $productIds, $productGroup, $context);
}
public function onCrossSellingLoadedEvent(ProductCrossSellingsLoadedEvent $event)
{
$crossSellings = $event->getCrossSellings();
$context = $event->getContext();
$productGroup = $this->productGroupService->getProductGroup($event->getSalesChannelContext());
$productIds = $this->productGroupService->getProductIds($productGroup);
foreach ($crossSellings as $crossSelling) {
$products = $crossSelling->getProducts();
$filteredProducts = $products->filter(function (ProductEntity $product) use ($productIds, $productGroup, $context) {
return $this->filterByProductId($product->getId(), $productIds, $productGroup, $context);
});
$crossSelling->setProducts($filteredProducts);
}
}
public function onProductPageLoadedEvent(ProductPageLoadedEvent $event)
{
$configuratorSettings = $event->getPage()->getConfiguratorSettings();
$context = $event->getContext();
$productGroup = $this->productGroupService->getProductGroup($event->getSalesChannelContext());
$productIds = $this->productGroupService->getProductIds($productGroup);
if ($productGroup->getGroupMode() === 'whitelist plus') {
$fallBackGroup = $this->productGroupService->getFallbackGroup($context);
$fallBackProductIds = $this->productGroupService->getProductIds($fallBackGroup);
if ($fallBackGroup->getGroupMode() === 'whitelist') {
$productIds = array_unique(array_merge($fallBackProductIds, $productIds));
} else {
$productIds = array_diff($fallBackProductIds, $productIds);
}
}
$groupMode = self::GROUP_MODE_BLACK;
if ($productGroup->getGroupMode() === 'whitelist' || ($productGroup->getGroupMode() === 'whitelist plus' && $fallBackGroup->getGroupMode() === 'whitelist')) {
$groupMode = self::GROUP_MODE_WHITE;
}
$mainProduct = $event->getPage()->getProduct();
$mainOptions = $this->getSortedOptions($mainProduct);
if ($mainProduct->getParentId()) {
$parentId = $mainProduct->getParentId();
} else {
$parentId = $mainProduct->getId();
}
list($combinations, $optionIds) = $this->getActiveOptions($parentId, $productIds, $groupMode);
foreach ($configuratorSettings as $configuratorSetting) {
$allowedOptions = new PropertyGroupOptionCollection();
foreach ($configuratorSetting->getOptions()->getElements() as $option) {
if (in_array($option->getId(), $optionIds)) {
if (in_array($option->getId(), $mainProduct->getOptionIds())) {
$allowedOptions->add($option);
continue;
}
$option->setCombinable(false);
$compareCombination = $mainOptions;
$compareCombination[$option->getGroupId()] = $option->getId();
$compareCombination = array_values($compareCombination);
foreach ($combinations as $combination) {
$combinable = true;
foreach ($compareCombination as $optionValue) {
if (!in_array($optionValue, $combination)) {
$combinable = false;
break;
}
}
if ($combinable) {
$option->setCombinable(true);
break;
}
}
$allowedOptions->add($option);
}
}
$configuratorSetting->setOptions($allowedOptions);
}
}
private function filterCriteria($criteria, $productIds, $productGroup, $context): void
{
if ($productGroup->getGroupMode() === 'whitelist') {
$criteria->addFilter(new EqualsAnyFilter('id', $productIds));
} elseif ($productGroup->getGroupMode() === 'whitelist plus') {
$fallBackGroup = $this->productGroupService->getFallbackGroup($context);
$fallBackProductIds = $this->productGroupService->getProductIds($fallBackGroup);
if ($fallBackGroup->getGroupMode() === 'whitelist') {
$mergedProductIds = array_unique(array_merge($fallBackProductIds, $productIds));
$criteria->addFilter(new EqualsAnyFilter('id', $mergedProductIds));
} else {
$diffedProductIds = array_diff($fallBackProductIds, $productIds);
if (count($diffedProductIds) > 0) {
$criteria->addFilter(
new NotFilter(
NotFilter::CONNECTION_OR,
[new EqualsAnyFilter('id', $diffedProductIds)]
)
);
}
}
} else {
if (count($productIds) > 0) {
$criteria->addFilter(
new NotFilter(
NotFilter::CONNECTION_OR,
[new EqualsAnyFilter('id', $productIds)]
)
);
}
}
}
private function filterByProductId($productId, $productIds, $productGroup, $context): bool
{
if ($productGroup->getGroupMode() === 'whitelist') {
if (in_array($productId, $productIds)) {
return true;
}
return false;
} else if ($productGroup->getGroupMode() === 'whitelist plus') {
$fallBackGroup = $this->productGroupService->getFallbackGroup($context);
$fallBackProductIds = $this->productGroupService->getProductIds($fallBackGroup);
if ($fallBackGroup->getGroupMode() === 'whitelist') {
$mergedProductIds = array_unique(array_merge($fallBackProductIds, $productIds));
if (in_array($productId, $mergedProductIds)) {
return true;
}
return false;
} else {
$diffedProductIds = array_diff($fallBackProductIds, $productIds);
if (in_array($productId, $diffedProductIds)) {
return false;
}
return true;
}
} else {
if (in_array($productId, $productIds)) {
return false;
}
return true;
}
}
private function getActiveOptions($parentId, $productIds, $groupMode)
{
$inString = '';
if ($groupMode == self::GROUP_MODE_BLACK) {
$inString = 'NOT';
if (count($productIds) == 0) {
$productIds = [UUID::randomHex()];
}
}
$query = <<<SQL
SELECT option_ids
FROM product
WHERE parent_id=?
AND id $inString IN(?);
SQL;
$result = $this->connection->fetchFirstColumn(
$query,
[Uuid::fromHexToBytes($parentId), UUID::fromHexToBytesList($productIds)],
[\PDO::PARAM_STR, Connection::PARAM_STR_ARRAY]
);
$combinations = array_map(function (String $options) {
return json_decode($options);
}, $result);
$optionIds = array_unique(array_merge(...$combinations));
return [$combinations, $optionIds];
}
private function getSortedOptions(SalesChannelProductEntity $product)
{
$result = [];
foreach ($product->getOptions() as $option) {
$result[$option->getGroup()->getId()] = $option->getId();
}
return $result;
}
}