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 EventSubscriberInterface, ResetInterface
  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(true, true);
  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 $variants, string $parentId, string $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 $variants, int $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 $variantsPayload, string $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($criteria, Context::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 $groupName, string $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($criteria, Context::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. }