platform/src/Core/Content/ImportExport/Event/Subscriber/ProductVariantsSubscriber.php line 55

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\Uuid\Uuid;
  18. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  19. class ProductVariantsSubscriber implements EventSubscriberInterface
  20. {
  21.     private SyncServiceInterface $syncService;
  22.     private Connection $connection;
  23.     private EntityRepositoryInterface $groupRepository;
  24.     private EntityRepositoryInterface $optionRepository;
  25.     private array $groupIdCache = [];
  26.     private array $optionIdCache = [];
  27.     public function __construct(
  28.         SyncServiceInterface $syncService,
  29.         Connection $connection,
  30.         EntityRepositoryInterface $groupRepository,
  31.         EntityRepositoryInterface $optionRepository
  32.     ) {
  33.         $this->syncService $syncService;
  34.         $this->connection $connection;
  35.         $this->groupRepository $groupRepository;
  36.         $this->optionRepository $optionRepository;
  37.     }
  38.     public static function getSubscribedEvents()
  39.     {
  40.         return [
  41.             ImportExportAfterImportRecordEvent::class => 'onAfterImportRecord',
  42.         ];
  43.     }
  44.     public function onAfterImportRecord(ImportExportAfterImportRecordEvent $event): void
  45.     {
  46.         $row $event->getRow();
  47.         $entityName $event->getConfig()->get('sourceEntity');
  48.         $entityWrittenEvents $event->getResult()->getEvents();
  49.         if ($entityName !== ProductDefinition::ENTITY_NAME || empty($row['variants']) || !$entityWrittenEvents) {
  50.             return;
  51.         }
  52.         $variants $this->parseVariantString($row['variants']);
  53.         $entityWrittenEvent $entityWrittenEvents->filter(function ($event) {
  54.             return $event instanceof EntityWrittenEvent && $event->getEntityName() === ProductDefinition::ENTITY_NAME;
  55.         })->first();
  56.         if (!$entityWrittenEvent instanceof EntityWrittenEvent) {
  57.             return;
  58.         }
  59.         $writeResults $entityWrittenEvent->getWriteResults();
  60.         if (empty($writeResults)) {
  61.             return;
  62.         }
  63.         $parentId $writeResults[0]->getPrimaryKey();
  64.         $parentPayload $writeResults[0]->getPayload();
  65.         if (!\is_string($parentId)) {
  66.             return;
  67.         }
  68.         $payload $this->getCombinationsPayload($variants$parentId$parentPayload['productNumber']);
  69.         $variantIds array_column($payload'id');
  70.         $this->connection->executeStatement(
  71.             'DELETE FROM `product_option` WHERE `product_id` IN (:ids);',
  72.             ['ids' => Uuid::fromHexToBytesList($variantIds)],
  73.             ['ids' => Connection::PARAM_STR_ARRAY]
  74.         );
  75.         $configuratorSettingPayload $this->getProductConfiguratorSettingPayload($payload$parentId);
  76.         $this->connection->executeStatement(
  77.             'DELETE FROM `product_configurator_setting` WHERE `product_id` = :parentId AND `id` NOT IN (:ids);',
  78.             [
  79.                 'parentId' => Uuid::fromHexToBytes($parentId),
  80.                 'ids' => Uuid::fromHexToBytesList(array_column($configuratorSettingPayload'id')),
  81.             ],
  82.             ['ids' => Connection::PARAM_STR_ARRAY]
  83.         );
  84.         if (Feature::isActive('FEATURE_NEXT_15815')) {
  85.             $behavior = new SyncBehavior();
  86.         } else {
  87.             $behavior = new SyncBehavior(truetrue);
  88.         }
  89.         $result $this->syncService->sync([
  90.             new SyncOperation(
  91.                 'write',
  92.                 ProductDefinition::ENTITY_NAME,
  93.                 SyncOperation::ACTION_UPSERT,
  94.                 $payload
  95.             ),
  96.             new SyncOperation(
  97.                 'write',
  98.                 ProductConfiguratorSettingDefinition::ENTITY_NAME,
  99.                 SyncOperation::ACTION_UPSERT,
  100.                 $configuratorSettingPayload
  101.             ),
  102.         ], Context::createDefaultContext(), $behavior);
  103.         if (Feature::isActive('FEATURE_NEXT_15815')) {
  104.             // @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
  105.             return;
  106.         }
  107.         if (!$result->isSuccess()) {
  108.             $operation $result->get('write');
  109.             throw new ProcessingException(sprintf(
  110.                 'Failed writing variants for %s with errors: %s',
  111.                 $parentPayload['productNumber'],
  112.                 $operation json_encode(array_column($operation->getResult(), 'errors')) : ''
  113.             ));
  114.         }
  115.     }
  116.     /**
  117.      * convert "size: m, l, xl" to ["size|m", "size|l", "size|xl"]
  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(function ($option) use ($groupName) {
  134.                 return sprintf('%s|%s'$groupName$option);
  135.             }, $options);
  136.             $result[] = $options;
  137.         }
  138.         return $result;
  139.     }
  140.     private function throwExceptionFailedParsingVariants(string $variantsString): void
  141.     {
  142.         throw new ProcessingException(sprintf(
  143.             'Failed parsing variants from string "%s", valid format is: "size: L, XL, | color: Green, White"',
  144.             $variantsString
  145.         ));
  146.     }
  147.     private function getCombinationsPayload(array $variantsstring $parentIdstring $productNumber): array
  148.     {
  149.         $combinations $this->getCombinations($variants);
  150.         $payload = [];
  151.         foreach ($combinations as $key => $combination) {
  152.             $options = [];
  153.             foreach ($combination as $option) {
  154.                 list($group$option) = explode('|'$option);
  155.                 $optionId $this->getOptionId($group$option);
  156.                 $groupId $this->getGroupId($group);
  157.                 $options[] = [
  158.                     'id' => $optionId,
  159.                     'name' => $option,
  160.                     'group' => [
  161.                         'id' => $groupId,
  162.                         'name' => $group,
  163.                     ],
  164.                 ];
  165.             }
  166.             $variantId Uuid::fromStringToHex(sprintf('%s.%s'$parentId$key));
  167.             $variantProductNumber sprintf('%s.%s'$productNumber$key);
  168.             $payload[] = [
  169.                 'id' => $variantId,
  170.                 'parentId' => $parentId,
  171.                 'productNumber' => $variantProductNumber,
  172.                 'stock' => 0,
  173.                 'options' => $options,
  174.             ];
  175.         }
  176.         return $payload;
  177.     }
  178.     /**
  179.      * convert [["size|m", "size|l"], ["color|blue", "color|red"]]
  180.      * to [["size|m", "color|blue"], ["size|l", "color|blue"], ["size|m", "color|red"], ["size|l", "color|red"]]
  181.      */
  182.     private function getCombinations(array $variantsint $currentIndex 0): array
  183.     {
  184.         if (!isset($variants[$currentIndex])) {
  185.             return [];
  186.         }
  187.         if ($currentIndex === \count($variants) - 1) {
  188.             return $variants[$currentIndex];
  189.         }
  190.         // get combinations from subsequent arrays
  191.         $combinations $this->getCombinations($variants$currentIndex 1);
  192.         $result = [];
  193.         // concat each array from tmp with each element from $variants[$i]
  194.         foreach ($variants[$currentIndex] as $variant) {
  195.             foreach ($combinations as $combination) {
  196.                 $result[] = \is_array($combination) ? array_merge([$variant], $combination) : [$variant$combination];
  197.             }
  198.         }
  199.         return $result;
  200.     }
  201.     private function getProductConfiguratorSettingPayload(array $variantsPayloadstring $parentId): array
  202.     {
  203.         $options array_merge(...array_column($variantsPayload'options'));
  204.         $optionIds array_unique(array_column($options'id'));
  205.         $payload = [];
  206.         foreach ($optionIds as $optionId) {
  207.             $payload[] = [
  208.                 'id' => Uuid::fromStringToHex(sprintf('%s_configurator'$optionId)),
  209.                 'optionId' => $optionId,
  210.                 'productId' => $parentId,
  211.             ];
  212.         }
  213.         return $payload;
  214.     }
  215.     private function getGroupId(string $groupName): string
  216.     {
  217.         $groupId Uuid::fromStringToHex($groupName);
  218.         if (isset($this->groupIdCache[$groupId])) {
  219.             return $this->groupIdCache[$groupId];
  220.         }
  221.         $criteria = new Criteria();
  222.         $criteria->addFilter(new EqualsFilter('name'$groupName));
  223.         $group $this->groupRepository->search($criteriaContext::createDefaultContext())->first();
  224.         if ($group !== null) {
  225.             $this->groupIdCache[$groupId] = $group->getId();
  226.             return $group->getId();
  227.         }
  228.         $this->groupIdCache[$groupId] = $groupId;
  229.         return $groupId;
  230.     }
  231.     private function getOptionId(string $groupNamestring $optionName): string
  232.     {
  233.         $optionId Uuid::fromStringToHex(sprintf('%s.%s'$groupName$optionName));
  234.         if (isset($this->optionIdCache[$optionId])) {
  235.             return $this->optionIdCache[$optionId];
  236.         }
  237.         $criteria = new Criteria();
  238.         $criteria->addFilter(new EqualsFilter('name'$optionName));
  239.         $criteria->addFilter(new EqualsFilter('group.name'$groupName));
  240.         $option $this->optionRepository->search($criteriaContext::createDefaultContext())->first();
  241.         if ($option !== null) {
  242.             $this->optionIdCache[$optionId] = $option->getId();
  243.             return $option->getId();
  244.         }
  245.         $this->optionIdCache[$optionId] = $optionId;
  246.         return $optionId;
  247.     }
  248. }