vendor/shopware/core/Framework/DataAbstractionLayer/Write/EntityWriter.php line 177

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\DataAbstractionLayer\Write;
  3. use Shopware\Core\Framework\Api\Exception\IncompletePrimaryKeyException;
  4. use Shopware\Core\Framework\Api\Sync\SyncOperation;
  5. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityForeignKeyResolver;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityHydrator;
  7. use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
  8. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  9. use Shopware\Core\Framework\DataAbstractionLayer\EntityWriteResult;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\SetNullOnDelete;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Field\VersionField;
  16. use Shopware\Core\Framework\DataAbstractionLayer\MappingEntityDefinition;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\CascadeDeleteCommand;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\DeleteCommand;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\SetNullOnDeleteCommand;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommandQueue;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\RestrictDeleteViolation;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\RestrictDeleteViolationException;
  25. use Shopware\Core\Framework\Uuid\Uuid;
  26. use Shopware\Core\System\Language\LanguageLoaderInterface;
  27. /**
  28.  * @deprecated tag:v6.5.0 - reason:becomes-internal - Will be internal
  29.  * Handles all write operations in the system.
  30.  * Builds first a command queue over the WriteCommandExtractor and let execute this queue
  31.  * over the EntityWriteGateway (sql implementation in default).
  32.  */
  33. class EntityWriter implements EntityWriterInterface
  34. {
  35.     private EntityForeignKeyResolver $foreignKeyResolver;
  36.     private WriteCommandExtractor $commandExtractor;
  37.     private EntityWriteGatewayInterface $gateway;
  38.     private LanguageLoaderInterface $languageLoader;
  39.     private DefinitionInstanceRegistry $registry;
  40.     private EntityWriteResultFactory $factory;
  41.     /**
  42.      * @internal
  43.      */
  44.     public function __construct(
  45.         WriteCommandExtractor $writeResource,
  46.         EntityForeignKeyResolver $foreignKeyResolver,
  47.         EntityWriteGatewayInterface $gateway,
  48.         LanguageLoaderInterface $languageLoader,
  49.         DefinitionInstanceRegistry $registry,
  50.         EntityWriteResultFactory $factory
  51.     ) {
  52.         $this->foreignKeyResolver $foreignKeyResolver;
  53.         $this->commandExtractor $writeResource;
  54.         $this->gateway $gateway;
  55.         $this->languageLoader $languageLoader;
  56.         $this->registry $registry;
  57.         $this->factory $factory;
  58.     }
  59.     // TODO: prefetch
  60.     public function sync(array $operationsWriteContext $context): WriteResult
  61.     {
  62.         $commandQueue = new WriteCommandQueue();
  63.         $context->setLanguages(
  64.             $this->languageLoader->loadLanguages()
  65.         );
  66.         $writes = [];
  67.         $notFound = [];
  68.         $deletes = [];
  69.         foreach ($operations as $operation) {
  70.             if (!$operation instanceof SyncOperation) {
  71.                 continue;
  72.             }
  73.             $definition $this->registry->getByEntityName($operation->getEntity());
  74.             $this->validateWriteInput($operation->getPayload());
  75.             if ($operation->getAction() === SyncOperation::ACTION_DELETE) {
  76.                 $deletes[] = $this->factory->resolveDelete($definition$operation->getPayload());
  77.                 $notFound[] = $this->extractDeleteCommands($definition$operation->getPayload(), $context$commandQueue);
  78.                 continue;
  79.             }
  80.             if ($operation->getAction() === SyncOperation::ACTION_UPSERT) {
  81.                 $parameters = new WriteParameterBag($definition$context''$commandQueue);
  82.                 $payload $this->commandExtractor->normalize($definition$operation->getPayload(), $parameters);
  83.                 $this->gateway->prefetchExistences($parameters);
  84.                 $key $operation->getKey();
  85.                 foreach ($payload as $index => $row) {
  86.                     $parameters->setPath('/' $key '/' $index);
  87.                     $context->resetPaths();
  88.                     $this->commandExtractor->extract($row$parameters);
  89.                 }
  90.                 $writes[] = $this->factory->resolveWrite($definition$payload);
  91.             }
  92.         }
  93.         $context->getExceptions()->tryToThrow();
  94.         $this->gateway->execute($commandQueue->getCommandsInOrder(), $context);
  95.         $result $this->factory->build($commandQueue);
  96.         $notFound array_merge_recursive(...$notFound);
  97.         $writes array_merge_recursive(...$writes);
  98.         $deletes array_merge_recursive(...$deletes);
  99.         $result $this->factory->addParentResults($result$writes);
  100.         $result $this->factory->addDeleteResults($result$notFound$deletes);
  101.         return $result;
  102.     }
  103.     public function upsert(EntityDefinition $definition, array $rawDataWriteContext $writeContext): array
  104.     {
  105.         return $this->write($definition$rawData$writeContext);
  106.     }
  107.     public function insert(EntityDefinition $definition, array $rawDataWriteContext $writeContext): array
  108.     {
  109.         return $this->write($definition$rawData$writeContextInsertCommand::class);
  110.     }
  111.     public function update(EntityDefinition $definition, array $rawDataWriteContext $writeContext): array
  112.     {
  113.         return $this->write($definition$rawData$writeContextUpdateCommand::class);
  114.     }
  115.     /**
  116.      * @throws IncompletePrimaryKeyException
  117.      * @throws RestrictDeleteViolationException
  118.      */
  119.     public function delete(EntityDefinition $definition, array $idsWriteContext $writeContext): WriteResult
  120.     {
  121.         $this->validateWriteInput($ids);
  122.         $parents = [];
  123.         if (!$writeContext->hasState('merge-scope')) {
  124.             $parents $this->factory->resolveDelete($definition$ids);
  125.         }
  126.         $commandQueue = new WriteCommandQueue();
  127.         $notFound $this->extractDeleteCommands($definition$ids$writeContext$commandQueue);
  128.         $writeContext->setLanguages($this->languageLoader->loadLanguages());
  129.         $this->gateway->execute($commandQueue->getCommandsInOrder(), $writeContext);
  130.         $result $this->factory->build($commandQueue);
  131.         $parents array_merge_recursive($parents$this->factory->resolveMappings($result));
  132.         return $this->factory->addDeleteResults($result$notFound$parents);
  133.     }
  134.     private function write(EntityDefinition $definition, array $rawDataWriteContext $writeContext, ?string $ensure null): array
  135.     {
  136.         $this->validateWriteInput($rawData);
  137.         if (!$rawData) {
  138.             return [];
  139.         }
  140.         $commandQueue = new WriteCommandQueue();
  141.         $parameters = new WriteParameterBag($definition$writeContext''$commandQueue);
  142.         $writeContext->setLanguages($this->languageLoader->loadLanguages());
  143.         $rawData $this->commandExtractor->normalize($definition$rawData$parameters);
  144.         $writeContext->getExceptions()->tryToThrow();
  145.         $this->gateway->prefetchExistences($parameters);
  146.         foreach ($rawData as $index => $row) {
  147.             $parameters->setPath('/' $index);
  148.             $writeContext->resetPaths();
  149.             $this->commandExtractor->extract($row$parameters);
  150.         }
  151.         if ($ensure) {
  152.             $commandQueue->ensureIs($definition$ensure);
  153.         }
  154.         $writeContext->getExceptions()->tryToThrow();
  155.         $this->gateway->execute($commandQueue->getCommandsInOrder(), $writeContext);
  156.         $result $this->factory->build($commandQueue);
  157.         $parents array_merge(
  158.             $this->factory->resolveWrite($definition$rawData),
  159.             $this->factory->resolveMappings($result)
  160.         );
  161.         return $this->factory->addParentResults($result$parents);
  162.     }
  163.     /**
  164.      * @throws \InvalidArgumentException
  165.      */
  166.     private function validateWriteInput(array $data): void
  167.     {
  168.         $valid array_keys($data) === range(0, \count($data) - 1) || $data === [];
  169.         if (!$valid) {
  170.             throw new \InvalidArgumentException('Expected input to be non associative array.');
  171.         }
  172.     }
  173.     private function addReverseInheritedCommands(WriteCommandQueue $queueEntityDefinition $definitionWriteContext $writeContext, array $resolved): void
  174.     {
  175.         if ($definition instanceof MappingEntityDefinition) {
  176.             return;
  177.         }
  178.         $cascades $this->foreignKeyResolver->getAllReverseInherited($definition$resolved$writeContext->getContext());
  179.         foreach ($cascades as $affectedDefinitionClass => $keys) {
  180.             $affectedDefinition $this->registry->getByEntityName($affectedDefinitionClass);
  181.             foreach ($keys as $key) {
  182.                 if (!\is_array($key)) {
  183.                     $key = ['id' => $key];
  184.                 }
  185.                 $primary EntityHydrator::encodePrimaryKey($affectedDefinition$key$writeContext->getContext());
  186.                 $existence = new EntityExistence($affectedDefinition->getEntityName(), $primarytruefalsefalse, []);
  187.                 $queue->add($affectedDefinition, new UpdateCommand($affectedDefinition, [], $primary$existence''));
  188.             }
  189.         }
  190.     }
  191.     private function addDeleteCascadeCommands(WriteCommandQueue $queueEntityDefinition $definitionWriteContext $writeContext, array $resolved): void
  192.     {
  193.         if ($definition instanceof MappingEntityDefinition) {
  194.             return;
  195.         }
  196.         $cascades $this->foreignKeyResolver->getAffectedDeletes($definition$resolved$writeContext->getContext());
  197.         foreach ($cascades as $affectedDefinitionClass => $keys) {
  198.             $affectedDefinition $this->registry->getByEntityName($affectedDefinitionClass);
  199.             foreach ($keys as $key) {
  200.                 if (!\is_array($key)) {
  201.                     $key = ['id' => $key];
  202.                 }
  203.                 $primary EntityHydrator::encodePrimaryKey($affectedDefinition$key$writeContext->getContext());
  204.                 $existence = new EntityExistence($affectedDefinition->getEntityName(), $primarytruefalsefalse, []);
  205.                 $queue->add($affectedDefinition, new CascadeDeleteCommand($affectedDefinition$primary$existence));
  206.             }
  207.         }
  208.     }
  209.     private function addSetNullOnDeletesCommands(WriteCommandQueue $queueEntityDefinition $definitionWriteContext $writeContext, array $resolved): void
  210.     {
  211.         if ($definition instanceof MappingEntityDefinition) {
  212.             return;
  213.         }
  214.         $setNullFields $definition->getFields()->filterByFlag(SetNullOnDelete::class);
  215.         $setNulls $this->foreignKeyResolver->getAffectedSetNulls($definition$resolved$writeContext->getContext());
  216.         foreach ($setNulls as $affectedDefinitionClass => $restrictions) {
  217.             [$entity$field] = explode('.'$affectedDefinitionClass);
  218.             $affectedDefinition $this->registry->getByEntityName($entity);
  219.             /** @var AssociationField $associationField */
  220.             $associationField $setNullFields
  221.                 ->filter(fn (Field $setNullField) => $setNullField instanceof AssociationField && $setNullField->getReferenceField() === $field)
  222.                 ->first();
  223.             /** @var SetNullOnDelete $flag */
  224.             $flag $associationField->getFlag(SetNullOnDelete::class);
  225.             foreach ($restrictions as $key) {
  226.                 $payload = ['id' => Uuid::fromHexToBytes($key), $field => null];
  227.                 $primary EntityHydrator::encodePrimaryKey($affectedDefinition, ['id' => $key], $writeContext->getContext());
  228.                 $existence = new EntityExistence($affectedDefinition->getEntityName(), $primarytruefalsefalse, []);
  229.                 if ($definition->isVersionAware()) {
  230.                     $versionField str_replace('_id''_version_id'$field);
  231.                     $payload[$versionField] = null;
  232.                 }
  233.                 $queue->add($affectedDefinition, new SetNullOnDeleteCommand($affectedDefinition$payload$primary$existence''$flag->isEnforcedByConstraint()));
  234.             }
  235.         }
  236.     }
  237.     private function resolvePrimaryKeys(array $idsEntityDefinition $definitionWriteContext $writeContext): array
  238.     {
  239.         $fields $definition->getPrimaryKeys();
  240.         $resolved = [];
  241.         foreach ($ids as $raw) {
  242.             $mapped = [];
  243.             foreach ($fields as $field) {
  244.                 $property $field->getPropertyName();
  245.                 if (!($field instanceof StorageAware)) {
  246.                     continue;
  247.                 }
  248.                 if (\array_key_exists($property$raw)) {
  249.                     $mapped[$field->getStorageName()] = $raw[$property];
  250.                     continue;
  251.                 }
  252.                 if ($field instanceof ReferenceVersionField) {
  253.                     $mapped[$field->getStorageName()] = $writeContext->getContext()->getVersionId();
  254.                     continue;
  255.                 }
  256.                 if ($field instanceof VersionField) {
  257.                     $mapped[$field->getStorageName()] = $writeContext->getContext()->getVersionId();
  258.                     continue;
  259.                 }
  260.                 $fieldKeys $fields
  261.                     ->filter(
  262.                         function (Field $field) {
  263.                             return !$field instanceof VersionField && !$field instanceof ReferenceVersionField;
  264.                         }
  265.                     )
  266.                     ->map(
  267.                         function (Field $field) {
  268.                             return $field->getPropertyName();
  269.                         }
  270.                     );
  271.                 throw new IncompletePrimaryKeyException($fieldKeys);
  272.             }
  273.             $resolved[] = $mapped;
  274.         }
  275.         return $resolved;
  276.     }
  277.     private function extractDeleteCommands(EntityDefinition $definition, array $idsWriteContext $writeContextWriteCommandQueue $commandQueue): array
  278.     {
  279.         $parameters = new WriteParameterBag($definition$writeContext''$commandQueue);
  280.         $ids $this->commandExtractor->normalize($definition$ids$parameters);
  281.         $this->gateway->prefetchExistences($parameters);
  282.         $resolved $this->resolvePrimaryKeys($ids$definition$writeContext);
  283.         if (!$definition instanceof MappingEntityDefinition) {
  284.             $restrictions $this->foreignKeyResolver->getAffectedDeleteRestrictions($definition$resolved$writeContext->getContext(), true);
  285.             if (!empty($restrictions)) {
  286.                 throw new RestrictDeleteViolationException($definition, [new RestrictDeleteViolation($restrictions)]);
  287.             }
  288.         }
  289.         $skipped = [];
  290.         foreach ($resolved as $primaryKey) {
  291.             $mappedBytes array_map(function ($id) {
  292.                 return Uuid::fromHexToBytes($id);
  293.             }, $primaryKey);
  294.             $existence $this->gateway->getExistence($definition$mappedBytes, [], $commandQueue);
  295.             if ($existence->exists()) {
  296.                 $commandQueue->add($definition, new DeleteCommand($definition$mappedBytes$existence));
  297.                 continue;
  298.             }
  299.             $stripped = [];
  300.             foreach ($primaryKey as $key => $value) {
  301.                 $field $definition->getFields()->getByStorageName($key);
  302.                 if ($field instanceof VersionField || $field instanceof ReferenceVersionField) {
  303.                     continue;
  304.                 }
  305.                 $stripped[$key] = $value;
  306.             }
  307.             $skipped[$definition->getEntityName()][] = new EntityWriteResult(
  308.                 \count($stripped) === array_shift($stripped) : $stripped,
  309.                 $stripped,
  310.                 $definition->getEntityName(),
  311.                 EntityWriteResult::OPERATION_DELETE,
  312.                 $existence
  313.             );
  314.         }
  315.         // we had some logic in the command layer (pre-validate, post-validate, indexer which listens to this events)
  316.         // to trigger this logic for cascade deletes or set nulls, we add a fake commands for the affected rows
  317.         $this->addReverseInheritedCommands($commandQueue$definition$writeContext$resolved);
  318.         $this->addDeleteCascadeCommands($commandQueue$definition$writeContext$resolved);
  319.         $this->addSetNullOnDeletesCommands($commandQueue$definition$writeContext$resolved);
  320.         return $skipped;
  321.     }
  322. }