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

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 $collection, string $entityClass, EntityDefinition $definition, array $rows, string $root, Context $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 $row, string $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 $primaryKey, Context $context): array
  115. {
  116. $fields = $definition->getPrimaryKeys();
  117. $mapped = [];
  118. $existence = new EntityExistence($definition->getEntityName(), [], true, false, false, []);
  119. $params = new WriteParameterBag($definition, WriteContext::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(), $value, true);
  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 $definition, Entity $entity, string $root, array $row, Context $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 $definition, Entity $entity, string $root, array $row, Context $context, iterable $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($property, null);
  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 $row, string $root, Entity $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 $definition, Entity $entity, array $row, string $root, Context $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($typed, self::value($row, $root, $field)));
  252. $entity->$field = $typed->getSerializer()->decode($typed, self::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 $row, string $root, ?Field $field, Context $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, $context, self::$partial[$field->getPropertyName()] ?? []);
  291. }
  292. /**
  293. * @param array<mixed> $row
  294. */
  295. protected function customFields(EntityDefinition $definition, array $row, string $root, Entity $entity, ?Field $field, Context $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($customField, self::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($values, false));
  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 $row, string $root, string $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($string, true);
  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 $definition, string $entityClass, array $row, string $root, Context $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. }