vendor/shopware/core/Framework/DataAbstractionLayer/VersionManager.php line 115

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\DataAbstractionLayer;
  3. use Shopware\Core\Defaults;
  4. use Shopware\Core\Framework\Api\Context\AdminApiSource;
  5. use Shopware\Core\Framework\Context;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Exception\VersionMergeAlreadyLockedException;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Field\ChildrenAssociationField;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\DateTimeField;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\CascadeDelete;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\WriteProtected;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Field\ParentFkField;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslationsAssociationField;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Field\VersionField;
  26. use Shopware\Core\Framework\DataAbstractionLayer\FieldSerializer\JsonFieldSerializer;
  27. use Shopware\Core\Framework\DataAbstractionLayer\Read\EntityReaderInterface;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  29. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearcherInterface;
  30. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  31. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  32. use Shopware\Core\Framework\DataAbstractionLayer\Version\Aggregate\VersionCommit\VersionCommitDefinition;
  33. use Shopware\Core\Framework\DataAbstractionLayer\Version\Aggregate\VersionCommit\VersionCommitEntity;
  34. use Shopware\Core\Framework\DataAbstractionLayer\Version\Aggregate\VersionCommitData\VersionCommitDataDefinition;
  35. use Shopware\Core\Framework\DataAbstractionLayer\Version\Aggregate\VersionCommitData\VersionCommitDataEntity;
  36. use Shopware\Core\Framework\DataAbstractionLayer\Version\VersionDefinition;
  37. use Shopware\Core\Framework\DataAbstractionLayer\Write\CloneBehavior;
  38. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
  39. use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityExistence;
  40. use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityWriteGatewayInterface;
  41. use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityWriterInterface;
  42. use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteContext;
  43. use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteResult;
  44. use Shopware\Core\Framework\Uuid\Uuid;
  45. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  46. use Symfony\Component\Lock\LockFactory;
  47. use Symfony\Component\Serializer\SerializerInterface;
  48. /**
  49.  * @internal
  50.  */
  51. class VersionManager
  52. {
  53.     public const DISABLE_AUDIT_LOG 'disable-audit-log';
  54.     private EntityWriterInterface $entityWriter;
  55.     private EntityReaderInterface $entityReader;
  56.     private EntitySearcherInterface $entitySearcher;
  57.     private EntityWriteGatewayInterface $entityWriteGateway;
  58.     private EventDispatcherInterface $eventDispatcher;
  59.     private SerializerInterface $serializer;
  60.     private VersionCommitDefinition $versionCommitDefinition;
  61.     private VersionCommitDataDefinition $versionCommitDataDefinition;
  62.     private VersionDefinition $versionDefinition;
  63.     private DefinitionInstanceRegistry $registry;
  64.     private LockFactory $lockFactory;
  65.     public function __construct(
  66.         EntityWriterInterface $entityWriter,
  67.         EntityReaderInterface $entityReader,
  68.         EntitySearcherInterface $entitySearcher,
  69.         EntityWriteGatewayInterface $entityWriteGateway,
  70.         EventDispatcherInterface $eventDispatcher,
  71.         SerializerInterface $serializer,
  72.         DefinitionInstanceRegistry $registry,
  73.         VersionCommitDefinition $versionCommitDefinition,
  74.         VersionCommitDataDefinition $versionCommitDataDefinition,
  75.         VersionDefinition $versionDefinition,
  76.         LockFactory $lockFactory
  77.     ) {
  78.         $this->entityWriter $entityWriter;
  79.         $this->entityReader $entityReader;
  80.         $this->entitySearcher $entitySearcher;
  81.         $this->entityWriteGateway $entityWriteGateway;
  82.         $this->eventDispatcher $eventDispatcher;
  83.         $this->serializer $serializer;
  84.         $this->versionCommitDefinition $versionCommitDefinition;
  85.         $this->versionCommitDataDefinition $versionCommitDataDefinition;
  86.         $this->versionDefinition $versionDefinition;
  87.         $this->registry $registry;
  88.         $this->lockFactory $lockFactory;
  89.     }
  90.     public function upsert(EntityDefinition $definition, array $rawDataWriteContext $writeContext): array
  91.     {
  92.         $result $this->entityWriter->upsert($definition$rawData$writeContext);
  93.         $this->writeAuditLog($result$writeContext);
  94.         return $result;
  95.     }
  96.     public function insert(EntityDefinition $definition, array $rawDataWriteContext $writeContext): array
  97.     {
  98.         /** @var EntityWriteResult[] $result */
  99.         $result $this->entityWriter->insert($definition$rawData$writeContext);
  100.         $this->writeAuditLog($result$writeContext);
  101.         return $result;
  102.     }
  103.     public function update(EntityDefinition $definition, array $rawDataWriteContext $writeContext): array
  104.     {
  105.         $result $this->entityWriter->update($definition$rawData$writeContext);
  106.         $this->writeAuditLog($result$writeContext);
  107.         return $result;
  108.     }
  109.     public function delete(EntityDefinition $definition, array $idsWriteContext $writeContext): WriteResult
  110.     {
  111.         $result $this->entityWriter->delete($definition$ids$writeContext);
  112.         $this->writeAuditLog($result->getDeleted(), $writeContext);
  113.         return $result;
  114.     }
  115.     public function createVersion(EntityDefinition $definitionstring $idWriteContext $context, ?string $name null, ?string $versionId null): string
  116.     {
  117.         $primaryKey = [
  118.             'id' => $id,
  119.             'versionId' => Defaults::LIVE_VERSION,
  120.         ];
  121.         $versionId $versionId ?? Uuid::randomHex();
  122.         $versionData = ['id' => $versionId];
  123.         if ($name) {
  124.             $versionData['name'] = $name;
  125.         }
  126.         $context->scope(Context::SYSTEM_SCOPE, function ($context) use ($versionData): void {
  127.             $this->entityWriter->upsert($this->versionDefinition, [$versionData], $context);
  128.         });
  129.         $affected $this->cloneEntity($definition$primaryKey['id'], $primaryKey['id'], $versionId$context, new CloneBehavior(), false);
  130.         $versionContext $context->createWithVersionId($versionId);
  131.         $event EntityWrittenContainerEvent::createWithWrittenEvents($affected$versionContext->getContext(), []);
  132.         $this->eventDispatcher->dispatch($event);
  133.         $this->writeAuditLog($affected$context$versionIdtrue);
  134.         return $versionId;
  135.     }
  136.     public function merge(string $versionIdWriteContext $writeContext): void
  137.     {
  138.         $lock $this->lockFactory->createLock('sw-merge-version-' $versionId);
  139.         if (!$lock->acquire()) {
  140.             throw new VersionMergeAlreadyLockedException($versionId);
  141.         }
  142.         $criteria = new Criteria();
  143.         $criteria->addFilter(new EqualsFilter('version_commit.versionId'$versionId));
  144.         $criteria->addSorting(new FieldSorting('version_commit.autoIncrement'));
  145.         $commitIds $this->entitySearcher->search($this->versionCommitDefinition$criteria$writeContext->getContext());
  146.         $readCriteria = new Criteria($commitIds->getIds());
  147.         $readCriteria->addAssociation('data');
  148.         $readCriteria
  149.             ->getAssociation('data')
  150.             ->addSorting(new FieldSorting('autoIncrement'));
  151.         $commits $this->entityReader->read($this->versionCommitDefinition$readCriteria$writeContext->getContext());
  152.         $allChanges = [];
  153.         $entities = [];
  154.         $versionContext $writeContext->createWithVersionId($versionId);
  155.         $liveContext $writeContext->createWithVersionId(Defaults::LIVE_VERSION);
  156.         $writtenEvents = [];
  157.         $deletedEvents = [];
  158.         // merge all commits into a single write operation
  159.         foreach ($commits as $commit) {
  160.             foreach ($commit->getData() as $data) {
  161.                 $dataDefinition $this->registry->getByEntityName($data->getEntityName());
  162.                 // skip clone action, otherwise the payload would contain all data
  163.                 if ($data->getAction() !== 'clone') {
  164.                     $allChanges[] = $data;
  165.                 }
  166.                 $entity = [
  167.                     'definition' => $dataDefinition,
  168.                     'primary' => $data->getEntityId(),
  169.                 ];
  170.                 // deduplicate to prevent deletion errors
  171.                 $entityKey md5(JsonFieldSerializer::encodeJson($entity));
  172.                 $entities[$entityKey] = $entity;
  173.                 if (empty($data->getPayload()) && $data->getAction() !== 'delete') {
  174.                     continue;
  175.                 }
  176.                 switch ($data->getAction()) {
  177.                     case 'insert':
  178.                     case 'update':
  179.                     case 'upsert':
  180.                         if ($dataDefinition instanceof EntityTranslationDefinition && $this->translationHasParent($commit$data)) {
  181.                             break;
  182.                         }
  183.                         $payload $this->addVersionToPayload($data->getPayload(), $dataDefinitionDefaults::LIVE_VERSION);
  184.                         $payload $this->addTranslationToPayload($data->getEntityId(), $payload$dataDefinition$commit);
  185.                         $events $this->entityWriter->upsert($dataDefinition, [$payload], $liveContext);
  186.                         $writtenEvents array_merge_recursive($writtenEvents$events);
  187.                         break;
  188.                     case 'delete':
  189.                         $id $data->getEntityId();
  190.                         $id $this->addVersionToPayload($id$dataDefinitionDefaults::LIVE_VERSION);
  191.                         $deletedEvents[] = $this->entityWriter->delete($dataDefinition, [$id], $liveContext);
  192.                         break;
  193.                 }
  194.             }
  195.             $this->entityWriter->delete($this->versionCommitDefinition, [['id' => $commit->getId()]], $liveContext);
  196.         }
  197.         $newData array_map(function (VersionCommitDataEntity $data) {
  198.             $definition $this->registry->getByEntityName($data->getEntityName());
  199.             $id $data->getEntityId();
  200.             $id $this->addVersionToPayload($id$definitionDefaults::LIVE_VERSION);
  201.             $payload $this->addVersionToPayload($data->getPayload(), $definitionDefaults::LIVE_VERSION);
  202.             return [
  203.                 'entityId' => $id,
  204.                 'payload' => JsonFieldSerializer::encodeJson($payload),
  205.                 'userId' => $data->getUserId(),
  206.                 'integrationId' => $data->getIntegrationId(),
  207.                 'entityName' => $data->getEntityName(),
  208.                 'action' => $data->getAction(),
  209.                 'createdAt' => (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  210.             ];
  211.         }, $allChanges);
  212.         $commit = [
  213.             'versionId' => Defaults::LIVE_VERSION,
  214.             'data' => $newData,
  215.             'userId' => $writeContext->getContext()->getSource() instanceof AdminApiSource $writeContext->getContext()->getSource()->getUserId() : null,
  216.             'isMerge' => true,
  217.             'message' => 'merge commit ' . (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT),
  218.         ];
  219.         // create new version commit for merge commit
  220.         $this->entityWriter->insert($this->versionCommitDefinition, [$commit], $writeContext);
  221.         // delete version
  222.         $this->entityWriter->delete($this->versionDefinition, [['id' => $versionId]], $writeContext);
  223.         $versionContext->addState('merge-scope');
  224.         foreach ($entities as $entity) {
  225.             /** @var EntityDefinition|string $definition */
  226.             $definition $entity['definition'];
  227.             $primary $entity['primary'];
  228.             $primary $this->addVersionToPayload($primary$definition$versionId);
  229.             $this->entityWriter->delete($definition, [$primary], $versionContext);
  230.         }
  231.         $versionContext->removeState('merge-scope');
  232.         $lock->release();
  233.         $event EntityWrittenContainerEvent::createWithWrittenEvents($writtenEvents$liveContext->getContext(), []);
  234.         $this->eventDispatcher->dispatch($event);
  235.         foreach ($deletedEvents as $deletedEvent) {
  236.             $event EntityWrittenContainerEvent::createWithDeletedEvents($deletedEvent->getDeleted(), $liveContext->getContext(), $deletedEvent->getNotFound());
  237.             $this->eventDispatcher->dispatch($event);
  238.         }
  239.     }
  240.     public function clone(
  241.         EntityDefinition $definition,
  242.         string $id,
  243.         string $newId,
  244.         string $versionId,
  245.         WriteContext $context,
  246.         CloneBehavior $behavior
  247.     ): array {
  248.         return $this->cloneEntity($definition$id$newId$versionId$context$behaviortrue);
  249.     }
  250.     private function cloneEntity(
  251.         EntityDefinition $definition,
  252.         string $id,
  253.         string $newId,
  254.         string $versionId,
  255.         WriteContext $context,
  256.         CloneBehavior $behavior,
  257.         bool $writeAuditLog false
  258.     ): array {
  259.         $criteria = new Criteria([$id]);
  260.         $this->addCloneAssociations($definition$criteria$behavior->cloneChildren());
  261.         $detail $this->entityReader->read($definition$criteria$context->getContext())->first();
  262.         if ($detail === null) {
  263.             throw new \RuntimeException(sprintf('Cannot create new version. %s by id (%s) not found.'$definition->getEntityName(), $id));
  264.         }
  265.         $data json_decode($this->serializer->serialize($detail'json'), true);
  266.         $keepIds $newId === $id;
  267.         $data $this->filterPropertiesForClone($definition$data$keepIds$id$definition$context->getContext());
  268.         $data['id'] = $newId;
  269.         $createdAtField $definition->getField('createdAt');
  270.         $updatedAtField $definition->getField('updatedAt');
  271.         if ($createdAtField instanceof DateTimeField) {
  272.             $data['createdAt'] = new \DateTime();
  273.         }
  274.         if ($updatedAtField instanceof DateTimeField) {
  275.             if ($updatedAtField->getFlag(Required::class)) {
  276.                 $data['updatedAt'] = new \DateTime();
  277.             } else {
  278.                 $data['updatedAt'] = null;
  279.             }
  280.         }
  281.         $data array_replace_recursive($data$behavior->getOverwrites());
  282.         $versionContext $context->createWithVersionId($versionId);
  283.         $result null;
  284.         $versionContext->scope(Context::SYSTEM_SCOPE, function (WriteContext $context) use ($definition$data, &$result): void {
  285.             $result $this->entityWriter->insert($definition, [$data], $context);
  286.         });
  287.         if ($writeAuditLog) {
  288.             $this->writeAuditLog($result$versionContext);
  289.         }
  290.         return $result;
  291.     }
  292.     private function filterPropertiesForClone(EntityDefinition $definition, array $databool $keepIdsstring $cloneIdEntityDefinition $cloneDefinitionContext $context): array
  293.     {
  294.         $extensions = [];
  295.         $payload = [];
  296.         $fields $definition->getFields();
  297.         foreach ($fields as $field) {
  298.             /** @var WriteProtected|null $writeProtection */
  299.             $writeProtection $field->getFlag(WriteProtected::class);
  300.             if ($writeProtection && !$writeProtection->isAllowed(Context::SYSTEM_SCOPE)) {
  301.                 continue;
  302.             }
  303.             //set data and payload cursor to root or extensions to simplify following if conditions
  304.             $dataCursor $data;
  305.             $payloadCursor = &$payload;
  306.             if ($field instanceof VersionField || $field instanceof ReferenceVersionField) {
  307.                 continue;
  308.             }
  309.             if ($field->is(Extension::class)) {
  310.                 $dataCursor $data['extensions'] ?? [];
  311.                 $payloadCursor = &$extensions;
  312.             }
  313.             if (!\array_key_exists($field->getPropertyName(), $dataCursor)) {
  314.                 continue;
  315.             }
  316.             if (!$keepIds && $field instanceof ParentFkField) {
  317.                 continue;
  318.             }
  319.             $value $dataCursor[$field->getPropertyName()];
  320.             // remove reference of cloned entity in all sub entity routes. Appears in a parent-child nested data tree
  321.             if ($field instanceof FkField && !$keepIds && $value === $cloneId && $cloneDefinition === $field->getReferenceDefinition()) {
  322.                 continue;
  323.             }
  324.             if ($value === null) {
  325.                 continue;
  326.             }
  327.             //scalar value? assign directly
  328.             if (!$field instanceof AssociationField) {
  329.                 $payloadCursor[$field->getPropertyName()] = $value;
  330.                 continue;
  331.             }
  332.             //many to one should be skipped because it is no part of the root entity
  333.             if ($field instanceof ManyToOneAssociationField) {
  334.                 continue;
  335.             }
  336.             /** @var CascadeDelete|null $flag */
  337.             $flag $field->getFlag(CascadeDelete::class);
  338.             if (!$flag || !$flag->isCloneRelevant()) {
  339.                 continue;
  340.             }
  341.             if ($field instanceof OneToManyAssociationField) {
  342.                 $reference $field->getReferenceDefinition();
  343.                 $nested = [];
  344.                 foreach ($value as $item) {
  345.                     $nestedItem $this->filterPropertiesForClone($reference$item$keepIds$cloneId$cloneDefinition$context);
  346.                     if (!$keepIds) {
  347.                         $nestedItem $this->removePrimaryKey($field$nestedItem);
  348.                     }
  349.                     $nested[] = $nestedItem;
  350.                 }
  351.                 $nested array_filter($nested);
  352.                 if (empty($nested)) {
  353.                     continue;
  354.                 }
  355.                 $payloadCursor[$field->getPropertyName()] = $nested;
  356.                 continue;
  357.             }
  358.             if ($field instanceof ManyToManyAssociationField) {
  359.                 $nested = [];
  360.                 foreach ($value as $item) {
  361.                     $nested[] = ['id' => $item['id']];
  362.                 }
  363.                 if (empty($nested)) {
  364.                     continue;
  365.                 }
  366.                 $payloadCursor[$field->getPropertyName()] = $nested;
  367.                 continue;
  368.             }
  369.             if ($field instanceof OneToOneAssociationField && $value) {
  370.                 $reference $field->getReferenceDefinition();
  371.                 $nestedItem $this->filterPropertiesForClone($reference$value$keepIds$cloneId$cloneDefinition$context);
  372.                 if (!$keepIds) {
  373.                     $nestedItem $this->removePrimaryKey($field$nestedItem);
  374.                 }
  375.                 $payloadCursor[$field->getPropertyName()] = $nestedItem;
  376.             }
  377.         }
  378.         if (!empty($extensions)) {
  379.             $payload['extensions'] = $extensions;
  380.         }
  381.         return $payload;
  382.     }
  383.     private function writeAuditLog(array $writtenEventsWriteContext $writeContext, ?string $versionId nullbool $isClone false): void
  384.     {
  385.         if ($writeContext->getContext()->hasState(self::DISABLE_AUDIT_LOG)) {
  386.             return;
  387.         }
  388.         $versionId $versionId ?? $writeContext->getContext()->getVersionId();
  389.         if ($versionId === Defaults::LIVE_VERSION) {
  390.             return;
  391.         }
  392.         $commitId Uuid::randomBytes();
  393.         $date = (new \DateTime())->format(Defaults::STORAGE_DATE_TIME_FORMAT);
  394.         $source $writeContext->getContext()->getSource();
  395.         $userId $source instanceof AdminApiSource && $source->getUserId()
  396.             ? Uuid::fromHexToBytes($source->getUserId())
  397.             : null;
  398.         $insert = new InsertCommand(
  399.             $this->versionCommitDefinition,
  400.             [
  401.                 'id' => $commitId,
  402.                 'user_id' => $userId,
  403.                 'version_id' => Uuid::fromHexToBytes($versionId),
  404.                 'created_at' => $date,
  405.             ],
  406.             ['id' => $commitId],
  407.             new EntityExistence(
  408.                 $this->versionCommitDefinition->getEntityName(),
  409.                 ['id' => Uuid::fromBytesToHex($commitId)],
  410.                 false,
  411.                 false,
  412.                 false,
  413.                 []
  414.             ),
  415.             ''
  416.         );
  417.         $commands = [$insert];
  418.         foreach ($writtenEvents as $items) {
  419.             if (\count($items) === 0) {
  420.                 continue;
  421.             }
  422.             $definition $this->registry->getByEntityName($items[0]->getEntityName());
  423.             $entityName $definition->getEntityName();
  424.             if (!$definition->isVersionAware()) {
  425.                 continue;
  426.             }
  427.             if (mb_strpos('version'$entityName) === 0) {
  428.                 continue;
  429.             }
  430.             /** @var EntityWriteResult $item */
  431.             foreach ($items as $item) {
  432.                 $payload $item->getPayload();
  433.                 $primary $item->getPrimaryKey();
  434.                 if (!\is_array($primary)) {
  435.                     $primary = ['id' => $primary];
  436.                 }
  437.                 $primary['versionId'] = $versionId;
  438.                 $id Uuid::randomBytes();
  439.                 $commands[] = new InsertCommand(
  440.                     $this->versionCommitDataDefinition,
  441.                     [
  442.                         'id' => $id,
  443.                         'version_commit_id' => $commitId,
  444.                         'entity_name' => $entityName,
  445.                         'entity_id' => JsonFieldSerializer::encodeJson($primary),
  446.                         'payload' => JsonFieldSerializer::encodeJson($payload),
  447.                         'user_id' => $userId,
  448.                         'action' => $isClone 'clone' $item->getOperation(),
  449.                         'created_at' => $date,
  450.                     ],
  451.                     ['id' => $id],
  452.                     new EntityExistence(
  453.                         $this->versionCommitDataDefinition->getEntityName(),
  454.                         ['id' => Uuid::fromBytesToHex($id)],
  455.                         false,
  456.                         false,
  457.                         false,
  458.                         []
  459.                     ),
  460.                     ''
  461.                 );
  462.             }
  463.         }
  464.         if (\count($commands) <= 1) {
  465.             return;
  466.         }
  467.         $writeContext->scope(Context::SYSTEM_SCOPE, function () use ($commands$writeContext): void {
  468.             $this->entityWriteGateway->execute($commands$writeContext);
  469.         });
  470.     }
  471.     private function addVersionToPayload(array $payloadEntityDefinition $definitionstring $versionId): array
  472.     {
  473.         $fields $definition->getFields()->filter(function (Field $field) {
  474.             return $field instanceof VersionField || $field instanceof ReferenceVersionField;
  475.         });
  476.         foreach ($fields as $field) {
  477.             $payload[$field->getPropertyName()] = $versionId;
  478.         }
  479.         return $payload;
  480.     }
  481.     private function removePrimaryKey(AssociationField $field, array $nestedItem): array
  482.     {
  483.         $pkFields $field->getReferenceDefinition()->getPrimaryKeys();
  484.         /** @var Field|StorageAware $pkField */
  485.         foreach ($pkFields as $pkField) {
  486.             /*
  487.              * `EntityTranslationDefinition`s dont have an `id`, they use a composite primary key consisting of the
  488.              * entity id and the `languageId`. When cloning the entity we want to copy the `languageId`. The entity id
  489.              * has to be unset, so that its set by the parent, resulting in a valid primary key.
  490.              */
  491.             if ($field instanceof TranslationsAssociationField && $pkField->getStorageName() === $field->getLanguageField()) {
  492.                 continue;
  493.             }
  494.             if (\array_key_exists($pkField->getPropertyName(), $nestedItem)) {
  495.                 unset($nestedItem[$pkField->getPropertyName()]);
  496.             }
  497.         }
  498.         return $nestedItem;
  499.     }
  500.     private function addCloneAssociations(
  501.         EntityDefinition $definition,
  502.         Criteria $criteria,
  503.         bool $cloneChildren,
  504.         int $childCounter 1
  505.     ): void {
  506.         //add all cascade delete associations
  507.         $cascades $definition->getFields()->filter(function (Field $field) {
  508.             /** @var CascadeDelete|null $flag */
  509.             $flag $field->getFlag(CascadeDelete::class);
  510.             return $flag $flag->isCloneRelevant() : false;
  511.         });
  512.         /** @var AssociationField $cascade */
  513.         foreach ($cascades as $cascade) {
  514.             $nested $criteria->getAssociation($cascade->getPropertyName());
  515.             if ($cascade instanceof ManyToManyAssociationField) {
  516.                 continue;
  517.             }
  518.             //many to one shouldn't be cascaded
  519.             if ($cascade instanceof ManyToOneAssociationField) {
  520.                 continue;
  521.             }
  522.             $reference $cascade->getReferenceDefinition();
  523.             $childrenAware $reference->isChildrenAware();
  524.             //first level of parent-child tree?
  525.             if ($childrenAware && $reference !== $definition) {
  526.                 //where product.children.parentId IS NULL
  527.                 $nested->addFilter(new EqualsFilter($reference->getEntityName() . '.parentId'null));
  528.             }
  529.             if ($cascade instanceof ChildrenAssociationField) {
  530.                 //break endless loop
  531.                 if ($childCounter >= 30 || !$cloneChildren) {
  532.                     $criteria->removeAssociation($cascade->getPropertyName());
  533.                     continue;
  534.                 }
  535.                 ++$childCounter;
  536.                 $this->addCloneAssociations($reference$nested$cloneChildren$childCounter);
  537.                 continue;
  538.             }
  539.             $this->addCloneAssociations($reference$nested$cloneChildren);
  540.         }
  541.     }
  542.     private function translationHasParent(VersionCommitEntity $commitVersionCommitDataEntity $translationData): bool
  543.     {
  544.         $translationDefinition $this->registry->getByEntityName($translationData->getEntityName());
  545.         $parentEntity $translationDefinition->getParentDefinition()->getEntityName();
  546.         $parentPropertyName $this->getEntityForeignKeyName($parentEntity);
  547.         $parentId $translationData->getPayload()[$parentPropertyName];
  548.         foreach ($commit->getData() as $data) {
  549.             if ($data->getEntityName() !== $parentEntity) {
  550.                 continue;
  551.             }
  552.             $primary $data->getEntityId();
  553.             if (!isset($primary['id'])) {
  554.                 continue;
  555.             }
  556.             if ($primary['id'] === $parentId) {
  557.                 return true;
  558.             }
  559.         }
  560.         return false;
  561.     }
  562.     private function addTranslationToPayload(array $entityId, array $payloadEntityDefinition $definitionVersionCommitEntity $commit): array
  563.     {
  564.         $translationDefinition $definition->getTranslationDefinition();
  565.         if (!$translationDefinition) {
  566.             return $payload;
  567.         }
  568.         if (!isset($entityId['id'])) {
  569.             return $payload;
  570.         }
  571.         $id $entityId['id'];
  572.         $translations = [];
  573.         $foreignKeyName $this->getEntityForeignKeyName($definition->getEntityName());
  574.         foreach ($commit->getData() as $data) {
  575.             if ($data->getEntityName() !== $translationDefinition->getEntityName()) {
  576.                 continue;
  577.             }
  578.             $translation $data->getPayload();
  579.             if (!isset($translation[$foreignKeyName])) {
  580.                 continue;
  581.             }
  582.             if ($translation[$foreignKeyName] !== $id) {
  583.                 continue;
  584.             }
  585.             $translations[] = $this->addVersionToPayload($translation$translationDefinitionDefaults::LIVE_VERSION);
  586.         }
  587.         $payload['translations'] = $translations;
  588.         return $payload;
  589.     }
  590.     private function getEntityForeignKeyName(string $parentEntity): string
  591.     {
  592.         $parentPropertyName explode('_'$parentEntity);
  593.         $parentPropertyName array_map('ucfirst'$parentPropertyName);
  594.         return lcfirst(implode(''$parentPropertyName)) . 'Id';
  595.     }
  596. }