src/Core/Framework/DataAbstractionLayer/Dbal/EntityHydrator.php line 363

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