vendor/shopware/core/Framework/DataAbstractionLayer/Write/WriteCommandExtractor.php line 213

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\DataAbstractionLayer\Write;
  3. use Shopware\Core\Framework\DataAbstractionLayer\DefinitionInstanceRegistry;
  4. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  5. use Shopware\Core\Framework\DataAbstractionLayer\Exception\CanNotFindParentStorageFieldException;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InvalidParentAssociationException;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Exception\ParentFieldForeignKeyConstraintMissingException;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Exception\ParentFieldNotFoundException;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\ChildrenAssociationField;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Field\CreatedByField;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Computed;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Inherited;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Runtime;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\WriteProtected;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslationsAssociationField;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Field\UpdatedAtField;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Field\UpdatedByField;
  27. use Shopware\Core\Framework\DataAbstractionLayer\MappingEntityDefinition;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\InsertCommand;
  29. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\JsonUpdateCommand;
  30. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
  31. use Shopware\Core\Framework\DataAbstractionLayer\Write\DataStack\DataStack;
  32. use Shopware\Core\Framework\DataAbstractionLayer\Write\DataStack\KeyValuePair;
  33. use Shopware\Core\Framework\DataAbstractionLayer\Write\FieldException\WriteFieldException;
  34. use Shopware\Core\Framework\Feature;
  35. use Shopware\Core\Framework\Uuid\Uuid;
  36. use Shopware\Core\Framework\Validation\WriteConstraintViolationException;
  37. use Symfony\Component\Validator\ConstraintViolation;
  38. use Symfony\Component\Validator\ConstraintViolationList;
  39. /**
  40.  * @deprecated tag:v6.5.0 - reason:becomes-internal - Will be internal
  41.  * Builds the command queue for write operations.
  42.  *
  43.  * Contains recursive calls from extract->map->AssociationInterface->extract->map->....
  44.  */
  45. class WriteCommandExtractor
  46. {
  47.     private EntityWriteGatewayInterface $entityExistenceGateway;
  48.     /**
  49.      * @var DefinitionInstanceRegistry
  50.      */
  51.     private $definitionRegistry;
  52.     private array $fieldsForPrimaryKeyMapping = [];
  53.     /**
  54.      * @internal
  55.      */
  56.     public function __construct(
  57.         EntityWriteGatewayInterface $entityExistenceGateway,
  58.         DefinitionInstanceRegistry $definitionRegistry
  59.     ) {
  60.         $this->entityExistenceGateway $entityExistenceGateway;
  61.         $this->definitionRegistry $definitionRegistry;
  62.     }
  63.     public function normalize(EntityDefinition $definition, array $rawDataWriteParameterBag $parameters): array
  64.     {
  65.         foreach ($rawData as $i => $row) {
  66.             $parameters->setPath('/' $i);
  67.             $row $this->normalizeSingle($definition$row$parameters);
  68.             $rawData[$i] = $row;
  69.         }
  70.         return $rawData;
  71.     }
  72.     public function normalizeSingle(EntityDefinition $definition, array $dataWriteParameterBag $parameters): array
  73.     {
  74.         $done = [];
  75.         foreach ($definition->getPrimaryKeys() as $pkField) {
  76.             $data $pkField->getSerializer()->normalize($pkField$data$parameters);
  77.             $done[$pkField->getPropertyName()] = true;
  78.         }
  79.         $normalizedTranslations false;
  80.         foreach ($data as $property => $_) {
  81.             if (\array_key_exists($property$done)) {
  82.                 continue;
  83.             }
  84.             $field $definition->getFields()->get($property);
  85.             if ($field === null || $field instanceof AssociationField) {
  86.                 continue;
  87.             }
  88.             if ($field instanceof TranslatedField) {
  89.                 $normalizedTranslations true;
  90.             }
  91.             try {
  92.                 $data $field->getSerializer()->normalize($field$data$parameters);
  93.             } catch (WriteFieldException $e) {
  94.                 $parameters->getContext()->getExceptions()->add($e);
  95.             }
  96.             $done[$property] = true;
  97.         }
  98.         $translationsField $definition->getFields()->get('translations');
  99.         if ($translationsField instanceof TranslationsAssociationField) {
  100.             $data $this->normalizeTranslations($translationsField$data$parameters$normalizedTranslations);
  101.         }
  102.         foreach ($data as $property => $value) {
  103.             if (\array_key_exists($property$done)) {
  104.                 continue;
  105.             }
  106.             if ($property === 'extensions') {
  107.                 foreach ($value as $extensionName => $_) {
  108.                     $field $definition->getFields()->get($extensionName);
  109.                     if ($field === null) {
  110.                         continue;
  111.                     }
  112.                     try {
  113.                         $value $field->getSerializer()->normalize($field$value$parameters);
  114.                     } catch (WriteFieldException $e) {
  115.                         $parameters->getContext()->getExceptions()->add($e);
  116.                     }
  117.                 }
  118.                 $data[$property] = $value;
  119.                 continue;
  120.             }
  121.             $field $definition->getFields()->get($property);
  122.             if ($field instanceof ChildrenAssociationField) {
  123.                 continue;
  124.             }
  125.             if ($field === null || !$field instanceof AssociationField) {
  126.                 continue;
  127.             }
  128.             try {
  129.                 $data $field->getSerializer()->normalize($field$data$parameters);
  130.             } catch (WriteFieldException $e) {
  131.                 $parameters->getContext()->getExceptions()->add($e);
  132.             }
  133.         }
  134.         $field $parameters->getDefinition()->getFields()->getChildrenAssociationField();
  135.         if ($field !== null) {
  136.             try {
  137.                 $data $field->getSerializer()->normalize($field$data$parameters);
  138.             } catch (WriteFieldException $e) {
  139.                 $parameters->getContext()->getExceptions()->add($e);
  140.             }
  141.         }
  142.         $pk = [];
  143.         foreach ($definition->getPrimaryKeys() as $pkField) {
  144.             $v $data[$pkField->getPropertyName()] ?? null;
  145.             if ($v === null) {
  146.                 $pk null;
  147.                 break;
  148.             }
  149.             $pk[$pkField->getPropertyName()] = $v;
  150.         }
  151.         // could be incomplete
  152.         if ($pk !== null) {
  153.             $parameters->getPrimaryKeyBag()->add($definition$pk);
  154.         }
  155.         return $data;
  156.     }
  157.     public function extract(array $rawDataWriteParameterBag $parameters): array
  158.     {
  159.         $definition $parameters->getDefinition();
  160.         $fields $this->getFieldsInWriteOrder($definition);
  161.         $pkData $this->getPrimaryKey($rawData$parameters);
  162.         /** @var Field&StorageAware $pkField */
  163.         foreach ($definition->getPrimaryKeys() as $pkField) {
  164.             $parameters->getContext()->set($parameters->getDefinition()->getEntityName(), $pkField->getPropertyName(), Uuid::fromBytesToHex($pkData[$pkField->getStorageName()]));
  165.         }
  166.         if ($definition instanceof MappingEntityDefinition) {
  167.             // gateway will execute always a replace into
  168.             $existence = new EntityExistence($definition->getEntityName(), [], falsefalsefalse, []);
  169.         } else {
  170.             $existence $this->entityExistenceGateway->getExistence($definition$pkData$rawData$parameters->getCommandQueue());
  171.         }
  172.         if (!$existence->exists()) {
  173.             $defaults $existence->isChild() ? $definition->getChildDefaults() : $definition->getDefaults();
  174.             $rawData $this->fillRawDataWithDefaults($definition$parameters$rawData$defaults);
  175.         }
  176.         $mainFields $this->getMainFields($fields);
  177.         // without child association
  178.         $data $this->map($mainFields$rawData$existence$parameters);
  179.         $this->updateCommandQueue($definition$parameters$existence$pkData$data);
  180.         $translation $definition->getField('translations');
  181.         if ($translation instanceof TranslationsAssociationField) {
  182.             $this->map([$translation], $rawData$existence$parameters);
  183.         }
  184.         // call map with child associations only
  185.         $children array_filter($fields, static function (Field $field) {
  186.             return $field instanceof ChildrenAssociationField;
  187.         });
  188.         if (\count($children) > 0) {
  189.             $this->map($children$rawData$existence$parameters);
  190.         }
  191.         return $pkData;
  192.     }
  193.     /**
  194.      * @param array $data
  195.      *
  196.      * @deprecated tag:v6.5.0 - parameter $data will be natively typed to type array
  197.      */
  198.     public function extractJsonUpdate($dataEntityExistence $existenceWriteParameterBag $parameters): void
  199.     {
  200.         if (!\is_array($data)) {
  201.             Feature::triggerDeprecationOrThrow(
  202.                 'v6.5.0.0',
  203.                 'The first parameter of method "WriteCommandExtractor::extractJsonUpdate()" will be typed natively to type "array" in v6.5.0.0.'
  204.             );
  205.         }
  206.         foreach ($data as $storageName => $attributes) {
  207.             $entityName $existence->getEntityName();
  208.             if (!$entityName) {
  209.                 continue;
  210.             }
  211.             $definition $this->definitionRegistry->getByEntityName($entityName);
  212.             $pks Uuid::fromHexToBytesList($existence->getPrimaryKey());
  213.             $jsonUpdateCommand = new JsonUpdateCommand(
  214.                 $definition,
  215.                 $storageName,
  216.                 $attributes,
  217.                 $pks,
  218.                 $existence,
  219.                 $parameters->getPath()
  220.             );
  221.             $parameters->getCommandQueue()->add($jsonUpdateCommand->getDefinition(), $jsonUpdateCommand);
  222.         }
  223.     }
  224.     private function normalizeTranslations(TranslationsAssociationField $translationsField, array $dataWriteParameterBag $parametersbool $hasNormalizedTranslations): array
  225.     {
  226.         if (!$hasNormalizedTranslations) {
  227.             $definition $parameters->getDefinition();
  228.             if (!$translationsField->is(Required::class)) {
  229.                 return $data;
  230.             }
  231.             $parentField $this->getParentField($definition);
  232.             if ($parentField && isset($data[$parentField->getPropertyName()])) {
  233.                 // only normalize required translations if it's not a child
  234.                 return $data;
  235.             }
  236.         }
  237.         try {
  238.             $data $translationsField->getSerializer()->normalize($translationsField$data$parameters);
  239.         } catch (WriteFieldException $e) {
  240.             $parameters->getContext()->getExceptions()->add($e);
  241.         }
  242.         return $data;
  243.     }
  244.     private function getParentField(EntityDefinition $definition): ?FkField
  245.     {
  246.         if (!$definition->isInheritanceAware()) {
  247.             return null;
  248.         }
  249.         /** @var ManyToOneAssociationField|null $parent */
  250.         $parent $definition->getFields()->get('parent');
  251.         if (!$parent) {
  252.             throw new ParentFieldNotFoundException($definition);
  253.         }
  254.         if (!$parent instanceof ManyToOneAssociationField) {
  255.             throw new InvalidParentAssociationException($definition$parent);
  256.         }
  257.         $fk $definition->getFields()->getByStorageName($parent->getStorageName());
  258.         if (!$fk) {
  259.             throw new CanNotFindParentStorageFieldException($definition);
  260.         }
  261.         if (!$fk instanceof FkField) {
  262.             throw new ParentFieldForeignKeyConstraintMissingException($definition$fk);
  263.         }
  264.         return $fk;
  265.     }
  266.     private function map(array $fields, array $rawDataEntityExistence $existenceWriteParameterBag $parameters): array
  267.     {
  268.         $stack = new DataStack($rawData);
  269.         foreach ($fields as $field) {
  270.             $kvPair $this->getKeyValuePair($field$stack$existence);
  271.             if ($kvPair === null) {
  272.                 continue;
  273.             }
  274.             try {
  275.                 if ($field->is(WriteProtected::class)) {
  276.                     $this->validateContextHasPermission($field$kvPair$parameters);
  277.                 }
  278.                 $values $field->getSerializer()->encode($field$existence$kvPair$parameters);
  279.                 foreach ($values as $fieldKey => $fieldValue) {
  280.                     $stack->update($fieldKey$fieldValue);
  281.                 }
  282.             } catch (WriteFieldException $e) {
  283.                 $parameters->getContext()->getExceptions()->add($e);
  284.             }
  285.         }
  286.         return $stack->getResultAsArray();
  287.     }
  288.     private function skipField(Field $fieldEntityExistence $existence): bool
  289.     {
  290.         if ($existence->isChild() && $field->is(Inherited::class)) {
  291.             //inherited field of a child is never required
  292.             return true;
  293.         }
  294.         $create = !$existence->exists() || $existence->childChangedToParent();
  295.         if (
  296.             (!$field instanceof UpdatedAtField && !$field instanceof CreatedByField && !$field instanceof UpdatedByField)
  297.             && (!$create || !$field->is(Required::class))
  298.         ) {
  299.             return true;
  300.         }
  301.         return false;
  302.     }
  303.     private function getKeyValuePair(Field $fieldDataStack $stackEntityExistence $existence): ?KeyValuePair
  304.     {
  305.         $kvPair $stack->pop($field->getPropertyName());
  306.         // not in data stack?
  307.         if ($kvPair !== null) {
  308.             return $kvPair;
  309.         }
  310.         if ($field instanceof ReferenceVersionField && $field->is(Required::class)) {
  311.             return new KeyValuePair($field->getPropertyName(), nulltrue);
  312.         }
  313.         if ($this->skipField($field$existence)) {
  314.             return null;
  315.         }
  316.         return new KeyValuePair($field->getPropertyName(), nulltrue);
  317.     }
  318.     private function fillRawDataWithDefaults(EntityDefinition $definitionWriteParameterBag $parameters, array $rawData, array $defaults): array
  319.     {
  320.         if ($defaults === []) {
  321.             return $rawData;
  322.         }
  323.         $toBeNormalized $rawData;
  324.         foreach ($defaults as $key => $value) {
  325.             if (\array_key_exists($key$rawData)) {
  326.                 continue;
  327.             }
  328.             $toBeNormalized[$key] = $value;
  329.         }
  330.         // clone write context so that the normalize of the default values does not affect the normal write
  331.         $parameters = new WriteParameterBag($definition, clone $parameters->getContext(), $parameters->getPath(), $parameters->getCommandQueue(), $parameters->getPrimaryKeyBag());
  332.         $normalized $this->normalizeSingle($definition$toBeNormalized$parameters);
  333.         foreach ($defaults as $key => $_) {
  334.             if (\array_key_exists($key$rawData)) {
  335.                 continue;
  336.             }
  337.             $rawData[$key] = $normalized[$key];
  338.         }
  339.         return $rawData;
  340.     }
  341.     private function updateCommandQueue(
  342.         EntityDefinition $definition,
  343.         WriteParameterBag $parameterBag,
  344.         EntityExistence $existence,
  345.         array $pkData,
  346.         array $data
  347.     ): void {
  348.         $queue $parameterBag->getCommandQueue();
  349.         if ($existence->exists()) {
  350.             $queue->add($definition, new UpdateCommand($definition$data$pkData$existence$parameterBag->getPath()));
  351.             return;
  352.         }
  353.         $queue->add($definition, new InsertCommand($definitionarray_merge($pkData$data), $pkData$existence$parameterBag->getPath()));
  354.     }
  355.     /**
  356.      * @return Field[]
  357.      */
  358.     private function getFieldsInWriteOrder(EntityDefinition $definition): array
  359.     {
  360.         $fields $definition->getFields();
  361.         $filtered = [];
  362.         foreach ($fields as $field) {
  363.             if ($field->is(Computed::class)) {
  364.                 continue;
  365.             }
  366.             $filtered[$field->getExtractPriority()][] = $field;
  367.         }
  368.         krsort($filtered, \SORT_NUMERIC);
  369.         $sorted = [];
  370.         foreach ($filtered as $fields) {
  371.             foreach ($fields as $field) {
  372.                 $sorted[] = $field;
  373.             }
  374.         }
  375.         return $sorted;
  376.     }
  377.     private function getPrimaryKey(array $rawDataWriteParameterBag $parameters): array
  378.     {
  379.         $pk = [];
  380.         $pkFields $parameters->getDefinition()->getPrimaryKeys();
  381.         /** @var StorageAware&Field $pkField */
  382.         foreach ($pkFields as $pkField) {
  383.             $id $rawData[$pkField->getPropertyName()] ?? null;
  384.             $values $pkField->getSerializer()->encode(
  385.                 $pkField,
  386.                 new EntityExistence($parameters->getDefinition()->getEntityName(), [], falsefalsefalse, []),
  387.                 new KeyValuePair($pkField->getPropertyName(), $idtrue),
  388.                 $parameters
  389.             );
  390.             foreach ($values as $key => $value) {
  391.                 $pk[$key] = $value;
  392.             }
  393.         }
  394.         return $pk;
  395.     }
  396.     /**
  397.      * Returns all fields which are relevant to extract and map the primary key data of an entity definition data array.
  398.      * In case a primary key consist of Foreign Key fields, the corresponding association for these foreign keys must be
  399.      * returned in order to guarantee the creation of these sub entities and to extract the corresponding foreign key value
  400.      * from the nested data array
  401.      *
  402.      * Example: ProductCategoryDefinition
  403.      * Primary key:   product_id, category_id
  404.      *
  405.      * Both fields are defined as foreign key field.
  406.      * It is now possible to create both related entities (product and category), providing a nested data array:
  407.      * [
  408.      *      'product' => ['id' => '..', 'name' => '..'],
  409.      *      'category' => ['id' => '..', 'name' => '..']
  410.      * ]
  411.      *
  412.      * To extract the primary key data of the ProductCategoryDefinition it is required to extract first the product
  413.      * and category association and their foreign key fields.
  414.      *
  415.      * @param Field[] $fields
  416.      *
  417.      * @return Field[]
  418.      */
  419.     private function getFieldsForPrimaryKeyMapping(array $fieldsEntityDefinition $definition): array
  420.     {
  421.         if (isset($this->fieldsForPrimaryKeyMapping[$definition->getEntityName()])) {
  422.             return $this->fieldsForPrimaryKeyMapping[$definition->getEntityName()];
  423.         }
  424.         $primaryKeys $definition->getPrimaryKeys()->getElements();
  425.         $references array_filter($fields, static function (Field $field) {
  426.             return $field instanceof ManyToOneAssociationField;
  427.         });
  428.         foreach ($primaryKeys as $primaryKey) {
  429.             if (!$primaryKey instanceof FkField) {
  430.                 continue;
  431.             }
  432.             $association $this->getAssociationByStorageName($primaryKey->getStorageName(), $references);
  433.             if ($association) {
  434.                 $primaryKeys[] = $association;
  435.             }
  436.         }
  437.         usort($primaryKeys, static function (Field $aField $b) {
  438.             return $b->getExtractPriority() <=> $a->getExtractPriority();
  439.         });
  440.         return $this->fieldsForPrimaryKeyMapping[$definition->getEntityName()] = $primaryKeys;
  441.     }
  442.     private function getAssociationByStorageName(string $name, array $fields): ?ManyToOneAssociationField
  443.     {
  444.         /** @var ManyToOneAssociationField $association */
  445.         foreach ($fields as $association) {
  446.             if ($association->getStorageName() !== $name) {
  447.                 continue;
  448.             }
  449.             return $association;
  450.         }
  451.         return null;
  452.     }
  453.     /**
  454.      * @param Field[] $fields
  455.      *
  456.      * @return Field[]
  457.      */
  458.     private function getMainFields(array $fields): array
  459.     {
  460.         $main = [];
  461.         foreach ($fields as $field) {
  462.             if ($field instanceof ChildrenAssociationField) {
  463.                 continue;
  464.             }
  465.             if ($field instanceof TranslationsAssociationField) {
  466.                 continue;
  467.             }
  468.             if ($field->is(Runtime::class)) {
  469.                 continue;
  470.             }
  471.             if (!$field->is(PrimaryKey::class)) {
  472.                 $main[] = $field;
  473.                 continue;
  474.             }
  475.             if ($field instanceof FkField) {
  476.                 $main[] = $field;
  477.             }
  478.         }
  479.         return $main;
  480.     }
  481.     private function validateContextHasPermission(Field $fieldKeyValuePair $dataWriteParameterBag $parameters): void
  482.     {
  483.         /** @var WriteProtected $flag */
  484.         $flag $field->getFlag(WriteProtected::class);
  485.         if ($flag->isAllowed($parameters->getContext()->getContext()->getScope())) {
  486.             return;
  487.         }
  488.         $message 'This field is write-protected.';
  489.         $allowedOrigins '';
  490.         if ($flag->getAllowedScopes()) {
  491.             $message .= ' (Got: "%s" scope and "%s" is required)';
  492.             $allowedOrigins implode(' or '$flag->getAllowedScopes());
  493.         }
  494.         $violationList = new ConstraintViolationList();
  495.         $violationList->add(
  496.             new ConstraintViolation(
  497.                 sprintf(
  498.                     $message,
  499.                     $parameters->getContext()->getContext()->getScope(),
  500.                     $allowedOrigins
  501.                 ),
  502.                 $message,
  503.                 [
  504.                     $parameters->getContext()->getContext()->getScope(),
  505.                     $allowedOrigins,
  506.                 ],
  507.                 $data->getValue(),
  508.                 $data->getKey(),
  509.                 $data->getValue()
  510.             )
  511.         );
  512.         $parameters->getContext()->getExceptions()->add(
  513.             new WriteConstraintViolationException($violationList$parameters->getPath() . '/' $data->getKey())
  514.         );
  515.     }
  516. }