vendor/shopware/core/Content/ImportExport/Event/Subscriber/ProductVariantsSubscriber.php line 63

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Content\ImportExport\Event\Subscriber;
  3. use Doctrine\DBAL\ArrayParameterType;
  4. use Doctrine\DBAL\Connection;
  5. use Shopware\Core\Content\ImportExport\Event\ImportExportAfterImportRecordEvent;
  6. use Shopware\Core\Content\ImportExport\Exception\ProcessingException;
  7. use Shopware\Core\Content\Product\Aggregate\ProductConfiguratorSetting\ProductConfiguratorSettingDefinition;
  8. use Shopware\Core\Content\Product\ProductDefinition;
  9. use Shopware\Core\Framework\Api\Sync\SyncBehavior;
  10. use Shopware\Core\Framework\Api\Sync\SyncOperation;
  11. use Shopware\Core\Framework\Api\Sync\SyncServiceInterface;
  12. use Shopware\Core\Framework\Context;
  13. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  17. use Shopware\Core\Framework\Log\Package;
  18. use Shopware\Core\Framework\Uuid\Uuid;
  19. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  20. use Symfony\Contracts\Service\ResetInterface;
  21. /**
  22.  * @internal
  23.  *
  24.  * @phpstan-type CombinationPayload list<array{id: string, parentId: string, productNumber: string, stock: int, options: list<array{id: string, name: string, group: array{id: string, name: string}}>}>
  25.  */
  26. #[Package('system-settings')]
  27. class ProductVariantsSubscriber implements EventSubscriberInterfaceResetInterface
  28. {
  29.     /**
  30.      * @var array<string, string>
  31.      */
  32.     private array $groupIdCache = [];
  33.     /**
  34.      * @var array<string, string>
  35.      */
  36.     private array $optionIdCache = [];
  37.     /**
  38.      * @internal
  39.      */
  40.     public function __construct(
  41.         private readonly SyncServiceInterface $syncService,
  42.         private readonly Connection $connection,
  43.         private readonly EntityRepository $groupRepository,
  44.         private readonly EntityRepository $optionRepository
  45.     ) {
  46.     }
  47.     /**
  48.      * @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
  49.      */
  50.     public static function getSubscribedEvents(): array
  51.     {
  52.         return [
  53.             ImportExportAfterImportRecordEvent::class => 'onAfterImportRecord',
  54.         ];
  55.     }
  56.     public function onAfterImportRecord(ImportExportAfterImportRecordEvent $event): void
  57.     {
  58.         $row $event->getRow();
  59.         $entityName $event->getConfig()->get('sourceEntity');
  60.         $entityWrittenEvents $event->getResult()->getEvents();
  61.         if ($entityName !== ProductDefinition::ENTITY_NAME || empty($row['variants']) || !$entityWrittenEvents) {
  62.             return;
  63.         }
  64.         $variants $this->parseVariantString($row['variants']);
  65.         $entityWrittenEvent $entityWrittenEvents->filter(fn ($event) => $event instanceof EntityWrittenEvent && $event->getEntityName() === ProductDefinition::ENTITY_NAME)->first();
  66.         if (!$entityWrittenEvent instanceof EntityWrittenEvent) {
  67.             return;
  68.         }
  69.         $writeResults $entityWrittenEvent->getWriteResults();
  70.         if (empty($writeResults)) {
  71.             return;
  72.         }
  73.         $parentId $writeResults[0]->getPrimaryKey();
  74.         $parentPayload $writeResults[0]->getPayload();
  75.         if (!\is_string($parentId)) {
  76.             return;
  77.         }
  78.         $payload $this->getCombinationsPayload($variants$parentId$parentPayload['productNumber']);
  79.         $variantIds array_column($payload'id');
  80.         $this->connection->executeStatement(
  81.             'DELETE FROM `product_option` WHERE `product_id` IN (:ids);',
  82.             ['ids' => Uuid::fromHexToBytesList($variantIds)],
  83.             ['ids' => ArrayParameterType::STRING]
  84.         );
  85.         $configuratorSettingPayload $this->getProductConfiguratorSettingPayload($payload$parentId);
  86.         $this->connection->executeStatement(
  87.             'DELETE FROM `product_configurator_setting` WHERE `product_id` = :parentId AND `id` NOT IN (:ids);',
  88.             [
  89.                 'parentId' => Uuid::fromHexToBytes($parentId),
  90.                 'ids' => Uuid::fromHexToBytesList(array_column($configuratorSettingPayload'id')),
  91.             ],
  92.             ['ids' => ArrayParameterType::STRING]
  93.         );
  94.         $this->syncService->sync([
  95.             new SyncOperation(
  96.                 'write',
  97.                 ProductDefinition::ENTITY_NAME,
  98.                 SyncOperation::ACTION_UPSERT,
  99.                 $payload
  100.             ),
  101.             new SyncOperation(
  102.                 'write',
  103.                 ProductConfiguratorSettingDefinition::ENTITY_NAME,
  104.                 SyncOperation::ACTION_UPSERT,
  105.                 $configuratorSettingPayload
  106.             ),
  107.         ], Context::createDefaultContext(), new SyncBehavior());
  108.     }
  109.     public function reset(): void
  110.     {
  111.         $this->groupIdCache = [];
  112.         $this->optionIdCache = [];
  113.     }
  114.     /**
  115.      * convert "size: m, l, xl" to ["size|m", "size|l", "size|xl"]
  116.      *
  117.      * @return list<array<string>>
  118.      */
  119.     private function parseVariantString(string $variantsString): array
  120.     {
  121.         $result = [];
  122.         $groups explode('|'$variantsString);
  123.         foreach ($groups as $group) {
  124.             $groupOptions explode(':'$group);
  125.             if (\count($groupOptions) !== 2) {
  126.                 $this->throwExceptionFailedParsingVariants($variantsString);
  127.             }
  128.             $groupName trim($groupOptions[0]);
  129.             $options array_filter(array_map('trim'explode(','$groupOptions[1])));
  130.             if (empty($groupName) || empty($options)) {
  131.                 $this->throwExceptionFailedParsingVariants($variantsString);
  132.             }
  133.             $options array_map(fn ($option) => sprintf('%s|%s'$groupName$option), $options);
  134.             $result[] = $options;
  135.         }
  136.         return $result;
  137.     }
  138.     private function throwExceptionFailedParsingVariants(string $variantsString): void
  139.     {
  140.         throw new ProcessingException(sprintf(
  141.             'Failed parsing variants from string "%s", valid format is: "size: L, XL, | color: Green, White"',
  142.             $variantsString
  143.         ));
  144.     }
  145.     /**
  146.      * @param list<array<string>> $variants
  147.      *
  148.      * @return CombinationPayload
  149.      */
  150.     private function getCombinationsPayload(array $variantsstring $parentIdstring $productNumber): array
  151.     {
  152.         $combinations $this->getCombinations($variants);
  153.         $payload = [];
  154.         foreach ($combinations as $key => $combination) {
  155.             $options = [];
  156.             if (\is_string($combination)) {
  157.                 $combination = [$combination];
  158.             }
  159.             foreach ($combination as $option) {
  160.                 [$group$option] = explode('|'$option);
  161.                 $optionId $this->getOptionId($group$option);
  162.                 $groupId $this->getGroupId($group);
  163.                 $options[] = [
  164.                     'id' => $optionId,
  165.                     'name' => $option,
  166.                     'group' => [
  167.                         'id' => $groupId,
  168.                         'name' => $group,
  169.                     ],
  170.                 ];
  171.             }
  172.             $variantId Uuid::fromStringToHex(sprintf('%s.%s'$parentId$key));
  173.             $variantProductNumber sprintf('%s.%s'$productNumber$key);
  174.             $payload[] = [
  175.                 'id' => $variantId,
  176.                 'parentId' => $parentId,
  177.                 'productNumber' => $variantProductNumber,
  178.                 'stock' => 0,
  179.                 'options' => $options,
  180.             ];
  181.         }
  182.         return $payload;
  183.     }
  184.     /**
  185.      * convert [["size|m", "size|l"], ["color|blue", "color|red"]]
  186.      * to [["size|m", "color|blue"], ["size|l", "color|blue"], ["size|m", "color|red"], ["size|l", "color|red"]]
  187.      *
  188.      * @param list<array<string>> $variants
  189.      *
  190.      * @return list<array<string>>|array<string>
  191.      */
  192.     private function getCombinations(array $variantsint $currentIndex 0): array
  193.     {
  194.         if (!isset($variants[$currentIndex])) {
  195.             return [];
  196.         }
  197.         if ($currentIndex === \count($variants) - 1) {
  198.             return $variants[$currentIndex];
  199.         }
  200.         // get combinations from subsequent arrays
  201.         $combinations $this->getCombinations($variants$currentIndex 1);
  202.         $result = [];
  203.         // concat each array from tmp with each element from $variants[$i]
  204.         foreach ($variants[$currentIndex] as $variant) {
  205.             foreach ($combinations as $combination) {
  206.                 $result[] = \is_array($combination) ? [...[$variant], ...$combination] : [$variant$combination];
  207.             }
  208.         }
  209.         return $result;
  210.     }
  211.     /**
  212.      * @param CombinationPayload $variantsPayload
  213.      *
  214.      * @return list<array{id: string, optionId: string, productId: string}>
  215.      */
  216.     private function getProductConfiguratorSettingPayload(array $variantsPayloadstring $parentId): array
  217.     {
  218.         $options array_merge(...array_column($variantsPayload'options'));
  219.         $optionIds array_unique(array_column($options'id'));
  220.         $payload = [];
  221.         foreach ($optionIds as $optionId) {
  222.             $payload[] = [
  223.                 'id' => Uuid::fromStringToHex(sprintf('%s_configurator'$optionId)),
  224.                 'optionId' => $optionId,
  225.                 'productId' => $parentId,
  226.             ];
  227.         }
  228.         return $payload;
  229.     }
  230.     private function getGroupId(string $groupName): string
  231.     {
  232.         $groupId Uuid::fromStringToHex($groupName);
  233.         if (isset($this->groupIdCache[$groupId])) {
  234.             return $this->groupIdCache[$groupId];
  235.         }
  236.         $criteria = new Criteria();
  237.         $criteria->addFilter(new EqualsFilter('name'$groupName));
  238.         $group $this->groupRepository->search($criteriaContext::createDefaultContext())->first();
  239.         if ($group !== null) {
  240.             $this->groupIdCache[$groupId] = $group->getId();
  241.             return $group->getId();
  242.         }
  243.         $this->groupIdCache[$groupId] = $groupId;
  244.         return $groupId;
  245.     }
  246.     private function getOptionId(string $groupNamestring $optionName): string
  247.     {
  248.         $optionId Uuid::fromStringToHex(sprintf('%s.%s'$groupName$optionName));
  249.         if (isset($this->optionIdCache[$optionId])) {
  250.             return $this->optionIdCache[$optionId];
  251.         }
  252.         $criteria = new Criteria();
  253.         $criteria->addFilter(new EqualsFilter('name'$optionName));
  254.         $criteria->addFilter(new EqualsFilter('group.name'$groupName));
  255.         $option $this->optionRepository->search($criteriaContext::createDefaultContext())->first();
  256.         if ($option !== null) {
  257.             $this->optionIdCache[$optionId] = $option->getId();
  258.             return $option->getId();
  259.         }
  260.         $this->optionIdCache[$optionId] = $optionId;
  261.         return $optionId;
  262.     }
  263. }