<?php
namespace ApplifactionGuidedShopping\Controller;
use ApplifactionGuidedShopping\Entity\AccessoryRule\AccessoryRuleEntity;
use ApplifactionGuidedShopping\Entity\DynamicFilterRule\DynamicFilterRuleEntity;
use ApplifactionGuidedShopping\Entity\PropertyGroupOptionValue\PropertyGroupOptionValueCollection;
use ApplifactionGuidedShopping\Entity\PropertyGroupOptionValue\PropertyGroupOptionValueEntity;
use ApplifactionGuidedShopping\Service\CacheService;
use ApplifactionGuidedShopping\Service\NormalizationService;
use ApplifactionGuidedShopping\Event\AccessoryProductsFetchedEvent;
use Doctrine\DBAL\Connection;
use Psr\Log\LoggerInterface;
use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
use Shopware\Core\Content\Product\ProductCollection;
use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Content\ProductStream\ProductStreamEntity;
use Shopware\Core\Content\ProductStream\Service\ProductStreamBuilder;
use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionEntity;
use Shopware\Core\Content\Seo\SeoUrlPlaceholderHandler;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\CountAggregation;
use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\AggregationResultCollection;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\AndFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\OrFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\Profiling\Profiler;
use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Storefront\Controller\StorefrontController;
use Shopware\Storefront\Framework\Routing\RequestTransformer;
use Shopware\Storefront\Page\Product\QuickView\MinimalQuickViewPageLoader;
use Shopware\Storefront\Page\Product\QuickView\ProductQuickViewWidgetLoadedHook;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Twig\Environment;
#[Route(defaults: ['_routeScope' => ['storefront']])]
class AccessoryAssistantController extends StorefrontController
{
/**
* @var ProductStreamBuilder
*/
private $productStreamBuilder;
/**
* @var SalesChannelRepository
*/
private $productRepository;
/**
* @var EntityRepository
*/
private $accessoryRuleRepository;
/**
* @var Environment
*/
private $twig;
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var CacheService
*/
private $cacheService;
/**
* @var SeoUrlPlaceholderHandler
*/
protected $seoUrlPlaceholderHandler;
/**
* @var MinimalQuickViewPageLoader
*/
private $minimalQuickViewPageLoader;
/**
* @var NormalizationService
*/
private $normalizationService;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var Connection
*/
private $connection;
public function __construct(
ProductStreamBuilder $productStreamBuilder,
SalesChannelRepository $productRepository,
EntityRepository $accessoryRuleRepository,
Environment $twig,
EventDispatcherInterface $eventDispatcher,
CacheService $cacheService,
SeoUrlPlaceholderHandler $seoUrlPlaceholderHandler,
MinimalQuickViewPageLoader $minimalQuickViewPageLoader,
NormalizationService $normalizationService,
LoggerInterface $logger,
Connection $connection
)
{
$this->productStreamBuilder = $productStreamBuilder;
$this->productRepository = $productRepository;
$this->accessoryRuleRepository = $accessoryRuleRepository;
$this->twig = $twig;
$this->eventDispatcher = $eventDispatcher;
$this->cacheService = $cacheService;
$this->seoUrlPlaceholderHandler = $seoUrlPlaceholderHandler;
$this->minimalQuickViewPageLoader = $minimalQuickViewPageLoader;
$this->normalizationService = $normalizationService;
$this->logger = $logger;
$this->connection = $connection;
}
/**
* @Route("/accessory-assistant/matching-accessory-rule-ids/{productId}", name="frontend.applifaction.accessory-assistant.matching-accessory-rule-ids", options={"seo"="false"}, methods={"POST"}, defaults={"XmlHttpRequest": true})
*/
public function accessoryRuleMatch(string $productId, SalesChannelContext $salesChannelContext): Response
{
return Profiler::trace('frontend.applifaction.accessory-assistant.matching-accessory-rule-ids', function () use ($productId, $salesChannelContext) {
return $this->cacheService->getCacheValueOrRecalculate(
'frontend.applifaction.accessory-assistant.matching-accessory-rule-ids',
['productId' => $productId, 'productUpdatedAt' => $this->cacheService->fetchUpdatedAtByProductIds([$productId])],
\DateInterval::createFromDateString('24 hours'),
function () use ($productId, $salesChannelContext) {
return new JsonResponse(['matchingRuleIds' => $this->fetchMatchingAccessoryRuleIds($productId, $salesChannelContext)]);
},
$salesChannelContext);
});
}
/**
* @Route("/accessory-assistant/modal-content", name="frontend.applifaction.accessory-assistant.modal-content", options={"seo"="false"}, methods={"POST"}, defaults={"XmlHttpRequest": true})
*/
public function modalContent(Request $request, SalesChannelContext $salesChannelContext): Response
{
return Profiler::trace('frontend.applifaction.accessory-assistant.modal-content', function () use ($request, $salesChannelContext) {
$productId = $request->get('productId');
$matchingRuleIds = $request->get('matchingRuleIds');
return $this->cacheService->getCacheValueOrRecalculate(
'frontend.applifaction.accessory-assistant.modal-content',
[
'productId' => $productId,
'matchingRuleIds' => $matchingRuleIds,
'productUpdatedAt' => $this->cacheService->fetchUpdatedAtByProductIds([$productId]),
'ruleUpdatedAt' => $this->cacheService->fetchUpdatedAtByRuleIds($matchingRuleIds)
],
\DateInterval::createFromDateString('24 hours'),
function () use ($productId, $matchingRuleIds, $request, $salesChannelContext) {
return new Response($this->fetchAccessoryGroupedByMatchingRule($productId, $matchingRuleIds, $request, $salesChannelContext));
},
$salesChannelContext);
});
}
/**
* @Route("/accessory-assistant/quickview/{productId}", name="frontend.applifaction.accessory-assistant.quickview", methods={"GET"}, defaults={"XmlHttpRequest": true})
*/
public function accessoryAssistantQuickView(Request $request, SalesChannelContext $context): Response
{
$page = $this->minimalQuickViewPageLoader->load($request, $context);
$this->hook(new ProductQuickViewWidgetLoadedHook($page, $context));
return $this->renderStorefront('@ApplifactionGuidedShopping/storefront/modal/modal-accessory-assistant-quickview-inner.html.twig', ['page' => $page]);
}
private function fetchAccessoryGroupedByMatchingRule(
string $productId,
array $matchingRuleIds,
Request $request,
SalesChannelContext $salesChannelContext): string
{
$productsGroupedByMatchingRule = [];
$productCriteria = new Criteria();
$productCriteria->setTitle('applifaction::accessory-rule::products');
$productCriteria->addSorting(new FieldSorting('sales', FieldSorting::DESCENDING));
$accessoryRuleCriteria = new Criteria();
$accessoryRuleCriteria->setTitle('applifaction::accessory-rule::results');
$accessoryRuleCriteria->setLimit(200);
$accessoryRuleCriteria->addFilter(new EqualsAnyFilter('id', $matchingRuleIds));
$accessoryRuleCriteria->addAssociation('accessoryStreams');
$accessoryRuleCriteria->addAssociation('accessoryIncludedOptions');
$accessoryRuleCriteria->addAssociation('accessoryExcludedOptions');
$accessoryRuleCriteria->addAssociation('dynamicFilterRules.mainProductCustomField');
$accessoryRuleCriteria->addAssociation('dynamicFilterRules.accessoryGroup');
$accessoryRuleCriteria->addSorting(new FieldSorting('position', FieldSorting::ASCENDING));
// Check for cache hit or aggregate result
/** @var EntitySearchResult $accessoryRules */
$accessoryRules = $this->cacheService->getCacheValueOrRecalculate(
$accessoryRuleCriteria->getTitle(),
[
'productId' => $productId,
'productUpdatedAt' => $this->cacheService->fetchUpdatedAtByProductIds([$productId])
],
\DateInterval::createFromDateString('24 hours'),
function () use ($accessoryRuleCriteria, $salesChannelContext) {
return $this->accessoryRuleRepository->search($accessoryRuleCriteria, $salesChannelContext->getContext());
},
$salesChannelContext,
$accessoryRuleCriteria);
/** @var AccessoryRuleEntity $accessoryRule */
foreach ($accessoryRules as $accessoryRule) {
if ($accessoryRule->getAccessoryStreams()->count() === 0
&& $accessoryRule->getAccessoryIncludedOptions()->count() === 0
&& $accessoryRule->getDynamicFilterRules()->count() === 0) {
continue;
}
$streamFilters = [];
$optionIncludedFilters = [];
$optionExcludedFilters = [];
$ruleProductCriteria = clone $productCriteria;
$ruleProductCriteria->setTitle('applifaction::accessory-rule::products::rule-id-' . $accessoryRule->getId());
$accessoryRuleId = $accessoryRule->getId();
// Add product stream apiFilters to the criteria
/** @var ProductStreamEntity $accessoryStream */
foreach ($accessoryRule->getAccessoryStreams() as $accessoryStream) {
$filters = $this->productStreamBuilder->buildFilters(
$accessoryStream->getId(),
$salesChannelContext->getContext()
);
$streamFilters[] = $filters[0];
}
if (!empty($streamFilters)) {
$ruleProductCriteria->addFilter(new OrFilter($streamFilters));
}
// Add included property group options to the criteria
/** @var PropertyGroupOptionEntity $accessoryIncludedOptions */
foreach ($accessoryRule->getAccessoryIncludedOptions() as $accessoryIncludedOptions) {
$optionIncludedFilters[] = $accessoryIncludedOptions->getId();
}
if (!empty($optionIncludedFilters)) {
$ruleProductCriteria->addFilter(new EqualsAnyFilter('propertyIds', $optionIncludedFilters));
}
// Add excluded property group options to the criteria
/** @var PropertyGroupOptionEntity $accessoryExcludedOptions */
foreach ($accessoryRule->getAccessoryExcludedOptions() as $accessoryExcludedOptions) {
$optionExcludedFilters[] = $accessoryExcludedOptions->getId();
}
if (!empty($optionExcludedFilters)) {
$ruleProductCriteria->addFilter(
new NotFilter(
NotFilter::CONNECTION_AND,
[new EqualsAnyFilter('propertyIds', $optionExcludedFilters)]
)
);
}
$this->applyDynamicAccessoryFilters($productId, $ruleProductCriteria, $accessoryRule, $salesChannelContext);
$this->eventDispatcher->dispatch(
new ProductListingCriteriaEvent($request, $ruleProductCriteria, $salesChannelContext)
);
$ruleProductCriteria->setLimit(24);
/** @var EntitySearchResult $searchResult */
$searchResult = $this->cacheService->getCacheValueOrRecalculate(
$ruleProductCriteria->getTitle(),
[
'productId' => $productId,
'accessoryRuleId' => $accessoryRuleId,
'productUpdatedAt' => $this->cacheService->fetchUpdatedAtByProductIds([$productId]),
'ruleUpdatedAt' => $this->cacheService->fetchUpdatedAtByRuleIds([$accessoryRuleId])
],
\DateInterval::createFromDateString('24 hours'),
function () use ($ruleProductCriteria, $accessoryRule, $salesChannelContext, $productId) {
return Profiler::trace('applifaction.accessory-rule-search-products::rule-' . $accessoryRule->getId(), function () use ($ruleProductCriteria, $salesChannelContext, $productId) {
$products = $this->productRepository->search($ruleProductCriteria, $salesChannelContext);
$event = new AccessoryProductsFetchedEvent($products);
$this->eventDispatcher->dispatch($event);
// If the accessory is only compatible with a few main products, remove it in case the main product is not one of these
$tempProducts = clone $products;
/** @var ProductEntity $product */
foreach ($tempProducts as $product) {
$customFields = $product->getCustomFields();
if (!isset($customFields['ags_compatible_products']) || !is_string($customFields['ags_compatible_products'])) continue;
$productNumbers = preg_split('/, */', $customFields['ags_compatible_products']);
if (empty($productNumbers)) continue;
$productIds = $this->fetchProductIdsByProductNumbers($productNumbers);
if (in_array($productId, $productIds)) continue;
$products->remove($product->getId());
}
// Remove sold out products - they shouldn't be displayed
$tempProducts = clone $products;
$productCount = 0;
$maxProductCount = 12;
/** @var ProductEntity $product */
foreach ($tempProducts as $product) {
$isPurchasable = $product->getAvailable() &&
$product->getChildCount() <= 0 &&
$product->getCalculatedMaxPurchase() > 0 &&
$productCount <= $maxProductCount;
if (!$isPurchasable) {
$products->remove($product->getId());
} else {
$productCount++;
}
}
return $products;
});
},
$salesChannelContext,
$accessoryRuleCriteria);
$productsGroupedByMatchingRule[$accessoryRule->getId()] = [
'rule' => $accessoryRule,
'products' => $searchResult
];
}
$html = $this->twig->render('@ApplifactionGuidedShopping/storefront/modal/modal-accessory-assistant-inner.html.twig', [
'productsGroupedByMatchingRule' => $productsGroupedByMatchingRule,
]);
$host = $request->attributes->get(RequestTransformer::SALES_CHANNEL_ABSOLUTE_BASE_URL)
. $request->attributes->get(RequestTransformer::SALES_CHANNEL_BASE_URL);
$html = $this->seoUrlPlaceholderHandler->replace($html, $host, $salesChannelContext);
return $html;
}
private function fetchMatchingAccessoryRuleIds(
string $productId,
SalesChannelContext $salesChannelContext): array
{
$matchingRuleIds = [];
$productCriteria = new Criteria();
$productCriteria->setTitle('applifaction::accessory-rule::product-matches');
$productCriteria->setLimit(1);
$productCriteria->addFilter(new EqualsFilter('id', $productId));
$productCriteria->addAggregation(new CountAggregation('count', 'id'));
$accessoryRuleCriteria = new Criteria();
$accessoryRuleCriteria->setTitle('applifaction::accessory-rule::all-with-main-product-properties');
$accessoryRuleCriteria->setLimit(200);
$accessoryRuleCriteria->addAssociation('mainProductStreams');
$accessoryRuleCriteria->addAssociation('mainProductIncludedOptions');
$accessoryRuleCriteria->addAssociation('mainProductExcludedOptions');
$accessoryRuleCriteria->addAssociation('dynamicFilterRules.mainProductCustomField');
$accessoryRuleCriteria->addSorting(new FieldSorting('position', FieldSorting::ASCENDING));
// Check for cache hit or aggregate result
/** @var EntitySearchResult $accessoryRules */
$accessoryRules = $this->cacheService->getCacheValueOrRecalculate(
$accessoryRuleCriteria->getTitle(),
[
'productId' => $productId,
'productUpdatedAt' => $this->cacheService->fetchUpdatedAtByProductIds([$productId])
],
\DateInterval::createFromDateString('24 hours'),
function () use ($accessoryRuleCriteria, $salesChannelContext) {
return $this->accessoryRuleRepository->search($accessoryRuleCriteria, $salesChannelContext->getContext());
},
$salesChannelContext,
$accessoryRuleCriteria);
/** @var AccessoryRuleEntity $accessoryRule */
foreach ($accessoryRules as $accessoryRule) {
if ($accessoryRule->getMainProductStreams()->count() === 0
&& $accessoryRule->getMainProductIncludedOptions()->count() === 0) {
continue;
}
// In case the accessoryRule has dynamic filter rules, the main product must contain at least one of its customFields or properties
if ($accessoryRule->getDynamicFilterRules()->count() > 0) {
$mainProductGroupIds = $accessoryRule->getDynamicFilterRules()->map(function (DynamicFilterRuleEntity $dynamicFilterRule) {
if ($dynamicFilterRule->getMainProductValueSource() === DynamicFilterRuleEntity::VALUE_SOURCE_PROPERTY && !!$dynamicFilterRule->getMainProductGroupId()) {
return $dynamicFilterRule->getMainProductGroupId();
} else {
return null;
}
});
$mainProductGroupIds = array_values(array_filter($mainProductGroupIds, fn($mainProductGroupId) => $mainProductGroupId !== null));
$mainProductCustomFields = $accessoryRule->getDynamicFilterRules()->map(function (DynamicFilterRuleEntity $dynamicFilterRule) {
if ($dynamicFilterRule->getMainProductValueSource() === DynamicFilterRuleEntity::VALUE_SOURCE_CUSTOM_FIELD && !!$dynamicFilterRule->getMainProductCustomField() && !!$dynamicFilterRule->getMainProductCustomFieldUnit()) {
return $dynamicFilterRule->getMainProductCustomField()->getName();
} else {
return null;
}
});
$mainProductCustomFields = array_values(array_filter($mainProductCustomFields, fn($mainProductGroupId) => $mainProductGroupId !== null));
if (!empty($mainProductGroupIds) || !empty($mainProductCustomFields)) {
$criteria = new Criteria();
$criteria->setTitle('applifaction::accessory-rule::dynamic-filter-product-property-check');
$criteria->addAssociation('properties');
$criteria->addFilter(new EqualsFilter('id', $productId));
$products = $this->productRepository->search($criteria, $salesChannelContext);
if ($products->count() === 0) continue;
$hasAtLeastOneCustomField = false;
$hasAtLeastOneProperty = false;
/** @var ProductEntity $product */
foreach ($products as $product) {
foreach ($product->getProperties() as $property) {
if (in_array($property->getGroupId(), $mainProductGroupIds)) {
$hasAtLeastOneProperty = true;
break;
}
}
if ($hasAtLeastOneProperty) break;
foreach ($mainProductCustomFields as $mainProductCustomField) {
if (in_array($mainProductCustomField, array_keys($product->getCustomFields()))) {
$hasAtLeastOneCustomField = true;
break;
}
}
if ($hasAtLeastOneCustomField) break;
}
if (!$hasAtLeastOneCustomField && !$hasAtLeastOneProperty) continue;
}
}
$streamFilters = [];
$optionFilters = [];
$ruleProductCriteria = clone $productCriteria;
$accessoryRuleId = $accessoryRule->getId();
// Add product stream apiFilters to the criteria
/** @var ProductStreamEntity $mainProductStream */
foreach ($accessoryRule->getMainProductStreams() as $mainProductStream) {
$queries = $this->productStreamBuilder->buildFilters(
$mainProductStream->getId(),
$salesChannelContext->getContext()
);
$streamFilters[] = $queries[0];
}
if (!empty($streamFilters)) {
$ruleProductCriteria->addFilter(new OrFilter($streamFilters));
}
// Add propertyOptions to the criteria
/** @var PropertyGroupOptionEntity $mainProductIncludedOptions */
foreach ($accessoryRule->getMainProductIncludedOptions() as $mainProductIncludedOptions) {
$optionFilters[] = $mainProductIncludedOptions->getId();
}
if (!empty($optionFilters)) {
$ruleProductCriteria->addFilter(new EqualsAnyFilter('propertyIds', $optionFilters));
}
// Check for cache hit or aggregate result
/** @var AggregationResultCollection $aggregationResult */
$aggregationResult = $this->cacheService->getCacheValueOrRecalculate(
$ruleProductCriteria->getTitle(),
[
'productId' => $productId,
'accessoryRuleId' => $accessoryRuleId,
'productUpdatedAt' => $this->cacheService->fetchUpdatedAtByProductIds([$productId]),
'ruleUpdatedAt' => $this->cacheService->fetchUpdatedAtByRuleIds([$accessoryRuleId])
],
\DateInterval::createFromDateString('24 hours'),
function () use ($ruleProductCriteria, $salesChannelContext) {
return $this->productRepository->aggregate($ruleProductCriteria, $salesChannelContext);
},
$salesChannelContext,
$ruleProductCriteria);
if ($aggregationResult->first()->getCount() > 0) {
$matchingRuleIds[] = $accessoryRule->getId();
}
}
return $matchingRuleIds;
}
private function applyDynamicAccessoryFilters(string $productId, Criteria $ruleProductCriteria, AccessoryRuleEntity $accessoryRule, SalesChannelContext $salesChannelContext)
{
$dynamicFilterRules = $accessoryRule->getDynamicFilterRules();
$allQueries = [];
// Keep only rules, which have both IDs set
$dynamicFilterRules = $dynamicFilterRules->filter(
function (DynamicFilterRuleEntity $rule) {
if ($rule->getMainProductValueSource() === DynamicFilterRuleEntity::VALUE_SOURCE_PROPERTY) {
return !!$rule->getMainProductGroupId() && !!$rule->getAccessoryGroupId();
} else {
return !!$rule->getMainProductCustomFieldId() && !!$rule->getMainProductCustomFieldUnit() && !!$rule->getAccessoryGroupId();
}
}
);
$mainProductGroupIds = $dynamicFilterRules->map(fn(DynamicFilterRuleEntity $dynamicFilterRule) => $dynamicFilterRule->getMainProductGroupId());
$mainProductGroupIds = array_values(array_filter($mainProductGroupIds, fn($mainProductGroupId) => $mainProductGroupId !== null));
if (empty($mainProductGroupIds)) return;
// Fetch main product and cache it
$mainProductCriteria = new Criteria();
$mainProductCriteria->addFilter(new EqualsFilter('id', $productId));
$mainProductCriteria->addAssociation('properties.group');
$mainProductCriteria->addAssociation('properties.agsPropertyGroupOptionValues');
$propertiesAssociation = $mainProductCriteria->getAssociation('properties');
$propertiesAssociation->addFilter(new EqualsAnyFilter('groupId', $mainProductGroupIds));
/** @var ProductCollection $mainProducts */
$mainProducts = $this->productRepository->search($mainProductCriteria, $salesChannelContext);
/** @var DynamicFilterRuleEntity $dynamicFilterRule */
foreach ($dynamicFilterRules as $dynamicFilterRule) {
$mainProductGroupId = $dynamicFilterRule->getMainProductGroupId();
$accessoryGroupId = $dynamicFilterRule->getAccessoryGroupId();
$accessoryGroupUnit = isset($dynamicFilterRule->getAccessoryGroup()->getCustomFields()['ags_property_group_unit']) ? strtolower($dynamicFilterRule->getAccessoryGroup()->getCustomFields()['ags_property_group_unit']) : null;
$mainProductCustomField = $dynamicFilterRule->getMainProductCustomField();
$mainProductValueSource = $dynamicFilterRule->getMainProductValueSource();
$queryPropertyValues = new PropertyGroupOptionValueCollection();
if (!!$mainProductCustomField && $mainProductValueSource === DynamicFilterRuleEntity::VALUE_SOURCE_CUSTOM_FIELD) {
/** @var ProductEntity $mainProduct */
foreach ($mainProducts as $mainProduct) {
$customFields = $mainProduct->getCustomFields();
if (isset($customFields[$mainProductCustomField->getName()])) {
$customFieldValue = $customFields[$mainProductCustomField->getName()];
if (!!$customFieldValue) {
$collection = new PropertyGroupOptionValueCollection();
$mainProductCustomFieldUnit = $dynamicFilterRule->getMainProductCustomFieldUnit() ?? 'kg';
try {
$collection = $this->normalizationService->parseString($customFieldValue, $mainProductCustomFieldUnit);
$collection->map(fn(PropertyGroupOptionValueEntity $value) => $value->convertUnit($accessoryGroupUnit));
$queryPropertyValues->merge($collection);
} catch (\Exception $e) {
$this->logger->error(join("\t", [$e->getMessage(), 'Product: ' . $mainProduct->getProductNumber(), 'Custom Field ' . $mainProductCustomField->getName() . ': ' . $customFieldValue]));
}
}
}
}
} else if (!!$mainProductGroupId) {
/** @var ProductEntity $mainProduct */
foreach ($mainProducts as $mainProduct) {
/** @var PropertyGroupOptionEntity $mainProductPropertyOption */
foreach ($mainProduct->getProperties() as $mainProductPropertyOption) {
if ($mainProductGroupId === $mainProductPropertyOption->getGroupId()) {
/** @var PropertyGroupOptionValueCollection|null $mainProductNormalizedPropertyValues */
$mainProductNormalizedPropertyValues = $mainProductPropertyOption->getExtension('agsPropertyGroupOptionValues');
if (!!$mainProductNormalizedPropertyValues && $mainProductNormalizedPropertyValues->count() > 0) {
try {
$mainProductNormalizedPropertyValues->map(fn(PropertyGroupOptionValueEntity $value) => $value->convertUnit($accessoryGroupUnit));
$queryPropertyValues->merge($mainProductNormalizedPropertyValues);
} catch (\Exception $e) {
$this->logger->error(join("\t", [$e->getMessage(), 'Product: ' . $mainProduct->getProductNumber()]));
}
}
}
}
}
}
$queries = [];
/** @var PropertyGroupOptionValueEntity $propertyValue */
foreach ($queryPropertyValues as $queryPropertyValue) {
// E.g. Accessory: Belastbarkeit -> Main Product: Weight
// E.g. Accessory: VESA Norm (300x300-800x600) -> Main Product: VESA Norm (600x600)
$queries[] = new EqualsFilter('properties.agsPropertyGroupOptionValues.propertyGroupOption.groupId', $accessoryGroupId);
if (!is_null($queryPropertyValue->getMinD1()) && !is_null($queryPropertyValue->getMaxD1())) {
$queries[] = new RangeFilter('properties.agsPropertyGroupOptionValues.minD1', ['lte' => $queryPropertyValue->getMaxD1()]);
$queries[] = new RangeFilter('properties.agsPropertyGroupOptionValues.maxD1', ['gte' => $queryPropertyValue->getMinD1()]);
}
if (!is_null($queryPropertyValue->getMinD2()) && !is_null($queryPropertyValue->getMaxD2())) {
$queries[] = new RangeFilter('properties.agsPropertyGroupOptionValues.minD2', ['lte' => $queryPropertyValue->getMaxD2()]);
$queries[] = new RangeFilter('properties.agsPropertyGroupOptionValues.maxD2', ['gte' => $queryPropertyValue->getMinD2()]);
}
if (!is_null($queryPropertyValue->getMinD3()) && !is_null($queryPropertyValue->getMaxD3())) {
$queries[] = new RangeFilter('properties.agsPropertyGroupOptionValues.minD3', ['lte' => $queryPropertyValue->getMaxD3()]);
$queries[] = new RangeFilter('properties.agsPropertyGroupOptionValues.maxD3', ['gte' => $queryPropertyValue->getMinD3()]);
}
}
if (!empty($queries)) {
$allQueries[] = new AndFilter($queries);
}
}
$ruleProductCriteria->addFilter(new AndFilter($allQueries));
}
/**
* @param array $productNumbers
* @return array
*/
public function fetchProductIdsByProductNumbers(array $productNumbers): array
{
$productIdSql = <<<SQL
SELECT LOWER(HEX(id)) as 'id'
FROM product
WHERE product_number IN (:productNumbers)
SQL;
try {
$productIdResults = $this->connection->fetchAllAssociative(
$productIdSql,
['productNumbers' => $productNumbers],
['productNumbers' => Connection::PARAM_STR_ARRAY]
);
} catch (\Exception $e) {
return [];
}
return array_map(fn($productIdResult) => $productIdResult['id'], $productIdResults);
}
}