vendor/shopware/core/Framework/DataAbstractionLayer/Dbal/EntityHydrator.php line 167

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\DataAbstractionLayer\Dbal;
  3. use Shopware\Core\Framework\Context;
  4. use Shopware\Core\Framework\DataAbstractionLayer\Entity;
  5. use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
  6. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Field\CustomFields;
  9. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Inherited;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Field\ParentAssociationField;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Field\VersionField;
  19. use Shopware\Core\Framework\DataAbstractionLayer\PartialEntity;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommandQueue;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Write\DataStack\KeyValuePair;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityExistence;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteContext;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteParameterBag;
  25. use Shopware\Core\Framework\Log\Package;
  26. use Shopware\Core\Framework\Struct\ArrayEntity;
  27. use Shopware\Core\Framework\Struct\ArrayStruct;
  28. use Symfony\Component\DependencyInjection\ContainerInterface;
  29. /**
  30.  * Allows to hydrate database values into struct objects.
  31.  *
  32.  * @internal
  33.  */
  34. #[Package('core')]
  35. class EntityHydrator
  36. {
  37.     /**
  38.      * @var array<mixed>
  39.      */
  40.     protected static array $partial = [];
  41.     /**
  42.      * @var array<bool>
  43.      */
  44.     protected static array $partialFullPaths = [];
  45.     /**
  46.      * @var array<mixed>
  47.      */
  48.     private static array $hydrated = [];
  49.     /**
  50.      * @var array<string>
  51.      */
  52.     private static array $manyToOne = [];
  53.     /**
  54.      * @var array<string, array<string, Field>>
  55.      */
  56.     private static array $translatedFields = [];
  57.     /**
  58.      * @internal
  59.      */
  60.     public function __construct(private readonly ContainerInterface $container)
  61.     {
  62.     }
  63.     /**
  64.      * @template TEntityCollection of EntityCollection<Entity>
  65.      *
  66.      * @param TEntityCollection $collection
  67.      * @param array<mixed> $rows
  68.      * @param array<string|array<string>> $partial
  69.      *
  70.      * @return TEntityCollection
  71.      */
  72.     public function hydrate(EntityCollection $collectionstring $entityClassEntityDefinition $definition, array $rowsstring $rootContext $context, array $partial = []): EntityCollection
  73.     {
  74.         self::$hydrated = [];
  75.         self::$partial $partial;
  76.         self::$partialFullPaths = [];
  77.         if (!empty(self::$partial)) {
  78.             /** @var TEntityCollection $collection */
  79.             $collection = new EntityCollection();
  80.             $this->mapPartialFieldsToHydrate(self::$partial$root);
  81.         }
  82.         foreach ($rows as $row) {
  83.             $collection->add($this->hydrateEntity($definition$entityClass$row$root$context));
  84.         }
  85.         return $collection;
  86.     }
  87.     /**
  88.      * @template EntityClass
  89.      *
  90.      * @param class-string<EntityClass> $class
  91.      *
  92.      * @return EntityClass
  93.      */
  94.     final public static function createClass(string $class)
  95.     {
  96.         return new $class();
  97.     }
  98.     /**
  99.      * @param array<mixed> $row
  100.      *
  101.      * @return array<mixed>
  102.      */
  103.     final public static function buildUniqueIdentifier(EntityDefinition $definition, array $rowstring $root): array
  104.     {
  105.         $primaryKeyFields $definition->getPrimaryKeys();
  106.         $primaryKey = [];
  107.         foreach ($primaryKeyFields as $field) {
  108.             if ($field instanceof VersionField || $field instanceof ReferenceVersionField) {
  109.                 continue;
  110.             }
  111.             $accessor $root '.' $field->getPropertyName();
  112.             $primaryKey[$field->getPropertyName()] = $field->getSerializer()->decode($field$row[$accessor]);
  113.         }
  114.         return $primaryKey;
  115.     }
  116.     /**
  117.      * @param array<string> $primaryKey
  118.      *
  119.      * @return array<string>
  120.      */
  121.     final public static function encodePrimaryKey(EntityDefinition $definition, array $primaryKeyContext $context): array
  122.     {
  123.         $fields $definition->getPrimaryKeys();
  124.         $mapped = [];
  125.         $existence = new EntityExistence($definition->getEntityName(), [], truefalsefalse, []);
  126.         $params = new WriteParameterBag($definitionWriteContext::createFromContext($context), '', new WriteCommandQueue());
  127.         foreach ($fields as $field) {
  128.             if ($field instanceof VersionField || $field instanceof ReferenceVersionField) {
  129.                 $value $context->getVersionId();
  130.             } else {
  131.                 $value $primaryKey[$field->getPropertyName()];
  132.             }
  133.             $kvPair = new KeyValuePair($field->getPropertyName(), $valuetrue);
  134.             $encoded $field->getSerializer()->encode($field$existence$kvPair$params);
  135.             foreach ($encoded as $key => $value) {
  136.                 $mapped[$key] = $value;
  137.             }
  138.         }
  139.         return $mapped;
  140.     }
  141.     /**
  142.      * Allows simple overwrite for specialized entity hydrators
  143.      *
  144.      * @param array<mixed> $row
  145.      */
  146.     protected function assign(EntityDefinition $definitionEntity $entitystring $root, array $rowContext $context): Entity
  147.     {
  148.         $entity $this->hydrateFields($definition$entity$root$row$context$definition->getFields());
  149.         return $entity;
  150.     }
  151.     /**
  152.      * @param array<mixed> $row
  153.      * @param iterable<Field> $fields
  154.      */
  155.     protected function hydrateFields(EntityDefinition $definitionEntity $entitystring $root, array $rowContext $contextiterable $fields): Entity
  156.     {
  157.         /** @var ArrayStruct<string, mixed> $foreignKeys */
  158.         $foreignKeys $entity->getExtension(EntityReader::FOREIGN_KEYS);
  159.         $isPartial self::$partial !== [];
  160.         foreach ($fields as $field) {
  161.             $property $field->getPropertyName();
  162.             $key $root '.' $property;
  163.             if ($isPartial && !isset(self::$partialFullPaths[$key])) {
  164.                 continue;
  165.             }
  166.             // initialize not loaded associations with null
  167.             if ($field instanceof AssociationField && $entity instanceof ArrayEntity) {
  168.                 $entity->set($propertynull);
  169.             }
  170.             if ($field instanceof ParentAssociationField) {
  171.                 continue;
  172.             }
  173.             if ($field instanceof ManyToManyAssociationField) {
  174.                 $this->manyToMany($row$root$entity$field);
  175.                 continue;
  176.             }
  177.             if ($field instanceof ManyToOneAssociationField || $field instanceof OneToOneAssociationField) {
  178.                 $association $this->manyToOne($row$root$field$context);
  179.                 if ($association === null && $entity instanceof PartialEntity) {
  180.                     continue;
  181.                 }
  182.                 if ($field->is(Extension::class)) {
  183.                     if ($association) {
  184.                         $entity->addExtension($property$association);
  185.                     }
  186.                 } else {
  187.                     $entity->assign([$property => $association]);
  188.                 }
  189.                 continue;
  190.             }
  191.             // other association fields are not handled in entity reader query
  192.             if ($field instanceof AssociationField) {
  193.                 continue;
  194.             }
  195.             if (!\array_key_exists($key$row)) {
  196.                 continue;
  197.             }
  198.             $value $row[$key];
  199.             $typed $field;
  200.             if ($field instanceof TranslatedField) {
  201.                 $typed EntityDefinitionQueryHelper::getTranslatedField($definition$field);
  202.             }
  203.             if ($typed instanceof CustomFields) {
  204.                 $this->customFields($definition$row$root$entity$field$context);
  205.                 continue;
  206.             }
  207.             if ($field instanceof TranslatedField) {
  208.                 // contains the resolved translation chain value
  209.                 $decoded $typed->getSerializer()->decode($typed$value);
  210.                 $entity->addTranslated($property$decoded);
  211.                 $inherited $definition->isInheritanceAware() && $context->considerInheritance();
  212.                 $chain EntityDefinitionQueryHelper::buildTranslationChain($root$context$inherited);
  213.                 // assign translated value of the first language
  214.                 $key array_shift($chain) . '.' $property;
  215.                 $decoded $typed->getSerializer()->decode($typed$row[$key]);
  216.                 $entity->assign([$property => $decoded]);
  217.                 continue;
  218.             }
  219.             $decoded $definition->decode($property$value);
  220.             if ($field->is(Extension::class)) {
  221.                 $foreignKeys->set($property$decoded);
  222.             } else {
  223.                 $entity->assign([$property => $decoded]);
  224.             }
  225.         }
  226.         return $entity;
  227.     }
  228.     /**
  229.      * @param array<mixed> $row
  230.      */
  231.     protected function manyToMany(array $rowstring $rootEntity $entity, ?Field $field): void
  232.     {
  233.         if ($field === null) {
  234.             throw new \RuntimeException('No field provided');
  235.         }
  236.         $accessor $root '.' $field->getPropertyName() . '.id_mapping';
  237.         // many to many isn't loaded in case of limited association criterias
  238.         if (!\array_key_exists($accessor$row)) {
  239.             return;
  240.         }
  241.         // explode hexed ids
  242.         $ids explode('||', (string) $row[$accessor]);
  243.         $ids array_map('strtolower'array_filter($ids));
  244.         /** @var ArrayStruct<string, mixed> $mapping */
  245.         $mapping $entity->getExtension(EntityReader::INTERNAL_MAPPING_STORAGE);
  246.         $mapping->set($field->getPropertyName(), $ids);
  247.     }
  248.     /**
  249.      * @param array<mixed> $row
  250.      * @param array<string, Field> $fields
  251.      */
  252.     protected function translate(EntityDefinition $definitionEntity $entity, array $rowstring $rootContext $context, array $fields): void
  253.     {
  254.         $inherited $definition->isInheritanceAware() && $context->considerInheritance();
  255.         $chain EntityDefinitionQueryHelper::buildTranslationChain($root$context$inherited);
  256.         $translatedFields $this->getTranslatedFields($definition$fields);
  257.         foreach ($translatedFields as $field => $typed) {
  258.             $fieldValue self::value($row$root$field);
  259.             $translation $fieldValue !== null $typed->getSerializer()->decode($typed$fieldValue) : null;
  260.             $entity->addTranslated($field$translation);
  261.             $chainFieldValue self::value($row$chain[0], $field);
  262.             $entity->$field $chainFieldValue !== null ? ($fieldValue === $chainFieldValue $translation $typed->getSerializer()->decode($typed$chainFieldValue)) : null;
  263.         }
  264.     }
  265.     /**
  266.      * @param array<Field> $fields
  267.      *
  268.      * @return array<string, Field>
  269.      */
  270.     protected function getTranslatedFields(EntityDefinition $definition, array $fields): array
  271.     {
  272.         $key $definition->getEntityName();
  273.         if (isset(self::$translatedFields[$key])) {
  274.             return self::$translatedFields[$key];
  275.         }
  276.         $translatedFields = [];
  277.         /** @var TranslatedField $field */
  278.         foreach ($fields as $field) {
  279.             $translatedFields[$field->getPropertyName()] = EntityDefinitionQueryHelper::getTranslatedField($definition$field);
  280.         }
  281.         return self::$translatedFields[$key] = $translatedFields;
  282.     }
  283.     /**
  284.      * @param array<mixed> $row
  285.      */
  286.     protected function manyToOne(array $rowstring $root, ?Field $fieldContext $context): ?Entity
  287.     {
  288.         if ($field === null) {
  289.             throw new \RuntimeException('No field provided');
  290.         }
  291.         if (!$field instanceof AssociationField) {
  292.             throw new \RuntimeException(\sprintf('Provided field %s is no association field'$field->getPropertyName()));
  293.         }
  294.         $pk $this->getManyToOneProperty($field);
  295.         $association $root '.' $field->getPropertyName();
  296.         $key $association '.' $pk;
  297.         if (!isset($row[$key])) {
  298.             return null;
  299.         }
  300.         if (self::$partial !== [] && !isset(self::$partialFullPaths[$pk])) {
  301.             self::$partialFullPaths[$key] = true;
  302.         }
  303.         return $this->hydrateEntity($field->getReferenceDefinition(), $field->getReferenceDefinition()->getEntityClass(), $row$association$context);
  304.     }
  305.     /**
  306.      * @param array<mixed> $row
  307.      */
  308.     protected function customFields(EntityDefinition $definition, array $rowstring $rootEntity $entity, ?Field $fieldContext $context): void
  309.     {
  310.         if ($field === null) {
  311.             return;
  312.         }
  313.         $inherited $field->is(Inherited::class) && $context->considerInheritance();
  314.         $propertyName $field->getPropertyName();
  315.         $value self::value($row$root$propertyName);
  316.         if ($field instanceof TranslatedField) {
  317.             $customField EntityDefinitionQueryHelper::getTranslatedField($definition$field);
  318.             $chain EntityDefinitionQueryHelper::buildTranslationChain($root$context$inherited);
  319.             $decoded $customField->getSerializer()->decode($customFieldself::value($row$chain[0], $propertyName));
  320.             $entity->assign([$propertyName => $decoded]);
  321.             $values = [];
  322.             foreach ($chain as $accessor) {
  323.                 $values[] = self::value($row$accessor$propertyName);
  324.             }
  325.             if (empty($values)) {
  326.                 return;
  327.             }
  328.             /**
  329.              * `array_merge`s ordering is reversed compared to the translations array.
  330.              * In other terms: The first argument has the lowest 'priority', so we need to reverse the array
  331.              */
  332.             $merged $this->mergeJson(array_reverse($valuesfalse));
  333.             $decoded $customField->getSerializer()->decode($customField$merged);
  334.             $entity->addTranslated($propertyName$decoded);
  335.             if ($inherited) {
  336.                 /*
  337.                  * The translations chains array has the structure: [
  338.                  *      main language,
  339.                  *      parent with main language,
  340.                  *      fallback language,
  341.                  *      parent with fallback language,
  342.                  * ]
  343.                  *
  344.                  * We need to join the first two to get the inherited field value of the main translation
  345.                  */
  346.                 $values = [
  347.                     self::value($row$chain[1], $propertyName),
  348.                     self::value($row$chain[0], $propertyName),
  349.                 ];
  350.                 $merged $this->mergeJson($values);
  351.                 $decoded $customField->getSerializer()->decode($customField$merged);
  352.                 $entity->assign([$propertyName => $decoded]);
  353.             }
  354.             return;
  355.         }
  356.         // field is not inherited or request should work with raw data? decode child attributes and return
  357.         if (!$inherited) {
  358.             $value $field->getSerializer()->decode($field$value);
  359.             $entity->assign([$propertyName => $value]);
  360.             return;
  361.         }
  362.         $parentKey $root '.' $propertyName '.inherited';
  363.         // parent has no attributes? decode only child attributes and return
  364.         if (!isset($row[$parentKey])) {
  365.             $value $field->getSerializer()->decode($field$value);
  366.             $entity->assign([$propertyName => $value]);
  367.             return;
  368.         }
  369.         // merge child attributes with parent attributes and assign
  370.         $mergedJson $this->mergeJson([$row[$parentKey], $value]);
  371.         $merged $field->getSerializer()->decode($field$mergedJson);
  372.         $entity->assign([$propertyName => $merged]);
  373.     }
  374.     /**
  375.      * @param array<mixed> $row
  376.      */
  377.     protected static function value(array $rowstring $rootstring $property): ?string
  378.     {
  379.         $accessor $root '.' $property;
  380.         return $row[$accessor] ?? null;
  381.     }
  382.     protected function getManyToOneProperty(AssociationField $field): string
  383.     {
  384.         $key $field->getReferenceDefinition()->getEntityName() . '.' $field->getReferenceField();
  385.         if (isset(self::$manyToOne[$key])) {
  386.             return self::$manyToOne[$key];
  387.         }
  388.         $reference $field->getReferenceDefinition()->getFields()->getByStorageName(
  389.             $field->getReferenceField()
  390.         );
  391.         if ($reference === null) {
  392.             throw new \RuntimeException(\sprintf(
  393.                 'Can not find field by storage name %s in definition %s',
  394.                 $field->getReferenceField(),
  395.                 $field->getReferenceDefinition()->getEntityName()
  396.             ));
  397.         }
  398.         return self::$manyToOne[$key] = $reference->getPropertyName();
  399.     }
  400.     /**
  401.      * @param array<string|null> $jsonStrings
  402.      */
  403.     protected function mergeJson(array $jsonStrings): string
  404.     {
  405.         $merged = [];
  406.         foreach ($jsonStrings as $string) {
  407.             if ($string === null) {
  408.                 continue;
  409.             }
  410.             $decoded json_decode($stringtrue512\JSON_THROW_ON_ERROR);
  411.             if (!$decoded) {
  412.                 continue;
  413.             }
  414.             foreach ($decoded as $key => $value) {
  415.                 if ($value === null) {
  416.                     continue;
  417.                 }
  418.                 $merged[$key] = $value;
  419.             }
  420.         }
  421.         return json_encode($merged\JSON_PRESERVE_ZERO_FRACTION \JSON_THROW_ON_ERROR);
  422.     }
  423.     /**
  424.      * @param array<string, mixed> $fields
  425.      */
  426.     private function mapPartialFieldsToHydrate(array $fieldsstring $currentPath): void
  427.     {
  428.         foreach ($fields as $field => $values) {
  429.             self::$partialFullPaths[$currentPath '.' $field] = true;
  430.             $this->mapPartialFieldsToHydrate($values$currentPath '.' $field);
  431.         }
  432.     }
  433.     /**
  434.      * @param array<mixed> $row
  435.      */
  436.     private function hydrateEntity(EntityDefinition $definitionstring $entityClass, array $rowstring $rootContext $context): Entity
  437.     {
  438.         $isPartial self::$partial !== [];
  439.         $hydratorClass $definition->getHydratorClass();
  440.         $entityClass $isPartial PartialEntity::class : $entityClass;
  441.         if ($isPartial) {
  442.             $hydratorClass EntityHydrator::class;
  443.         }
  444.         $hydrator $this->container->get($hydratorClass);
  445.         if (!$hydrator instanceof self) {
  446.             throw new \RuntimeException(\sprintf('Hydrator for entity %s not registered'$definition->getEntityName()));
  447.         }
  448.         $identifier implode('-'self::buildUniqueIdentifier($definition$row$root));
  449.         $cacheKey $root '::' $identifier;
  450.         if (isset(self::$hydrated[$cacheKey])) {
  451.             return self::$hydrated[$cacheKey];
  452.         }
  453.         $entity = new $entityClass();
  454.         if (!$entity instanceof Entity) {
  455.             throw new \RuntimeException(\sprintf('Expected instance of Entity.php, got %s'$entity::class));
  456.         }
  457.         $entity->addExtension(EntityReader::FOREIGN_KEYS, new ArrayStruct([], $definition->getEntityName() . '_foreign_keys_extension'));
  458.         $entity->addExtension(EntityReader::INTERNAL_MAPPING_STORAGE, new ArrayStruct());
  459.         $entity->setUniqueIdentifier($identifier);
  460.         $entity->internalSetEntityData($definition->getEntityName(), $definition->getFieldVisibility());
  461.         $entity $hydrator->assign($definition$entity$root$row$context);
  462.         return self::$hydrated[$cacheKey] = $entity;
  463.     }
  464. }