<?php declare(strict_types=1);namespace Shopware\Core\Content\ImportExport\Event\Subscriber;use Doctrine\DBAL\Connection;use Shopware\Core\Content\ImportExport\Event\ImportExportAfterImportRecordEvent;use Shopware\Core\Content\ImportExport\Exception\ProcessingException;use Shopware\Core\Content\Product\Aggregate\ProductConfiguratorSetting\ProductConfiguratorSettingDefinition;use Shopware\Core\Content\Product\ProductDefinition;use Shopware\Core\Framework\Api\Sync\SyncBehavior;use Shopware\Core\Framework\Api\Sync\SyncOperation;use Shopware\Core\Framework\Api\Sync\SyncServiceInterface;use Shopware\Core\Framework\Context;use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;use Shopware\Core\Framework\Feature;use Shopware\Core\Framework\Log\Package;use Shopware\Core\Framework\Uuid\Uuid;use Symfony\Component\EventDispatcher\EventSubscriberInterface;use Symfony\Contracts\Service\ResetInterface;/** * @deprecated tag:v6.5.0 - reason:becomes-internal - EventSubscribers will become internal in v6.5.0 */#[Package('system-settings')]class ProductVariantsSubscriber implements EventSubscriberInterface, ResetInterface{ private SyncServiceInterface $syncService; private Connection $connection; private EntityRepositoryInterface $groupRepository; private EntityRepositoryInterface $optionRepository; /** * @var array<string, string> */ private array $groupIdCache = []; /** * @var array<string, string> */ private array $optionIdCache = []; /** * @internal */ public function __construct( SyncServiceInterface $syncService, Connection $connection, EntityRepositoryInterface $groupRepository, EntityRepositoryInterface $optionRepository ) { $this->syncService = $syncService; $this->connection = $connection; $this->groupRepository = $groupRepository; $this->optionRepository = $optionRepository; } /** * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>> */ public static function getSubscribedEvents() { return [ ImportExportAfterImportRecordEvent::class => 'onAfterImportRecord', ]; } public function onAfterImportRecord(ImportExportAfterImportRecordEvent $event): void { $row = $event->getRow(); $entityName = $event->getConfig()->get('sourceEntity'); $entityWrittenEvents = $event->getResult()->getEvents(); if ($entityName !== ProductDefinition::ENTITY_NAME || empty($row['variants']) || !$entityWrittenEvents) { return; } $variants = $this->parseVariantString($row['variants']); $entityWrittenEvent = $entityWrittenEvents->filter(function ($event) { return $event instanceof EntityWrittenEvent && $event->getEntityName() === ProductDefinition::ENTITY_NAME; })->first(); if (!$entityWrittenEvent instanceof EntityWrittenEvent) { return; } $writeResults = $entityWrittenEvent->getWriteResults(); if (empty($writeResults)) { return; } $parentId = $writeResults[0]->getPrimaryKey(); $parentPayload = $writeResults[0]->getPayload(); if (!\is_string($parentId)) { return; } $payload = $this->getCombinationsPayload($variants, $parentId, $parentPayload['productNumber']); $variantIds = array_column($payload, 'id'); $this->connection->executeStatement( 'DELETE FROM `product_option` WHERE `product_id` IN (:ids);', ['ids' => Uuid::fromHexToBytesList($variantIds)], ['ids' => Connection::PARAM_STR_ARRAY] ); $configuratorSettingPayload = $this->getProductConfiguratorSettingPayload($payload, $parentId); $this->connection->executeStatement( 'DELETE FROM `product_configurator_setting` WHERE `product_id` = :parentId AND `id` NOT IN (:ids);', [ 'parentId' => Uuid::fromHexToBytes($parentId), 'ids' => Uuid::fromHexToBytesList(array_column($configuratorSettingPayload, 'id')), ], ['ids' => Connection::PARAM_STR_ARRAY] ); if (Feature::isActive('FEATURE_NEXT_15815')) { $behavior = new SyncBehavior(); } else { $behavior = new SyncBehavior(true, true); } $result = $this->syncService->sync([ new SyncOperation( 'write', ProductDefinition::ENTITY_NAME, SyncOperation::ACTION_UPSERT, $payload ), new SyncOperation( 'write', ProductConfiguratorSettingDefinition::ENTITY_NAME, SyncOperation::ACTION_UPSERT, $configuratorSettingPayload ), ], Context::createDefaultContext(), $behavior); if (Feature::isActive('FEATURE_NEXT_15815')) { // @internal (flag:FEATURE_NEXT_15815) - remove code below, "isSuccess" function will be removed, simply return because sync service would throw an exception in error case return; } if (!$result->isSuccess()) { $operation = $result->get('write'); throw new ProcessingException(sprintf( 'Failed writing variants for %s with errors: %s', $parentPayload['productNumber'], $operation ? json_encode(array_column($operation->getResult(), 'errors')) : '' )); } } public function reset(): void { $this->groupIdCache = []; $this->optionIdCache = []; } /** * convert "size: m, l, xl" to ["size|m", "size|l", "size|xl"] * * @return list<list<string>> */ private function parseVariantString(string $variantsString): array { $result = []; $groups = explode('|', $variantsString); foreach ($groups as $group) { $groupOptions = explode(':', $group); if (\count($groupOptions) !== 2) { $this->throwExceptionFailedParsingVariants($variantsString); } $groupName = trim($groupOptions[0]); $options = array_filter(array_map('trim', explode(',', $groupOptions[1]))); if (empty($groupName) || empty($options)) { $this->throwExceptionFailedParsingVariants($variantsString); } $options = array_map(function ($option) use ($groupName) { return sprintf('%s|%s', $groupName, $option); }, $options); $result[] = $options; } return $result; } private function throwExceptionFailedParsingVariants(string $variantsString): void { throw new ProcessingException(sprintf( 'Failed parsing variants from string "%s", valid format is: "size: L, XL, | color: Green, White"', $variantsString )); } /** * @param list<list<string>> $variants * * @return list<array<string, mixed>> */ private function getCombinationsPayload(array $variants, string $parentId, string $productNumber): array { $combinations = $this->getCombinations($variants); $payload = []; foreach ($combinations as $key => $combination) { $options = []; if (\is_string($combination)) { $combination = [$combination]; } foreach ($combination as $option) { list($group, $option) = explode('|', $option); $optionId = $this->getOptionId($group, $option); $groupId = $this->getGroupId($group); $options[] = [ 'id' => $optionId, 'name' => $option, 'group' => [ 'id' => $groupId, 'name' => $group, ], ]; } $variantId = Uuid::fromStringToHex(sprintf('%s.%s', $parentId, $key)); $variantProductNumber = sprintf('%s.%s', $productNumber, $key); $payload[] = [ 'id' => $variantId, 'parentId' => $parentId, 'productNumber' => $variantProductNumber, 'stock' => 0, 'options' => $options, ]; } return $payload; } /** * convert [["size|m", "size|l"], ["color|blue", "color|red"]] * to [["size|m", "color|blue"], ["size|l", "color|blue"], ["size|m", "color|red"], ["size|l", "color|red"]] * * @param list<list<string>> $variants * * @return list<list<string>>|list<string> */ private function getCombinations(array $variants, int $currentIndex = 0): array { if (!isset($variants[$currentIndex])) { return []; } if ($currentIndex === \count($variants) - 1) { return $variants[$currentIndex]; } // get combinations from subsequent arrays $combinations = $this->getCombinations($variants, $currentIndex + 1); $result = []; // concat each array from tmp with each element from $variants[$i] foreach ($variants[$currentIndex] as $variant) { foreach ($combinations as $combination) { $result[] = \is_array($combination) ? array_merge([$variant], $combination) : [$variant, $combination]; } } return $result; } /** * @param list<array<string, mixed>> $variantsPayload * * @return list<array<string, mixed>> */ private function getProductConfiguratorSettingPayload(array $variantsPayload, string $parentId): array { $options = array_merge(...array_column($variantsPayload, 'options')); $optionIds = array_unique(array_column($options, 'id')); $payload = []; foreach ($optionIds as $optionId) { $payload[] = [ 'id' => Uuid::fromStringToHex(sprintf('%s_configurator', $optionId)), 'optionId' => $optionId, 'productId' => $parentId, ]; } return $payload; } private function getGroupId(string $groupName): string { $groupId = Uuid::fromStringToHex($groupName); if (isset($this->groupIdCache[$groupId])) { return $this->groupIdCache[$groupId]; } $criteria = new Criteria(); $criteria->addFilter(new EqualsFilter('name', $groupName)); $group = $this->groupRepository->search($criteria, Context::createDefaultContext())->first(); if ($group !== null) { $this->groupIdCache[$groupId] = $group->getId(); return $group->getId(); } $this->groupIdCache[$groupId] = $groupId; return $groupId; } private function getOptionId(string $groupName, string $optionName): string { $optionId = Uuid::fromStringToHex(sprintf('%s.%s', $groupName, $optionName)); if (isset($this->optionIdCache[$optionId])) { return $this->optionIdCache[$optionId]; } $criteria = new Criteria(); $criteria->addFilter(new EqualsFilter('name', $optionName)); $criteria->addFilter(new EqualsFilter('group.name', $groupName)); $option = $this->optionRepository->search($criteria, Context::createDefaultContext())->first(); if ($option !== null) { $this->optionIdCache[$optionId] = $option->getId(); return $option->getId(); } $this->optionIdCache[$optionId] = $optionId; return $optionId; }}