<?php declare(strict_types=1);namespace Shopware\Core\Framework\DataAbstractionLayer\Dbal;use Shopware\Core\Framework\Context;use Shopware\Core\Framework\DataAbstractionLayer\Entity;use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;use Shopware\Core\Framework\DataAbstractionLayer\Field\CustomFields;use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Inherited;use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;use Shopware\Core\Framework\DataAbstractionLayer\Field\ParentAssociationField;use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;use Shopware\Core\Framework\DataAbstractionLayer\Field\VersionField;use Shopware\Core\Framework\DataAbstractionLayer\PartialEntity;use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\WriteCommandQueue;use Shopware\Core\Framework\DataAbstractionLayer\Write\DataStack\KeyValuePair;use Shopware\Core\Framework\DataAbstractionLayer\Write\EntityExistence;use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteContext;use Shopware\Core\Framework\DataAbstractionLayer\Write\WriteParameterBag;use Shopware\Core\Framework\Log\Package;use Shopware\Core\Framework\Struct\ArrayEntity;use Shopware\Core\Framework\Struct\ArrayStruct;use Symfony\Component\DependencyInjection\ContainerInterface;/** * Allows to hydrate database values into struct objects. * * @deprecated tag:v6.5.0 - reason:becomes-internal - Will be internal */#[Package('core')]class EntityHydrator{ /** * @var array<mixed> */ protected static array $partial = []; /** * @var array<mixed> */ private static array $hydrated = []; /** * @var array<string> */ private static array $manyToOne = []; /** * @var array<string, array<string, Field>> */ private static array $translatedFields = []; private ContainerInterface $container; /** * @internal */ public function __construct(ContainerInterface $container) { $this->container = $container; } /** * @param EntityCollection<Entity> $collection * @param array<mixed> $rows * @param array<string|array<string>> $partial * * @return EntityCollection<Entity> */ public function hydrate(EntityCollection $collection, string $entityClass, EntityDefinition $definition, array $rows, string $root, Context $context, array $partial = []): EntityCollection { self::$hydrated = []; self::$partial = $partial; if (!empty(self::$partial)) { $collection = new EntityCollection(); } foreach ($rows as $row) { $collection->add($this->hydrateEntity($definition, $entityClass, $row, $root, $context, $partial)); } return $collection; } /** * @template EntityClass * * @param class-string<EntityClass> $class * * @return EntityClass */ final public static function createClass(string $class) { return new $class(); } /** * @param array<mixed> $row * * @return array<mixed> */ final public static function buildUniqueIdentifier(EntityDefinition $definition, array $row, string $root): array { $primaryKeyFields = $definition->getPrimaryKeys(); $primaryKey = []; foreach ($primaryKeyFields as $field) { if ($field instanceof VersionField || $field instanceof ReferenceVersionField) { continue; } $accessor = $root . '.' . $field->getPropertyName(); $primaryKey[$field->getPropertyName()] = $field->getSerializer()->decode($field, $row[$accessor]); } return $primaryKey; } /** * @param array<string> $primaryKey * * @return array<string> */ final public static function encodePrimaryKey(EntityDefinition $definition, array $primaryKey, Context $context): array { $fields = $definition->getPrimaryKeys(); $mapped = []; $existence = new EntityExistence($definition->getEntityName(), [], true, false, false, []); $params = new WriteParameterBag($definition, WriteContext::createFromContext($context), '', new WriteCommandQueue()); foreach ($fields as $field) { if ($field instanceof VersionField || $field instanceof ReferenceVersionField) { $value = $context->getVersionId(); } else { $value = $primaryKey[$field->getPropertyName()]; } $kvPair = new KeyValuePair($field->getPropertyName(), $value, true); $encoded = $field->getSerializer()->encode($field, $existence, $kvPair, $params); foreach ($encoded as $key => $value) { $mapped[$key] = $value; } } return $mapped; } /** * Allows simple overwrite for specialized entity hydrators * * @param array<mixed> $row */ protected function assign(EntityDefinition $definition, Entity $entity, string $root, array $row, Context $context): Entity { $entity = $this->hydrateFields($definition, $entity, $root, $row, $context, $definition->getFields()); return $entity; } /** * @param array<mixed> $row * @param iterable<Field> $fields */ protected function hydrateFields(EntityDefinition $definition, Entity $entity, string $root, array $row, Context $context, iterable $fields): Entity { /** @var ArrayStruct<string, mixed> $foreignKeys */ $foreignKeys = $entity->getExtension(EntityReader::FOREIGN_KEYS); $isPartial = self::$partial !== []; foreach ($fields as $field) { $property = $field->getPropertyName(); if ($isPartial && !isset(self::$partial[$property])) { continue; } $key = $root . '.' . $property; // initialize not loaded associations with null if ($field instanceof AssociationField && $entity instanceof ArrayEntity) { $entity->set($property, null); } if ($field instanceof ParentAssociationField) { continue; } if ($field instanceof ManyToManyAssociationField) { $this->manyToMany($row, $root, $entity, $field); continue; } if ($field instanceof ManyToOneAssociationField || $field instanceof OneToOneAssociationField) { $association = $this->manyToOne($row, $root, $field, $context); if ($association === null && $entity instanceof PartialEntity) { continue; } if ($field->is(Extension::class)) { if ($association) { $entity->addExtension($property, $association); } } else { $entity->assign([$property => $association]); } continue; } //other association fields are not handled in entity reader query if ($field instanceof AssociationField) { continue; } if (!\array_key_exists($key, $row)) { continue; } $value = $row[$key]; $typed = $field; if ($field instanceof TranslatedField) { $typed = EntityDefinitionQueryHelper::getTranslatedField($definition, $field); } if ($typed instanceof CustomFields) { $this->customFields($definition, $row, $root, $entity, $field, $context); continue; } if ($field instanceof TranslatedField) { // contains the resolved translation chain value $decoded = $typed->getSerializer()->decode($typed, $value); $entity->addTranslated($property, $decoded); $inherited = $definition->isInheritanceAware() && $context->considerInheritance(); $chain = EntityDefinitionQueryHelper::buildTranslationChain($root, $context, $inherited); // assign translated value of the first language $key = array_shift($chain) . '.' . $property; $decoded = $typed->getSerializer()->decode($typed, $row[$key]); $entity->assign([$property => $decoded]); continue; } $decoded = $definition->decode($property, $value); if ($field->is(Extension::class)) { $foreignKeys->set($property, $decoded); } else { $entity->assign([$property => $decoded]); } } return $entity; } /** * @param array<mixed> $row */ protected function manyToMany(array $row, string $root, Entity $entity, ?Field $field): void { if ($field === null) { throw new \RuntimeException('No field provided'); } $accessor = $root . '.' . $field->getPropertyName() . '.id_mapping'; //many to many isn't loaded in case of limited association criterias if (!\array_key_exists($accessor, $row)) { return; } //explode hexed ids $ids = explode('||', (string) $row[$accessor]); $ids = array_map('strtolower', array_filter($ids)); /** @var ArrayStruct<string, mixed> $mapping */ $mapping = $entity->getExtension(EntityReader::INTERNAL_MAPPING_STORAGE); $mapping->set($field->getPropertyName(), $ids); } /** * @param array<mixed> $row * @param array<string, Field> $fields */ protected function translate(EntityDefinition $definition, Entity $entity, array $row, string $root, Context $context, array $fields): void { $inherited = $definition->isInheritanceAware() && $context->considerInheritance(); $chain = EntityDefinitionQueryHelper::buildTranslationChain($root, $context, $inherited); $translatedFields = $this->getTranslatedFields($definition, $fields); foreach ($translatedFields as $field => $typed) { $entity->addTranslated($field, $typed->getSerializer()->decode($typed, self::value($row, $root, $field))); $entity->$field = $typed->getSerializer()->decode($typed, self::value($row, $chain[0], $field)); } } /** * @param array<Field> $fields * * @return array<string, Field> */ protected function getTranslatedFields(EntityDefinition $definition, array $fields): array { $key = $definition->getEntityName(); if (isset(self::$translatedFields[$key])) { return self::$translatedFields[$key]; } $translatedFields = []; /** @var TranslatedField $field */ foreach ($fields as $field) { $translatedFields[$field->getPropertyName()] = EntityDefinitionQueryHelper::getTranslatedField($definition, $field); } return self::$translatedFields[$key] = $translatedFields; } /** * @param array<mixed> $row */ protected function manyToOne(array $row, string $root, ?Field $field, Context $context): ?Entity { if ($field === null) { throw new \RuntimeException('No field provided'); } if (!$field instanceof AssociationField) { throw new \RuntimeException(sprintf('Provided field %s is no association field', $field->getPropertyName())); } $pk = $this->getManyToOneProperty($field); $association = $root . '.' . $field->getPropertyName(); $key = $association . '.' . $pk; if (!isset($row[$key])) { return null; } return $this->hydrateEntity($field->getReferenceDefinition(), $field->getReferenceDefinition()->getEntityClass(), $row, $association, $context, self::$partial[$field->getPropertyName()] ?? []); } /** * @param array<mixed> $row */ protected function customFields(EntityDefinition $definition, array $row, string $root, Entity $entity, ?Field $field, Context $context): void { if ($field === null) { return; } $inherited = $field->is(Inherited::class) && $context->considerInheritance(); $propertyName = $field->getPropertyName(); $value = self::value($row, $root, $propertyName); if ($field instanceof TranslatedField) { $customField = EntityDefinitionQueryHelper::getTranslatedField($definition, $field); $chain = EntityDefinitionQueryHelper::buildTranslationChain($root, $context, $inherited); $decoded = $customField->getSerializer()->decode($customField, self::value($row, $chain[0], $propertyName)); $entity->assign([$propertyName => $decoded]); $values = []; foreach ($chain as $accessor) { $key = $accessor . '.' . $propertyName; $values[] = $row[$key] ?? null; } if (empty($values)) { return; } /** * `array_merge`s ordering is reversed compared to the translations array. * In other terms: The first argument has the lowest 'priority', so we need to reverse the array */ $merged = $this->mergeJson(array_reverse($values, false)); $decoded = $customField->getSerializer()->decode($customField, $merged); $entity->addTranslated($propertyName, $decoded); if ($inherited) { $entity->assign([$propertyName => $decoded]); } return; } // field is not inherited or request should work with raw data? decode child attributes and return if (!$inherited) { $value = $field->getSerializer()->decode($field, $value); $entity->assign([$propertyName => $value]); return; } $parentKey = $root . '.' . $propertyName . '.inherited'; // parent has no attributes? decode only child attributes and return if (!isset($row[$parentKey])) { $value = $field->getSerializer()->decode($field, $value); $entity->assign([$propertyName => $value]); return; } // merge child attributes with parent attributes and assign $mergedJson = $this->mergeJson([$row[$parentKey], $value]); $merged = $field->getSerializer()->decode($field, $mergedJson); $entity->assign([$propertyName => $merged]); } /** * @param array<mixed> $row */ protected static function value(array $row, string $root, string $property): ?string { $accessor = $root . '.' . $property; return $row[$accessor] ?? null; } protected function getManyToOneProperty(AssociationField $field): string { $key = $field->getReferenceDefinition()->getEntityName() . '.' . $field->getReferenceField(); if (isset(self::$manyToOne[$key])) { return self::$manyToOne[$key]; } $reference = $field->getReferenceDefinition()->getFields()->getByStorageName( $field->getReferenceField() ); if ($reference === null) { throw new \RuntimeException(sprintf( 'Can not find field by storage name %s in definition %s', $field->getReferenceField(), $field->getReferenceDefinition()->getEntityName() )); } return self::$manyToOne[$key] = $reference->getPropertyName(); } /** * @param array<string|null> $jsonStrings */ protected function mergeJson(array $jsonStrings): string { $merged = []; foreach ($jsonStrings as $string) { if ($string === null) { continue; } $decoded = json_decode($string, true); if (!$decoded) { continue; } foreach ($decoded as $key => $value) { if ($value === null) { continue; } $merged[$key] = $value; } } return json_encode($merged, \JSON_PRESERVE_ZERO_FRACTION | \JSON_THROW_ON_ERROR); } /** * @param array<mixed> $row * @param array<string|array<string>> $partial */ private function hydrateEntity(EntityDefinition $definition, string $entityClass, array $row, string $root, Context $context, array $partial = []): Entity { $isPartial = $partial !== []; $hydratorClass = $definition->getHydratorClass(); $entityClass = $isPartial ? PartialEntity::class : $entityClass; if ($isPartial) { $hydratorClass = EntityHydrator::class; } $hydrator = $this->container->get($hydratorClass); if (!$hydrator instanceof self) { throw new \RuntimeException(sprintf('Hydrator for entity %s not registered', $definition->getEntityName())); } $identifier = implode('-', self::buildUniqueIdentifier($definition, $row, $root)); $cacheKey = $root . '::' . $identifier; if (isset(self::$hydrated[$cacheKey])) { return self::$hydrated[$cacheKey]; } $entity = new $entityClass(); if (!$entity instanceof Entity) { throw new \RuntimeException(sprintf('Expected instance of Entity.php, got %s', \get_class($entity))); } $entity->addExtension(EntityReader::FOREIGN_KEYS, new ArrayStruct()); $entity->addExtension(EntityReader::INTERNAL_MAPPING_STORAGE, new ArrayStruct()); $entity->setUniqueIdentifier($identifier); $entity->internalSetEntityData($definition->getEntityName(), $definition->getFieldVisibility()); $entity = $hydrator->assign($definition, $entity, $root, $row, $context); return self::$hydrated[$cacheKey] = $entity; }}