<?php
namespace SanettaTheme\Service;
use Doctrine\DBAL\Connection;
use Monolog\Logger;
use SanettaTheme\Struct\ProductHover;
use Shopware\Core\Content\Product\ProductCollection;
use Shopware\Core\Content\Product\SalesChannel\Detail\AvailableCombinationResult;
use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionCollection;
use Shopware\Core\Content\Property\Aggregate\PropertyGroupOption\PropertyGroupOptionEntity;
use Shopware\Core\Content\Property\PropertyGroupCollection;
use Shopware\Core\Content\Property\PropertyGroupDefinition;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\Doctrine\FetchModeHelper;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use function array_key_exists;
class VariantLoader
{
protected EntityRepositoryInterface $configuratorRepository;
protected Connection $connection;
protected Logger $logger;
public function __construct(
EntityRepositoryInterface $configuratorRepository,
Connection $connection,
Logger $logger
) {
$this->configuratorRepository = $configuratorRepository;
$this->connection = $connection;
$this->logger = $logger;
}
public function load(ProductCollection $products, SalesChannelContext $context): void
{
$productSettings = $this->loadSettings($products, $context);
if (empty($productSettings)) {
return;
}
$productIds = array_filter($products->map(function (SalesChannelProductEntity $product) {
return $product->getParentId() ?? $product->getId();
}));
$allCombinations = $this->loadCombinations($productIds, $context->getContext());
/** @var \Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity $product */
foreach ($products as $product) {
if ($product->getConfiguratorSettings() !== null || !$product->getParentId() || empty($productSettings[$product->getParentId()])) {
$product->addExtension('groups', new PropertyGroupCollection());
continue;
}
$groups = $this->sortSettings($productSettings[$product->getParentId()], $product);
$combinations = $allCombinations[$product->getParentId()];
$current = $this->buildCurrentOptions($product, $groups);
foreach ($groups as $group) {
$options = $group->getOptions();
if ($options === null) {
continue;
}
foreach ($options as $option) {
$combinable = $this->isCombinable($option, $current, $combinations);
if ($combinable === null) {
$options->remove($option->getId());
continue;
}
$option->setGroup(null);
$option->setCombinable($combinable);
}
$group->setOptions($options);
}
$product->addExtension('groups', $groups);
}
}
protected function loadSettings(
ProductCollection $products,
SalesChannelContext $context
): ?array {
$allSettings = [];
$criteria = (new Criteria())->addFilter(
new EqualsAnyFilter(
'productId',
$products->map(function (SalesChannelProductEntity $product) {
return $product->getParentId() ?? $product->getId();
})
)
);
$criteria->addAssociation('option.group')
->addAssociation('option.media')
->addAssociation('media');
$settings = $this->configuratorRepository
->search($criteria, $context->getContext())
->getEntities();
if ($settings->count() <= 0) {
return null;
}
/** @var \Shopware\Core\Content\Product\Aggregate\ProductConfiguratorSetting\ProductConfiguratorSettingEntity $setting */
foreach ($settings as $setting) {
$productId = $setting->getProductId();
if (array_key_exists($productId, $allSettings)) {
$allSettings[$productId][] = clone $setting;
} else {
$allSettings[$productId] = [clone $setting];
}
}
/** @var \Shopware\Core\Content\Product\Aggregate\ProductConfiguratorSetting\ProductConfiguratorSettingEntity[] $settings */
foreach ($allSettings as $productId => $settings) {
$groups = [];
foreach ($settings as $setting) {
$option = $setting->getOption();
if ($option === null) {
continue;
}
$group = $option->getGroup();
if ($group === null) {
continue;
}
$groupId = $group->getId();
if (isset($groups[$groupId])) {
$group = $groups[$groupId];
}
$groups[$groupId] = $group;
if ($group->getOptions() === null) {
$group->setOptions(new PropertyGroupOptionCollection());
}
$group->getOptions()->add($option);
$option->setConfiguratorSetting($setting);
}
$allSettings[$productId] = $groups;
}
return $allSettings;
}
protected function loadCombinations(
array $productIds,
Context $context
): array {
$allCombinations = [];
$query = $this->connection->createQueryBuilder();
$query->from('product')
->select([
'LOWER(HEX(product.id))',
'LOWER(HEX(product.parent_id)) as parent_id',
'product.option_ids as options',
'product.product_number as productNumber',
'product.available',
])
->leftJoin('product', 'product', 'parent', 'product.parent_id = parent.id')
->andWhere('product.parent_id IN (:id)')
->andWhere('product.version_id = :versionId')
->andWhere('IFNULL(product.active, parent.active) = :active')
->andWhere('product.option_ids IS NOT NULL')
->setParameter('id', Uuid::fromHexToBytesList($productIds), Connection::PARAM_STR_ARRAY)
->setParameter('versionId', Uuid::fromHexToBytes($context->getVersionId()))
->setParameter('active', true);
$combinations = $query->execute()->fetchAll();
$combinations = FetchModeHelper::groupUnique($combinations);
foreach ($combinations as $combination) {
$parentId = $combination['parent_id'];
if (array_key_exists($parentId, $allCombinations)) {
$allCombinations[$parentId][] = $combination;
} else {
$allCombinations[$parentId] = [$combination];
}
}
foreach ($allCombinations as $parentId => $groupedCombinations) {
$available = [];
foreach ($groupedCombinations as $combination) {
$combination['options'] = json_decode($combination['options'], true);
$available[] = $combination;
}
$result = new AvailableCombinationResult();
foreach ($available as $combination) {
$result->addCombination($combination['options']);
}
$allCombinations[$parentId] = $result;
}
return $allCombinations;
}
protected function sortSettings(
?array $groups,
SalesChannelProductEntity $product
): PropertyGroupCollection {
if (!$groups) {
return new PropertyGroupCollection();
}
$sorted = [];
foreach ($groups as $group) {
if (!$group) {
continue;
}
if (!$group->getOptions()) {
$group->setOptions(new PropertyGroupOptionCollection());
}
$sorted[$group->getId()] = $group;
}
/** @var \Shopware\Core\Content\Property\PropertyGroupEntity $group */
foreach ($sorted as $group) {
$group->getOptions()->sort(
static function (PropertyGroupOptionEntity $a, PropertyGroupOptionEntity $b) use ($group) {
if ($a->getConfiguratorSetting()->getPosition() !== $b->getConfiguratorSetting()->getPosition()) {
return $a->getConfiguratorSetting()->getPosition() <=> $b->getConfiguratorSetting()->getPosition();
}
if ($group->getSortingType() === PropertyGroupDefinition::SORTING_TYPE_ALPHANUMERIC) {
return strnatcmp($a->getTranslation('name'), $b->getTranslation('name'));
}
return ($a->getTranslation('position') ?? $a->getPosition() ?? 0) <=> ($b->getTranslation('position') ?? $b->getPosition() ?? 0);
}
);
}
$collection = new PropertyGroupCollection($sorted);
$config = $product->getConfiguratorGroupConfig();
if (!$config) {
$collection->sortByPositions();
return $collection;
}
$sortedGroupIds = array_column($config, 'id');
$sortedGroupIds = array_unique(array_merge($sortedGroupIds, $collection->getIds()));
$collection->sortByIdArray($sortedGroupIds);
return $collection;
}
protected function buildCurrentOptions(
SalesChannelProductEntity $product,
PropertyGroupCollection $groups
): array {
$keyMap = $groups->getOptionIdMap();
$current = [];
foreach ($product->getOptionIds() as $optionId) {
$groupId = $keyMap[$optionId] ?? null;
if ($groupId === null) {
continue;
}
$current[$groupId] = $optionId;
}
return $current;
}
protected function isCombinable(
PropertyGroupOptionEntity $option,
array $current,
AvailableCombinationResult $combinations
): ?bool {
unset($current[$option->getGroupId()]);
$current[] = $option->getId();
if ($combinations->hasCombination($current)) {
return true;
}
if ($combinations->hasOptionId($option->getId())) {
return false;
}
return null;
}
}