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

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