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

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