AbstractHydrator.php
9 months ago
ArrayHydrator.php
9 months ago
HydrationException.php
9 months ago
IterableResult.php
9 months ago
ObjectHydrator.php
9 months ago
ScalarColumnHydrator.php
9 months ago
ScalarHydrator.php
9 months ago
SimpleObjectHydrator.php
9 months ago
SingleScalarHydrator.php
9 months ago
index.php
9 months ago
ObjectHydrator.php
398 lines
| 1 | <?php |
| 2 | declare (strict_types=1); |
| 3 | namespace MailPoetVendor\Doctrine\ORM\Internal\Hydration; |
| 4 | if (!defined('ABSPATH')) exit; |
| 5 | use BackedEnum; |
| 6 | use MailPoetVendor\Doctrine\Common\Collections\ArrayCollection; |
| 7 | use MailPoetVendor\Doctrine\ORM\Mapping\ClassMetadata; |
| 8 | use MailPoetVendor\Doctrine\ORM\PersistentCollection; |
| 9 | use MailPoetVendor\Doctrine\ORM\Query; |
| 10 | use MailPoetVendor\Doctrine\ORM\UnitOfWork; |
| 11 | use function array_fill_keys; |
| 12 | use function array_keys; |
| 13 | use function array_map; |
| 14 | use function count; |
| 15 | use function is_array; |
| 16 | use function key; |
| 17 | use function ltrim; |
| 18 | use function spl_object_id; |
| 19 | class ObjectHydrator extends AbstractHydrator |
| 20 | { |
| 21 | private $identifierMap = []; |
| 22 | private $resultPointers = []; |
| 23 | private $idTemplate = []; |
| 24 | private $resultCounter = 0; |
| 25 | private $rootAliases = []; |
| 26 | private $initializedCollections = []; |
| 27 | private $uninitializedCollections = []; |
| 28 | private $existingCollections = []; |
| 29 | protected function prepare() |
| 30 | { |
| 31 | if (!isset($this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD])) { |
| 32 | $this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD] = \true; |
| 33 | } |
| 34 | foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) { |
| 35 | $this->identifierMap[$dqlAlias] = []; |
| 36 | $this->idTemplate[$dqlAlias] = ''; |
| 37 | // Remember which associations are "fetch joined", so that we know where to inject |
| 38 | // collection stubs or proxies and where not. |
| 39 | if (!isset($this->resultSetMapping()->relationMap[$dqlAlias])) { |
| 40 | continue; |
| 41 | } |
| 42 | $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; |
| 43 | if (!isset($this->resultSetMapping()->aliasMap[$parent])) { |
| 44 | throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent); |
| 45 | } |
| 46 | $sourceClassName = $this->resultSetMapping()->aliasMap[$parent]; |
| 47 | $sourceClass = $this->getClassMetadata($sourceClassName); |
| 48 | $assoc = $sourceClass->associationMappings[$this->resultSetMapping()->relationMap[$dqlAlias]]; |
| 49 | $this->_hints['fetched'][$parent][$assoc['fieldName']] = \true; |
| 50 | if ($assoc['type'] === ClassMetadata::MANY_TO_MANY) { |
| 51 | continue; |
| 52 | } |
| 53 | // Mark any non-collection opposite sides as fetched, too. |
| 54 | if ($assoc['mappedBy']) { |
| 55 | $this->_hints['fetched'][$dqlAlias][$assoc['mappedBy']] = \true; |
| 56 | continue; |
| 57 | } |
| 58 | // handle fetch-joined owning side bi-directional one-to-one associations |
| 59 | if ($assoc['inversedBy']) { |
| 60 | $class = $this->getClassMetadata($className); |
| 61 | $inverseAssoc = $class->associationMappings[$assoc['inversedBy']]; |
| 62 | if (!($inverseAssoc['type'] & ClassMetadata::TO_ONE)) { |
| 63 | continue; |
| 64 | } |
| 65 | $this->_hints['fetched'][$dqlAlias][$inverseAssoc['fieldName']] = \true; |
| 66 | } |
| 67 | } |
| 68 | } |
| 69 | protected function cleanup() |
| 70 | { |
| 71 | $eagerLoad = isset($this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD]) && $this->_hints[UnitOfWork::HINT_DEFEREAGERLOAD] === \true; |
| 72 | parent::cleanup(); |
| 73 | $this->identifierMap = $this->initializedCollections = $this->uninitializedCollections = $this->existingCollections = $this->resultPointers = []; |
| 74 | if ($eagerLoad) { |
| 75 | $this->_uow->triggerEagerLoads(); |
| 76 | } |
| 77 | $this->_uow->hydrationComplete(); |
| 78 | } |
| 79 | protected function cleanupAfterRowIteration() : void |
| 80 | { |
| 81 | $this->identifierMap = $this->initializedCollections = $this->uninitializedCollections = $this->existingCollections = $this->resultPointers = []; |
| 82 | } |
| 83 | protected function hydrateAllData() |
| 84 | { |
| 85 | $result = []; |
| 86 | while ($row = $this->statement()->fetchAssociative()) { |
| 87 | $this->hydrateRowData($row, $result); |
| 88 | } |
| 89 | // Take snapshots from all newly initialized collections |
| 90 | foreach ($this->initializedCollections as $coll) { |
| 91 | $coll->takeSnapshot(); |
| 92 | } |
| 93 | foreach ($this->uninitializedCollections as $coll) { |
| 94 | if (!$coll->isInitialized()) { |
| 95 | $coll->setInitialized(\true); |
| 96 | } |
| 97 | } |
| 98 | return $result; |
| 99 | } |
| 100 | private function initRelatedCollection($entity, ClassMetadata $class, string $fieldName, string $parentDqlAlias) : PersistentCollection |
| 101 | { |
| 102 | $oid = spl_object_id($entity); |
| 103 | $relation = $class->associationMappings[$fieldName]; |
| 104 | $value = $class->reflFields[$fieldName]->getValue($entity); |
| 105 | if ($value === null || is_array($value)) { |
| 106 | $value = new ArrayCollection((array) $value); |
| 107 | } |
| 108 | if (!$value instanceof PersistentCollection) { |
| 109 | $value = new PersistentCollection($this->_em, $this->_metadataCache[$relation['targetEntity']], $value); |
| 110 | $value->setOwner($entity, $relation); |
| 111 | $class->reflFields[$fieldName]->setValue($entity, $value); |
| 112 | $this->_uow->setOriginalEntityProperty($oid, $fieldName, $value); |
| 113 | $this->initializedCollections[$oid . $fieldName] = $value; |
| 114 | } elseif (isset($this->_hints[Query::HINT_REFRESH]) || isset($this->_hints['fetched'][$parentDqlAlias][$fieldName]) && !$value->isInitialized()) { |
| 115 | // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED! |
| 116 | $value->setDirty(\false); |
| 117 | $value->setInitialized(\true); |
| 118 | $value->unwrap()->clear(); |
| 119 | $this->initializedCollections[$oid . $fieldName] = $value; |
| 120 | } else { |
| 121 | // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN! |
| 122 | $this->existingCollections[$oid . $fieldName] = $value; |
| 123 | } |
| 124 | return $value; |
| 125 | } |
| 126 | private function getEntity(array $data, string $dqlAlias) |
| 127 | { |
| 128 | $className = $this->resultSetMapping()->aliasMap[$dqlAlias]; |
| 129 | if (isset($this->resultSetMapping()->discriminatorColumns[$dqlAlias])) { |
| 130 | $fieldName = $this->resultSetMapping()->discriminatorColumns[$dqlAlias]; |
| 131 | if (!isset($this->resultSetMapping()->metaMappings[$fieldName])) { |
| 132 | throw HydrationException::missingDiscriminatorMetaMappingColumn($className, $fieldName, $dqlAlias); |
| 133 | } |
| 134 | $discrColumn = $this->resultSetMapping()->metaMappings[$fieldName]; |
| 135 | if (!isset($data[$discrColumn])) { |
| 136 | throw HydrationException::missingDiscriminatorColumn($className, $discrColumn, $dqlAlias); |
| 137 | } |
| 138 | if ($data[$discrColumn] === '') { |
| 139 | throw HydrationException::emptyDiscriminatorValue($dqlAlias); |
| 140 | } |
| 141 | $discrMap = $this->_metadataCache[$className]->discriminatorMap; |
| 142 | $discriminatorValue = $data[$discrColumn]; |
| 143 | if ($discriminatorValue instanceof BackedEnum) { |
| 144 | $discriminatorValue = $discriminatorValue->value; |
| 145 | } |
| 146 | $discriminatorValue = (string) $discriminatorValue; |
| 147 | if (!isset($discrMap[$discriminatorValue])) { |
| 148 | throw HydrationException::invalidDiscriminatorValue($discriminatorValue, array_keys($discrMap)); |
| 149 | } |
| 150 | $className = $discrMap[$discriminatorValue]; |
| 151 | unset($data[$discrColumn]); |
| 152 | } |
| 153 | if (isset($this->_hints[Query::HINT_REFRESH_ENTITY], $this->rootAliases[$dqlAlias])) { |
| 154 | $this->registerManaged($this->_metadataCache[$className], $this->_hints[Query::HINT_REFRESH_ENTITY], $data); |
| 155 | } |
| 156 | $this->_hints['fetchAlias'] = $dqlAlias; |
| 157 | return $this->_uow->createEntity($className, $data, $this->_hints); |
| 158 | } |
| 159 | private function getEntityFromIdentityMap(string $className, array $data) |
| 160 | { |
| 161 | // TODO: Abstract this code and UnitOfWork::createEntity() equivalent? |
| 162 | $class = $this->_metadataCache[$className]; |
| 163 | if ($class->isIdentifierComposite) { |
| 164 | $idHash = UnitOfWork::getIdHashByIdentifier(array_map( |
| 165 | static function (string $fieldName) use($data, $class) { |
| 166 | return isset($class->associationMappings[$fieldName]) ? $data[$class->associationMappings[$fieldName]['joinColumns'][0]['name']] : $data[$fieldName]; |
| 167 | }, |
| 168 | $class->identifier |
| 169 | )); |
| 170 | return $this->_uow->tryGetByIdHash(ltrim($idHash), $class->rootEntityName); |
| 171 | } elseif (isset($class->associationMappings[$class->identifier[0]])) { |
| 172 | return $this->_uow->tryGetByIdHash($data[$class->associationMappings[$class->identifier[0]]['joinColumns'][0]['name']], $class->rootEntityName); |
| 173 | } |
| 174 | return $this->_uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName); |
| 175 | } |
| 176 | protected function hydrateRowData(array $row, array &$result) |
| 177 | { |
| 178 | // Initialize |
| 179 | $id = $this->idTemplate; |
| 180 | // initialize the id-memory |
| 181 | $nonemptyComponents = []; |
| 182 | // Split the row data into chunks of class data. |
| 183 | $rowData = $this->gatherRowData($row, $id, $nonemptyComponents); |
| 184 | // reset result pointers for each data row |
| 185 | $this->resultPointers = []; |
| 186 | // Hydrate the data chunks |
| 187 | foreach ($rowData['data'] as $dqlAlias => $data) { |
| 188 | $entityName = $this->resultSetMapping()->aliasMap[$dqlAlias]; |
| 189 | if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) { |
| 190 | // It's a joined result |
| 191 | $parentAlias = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; |
| 192 | // we need the $path to save into the identifier map which entities were already |
| 193 | // seen for this parent-child relationship |
| 194 | $path = $parentAlias . '.' . $dqlAlias; |
| 195 | // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs |
| 196 | if (!isset($nonemptyComponents[$parentAlias])) { |
| 197 | // TODO: Add special case code where we hydrate the right join objects into identity map at least |
| 198 | continue; |
| 199 | } |
| 200 | $parentClass = $this->_metadataCache[$this->resultSetMapping()->aliasMap[$parentAlias]]; |
| 201 | $relationField = $this->resultSetMapping()->relationMap[$dqlAlias]; |
| 202 | $relation = $parentClass->associationMappings[$relationField]; |
| 203 | $reflField = $parentClass->reflFields[$relationField]; |
| 204 | // Get a reference to the parent object to which the joined element belongs. |
| 205 | if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parentAlias])) { |
| 206 | $objectClass = $this->resultPointers[$parentAlias]; |
| 207 | $parentObject = $objectClass[key($objectClass)]; |
| 208 | } elseif (isset($this->resultPointers[$parentAlias])) { |
| 209 | $parentObject = $this->resultPointers[$parentAlias]; |
| 210 | } else { |
| 211 | // Parent object of relation not found, mark as not-fetched again |
| 212 | if (isset($nonemptyComponents[$dqlAlias])) { |
| 213 | $element = $this->getEntity($data, $dqlAlias); |
| 214 | // Update result pointer and provide initial fetch data for parent |
| 215 | $this->resultPointers[$dqlAlias] = $element; |
| 216 | $rowData['data'][$parentAlias][$relationField] = $element; |
| 217 | } else { |
| 218 | $element = null; |
| 219 | } |
| 220 | // Mark as not-fetched again |
| 221 | unset($this->_hints['fetched'][$parentAlias][$relationField]); |
| 222 | continue; |
| 223 | } |
| 224 | $oid = spl_object_id($parentObject); |
| 225 | // Check the type of the relation (many or single-valued) |
| 226 | if (!($relation['type'] & ClassMetadata::TO_ONE)) { |
| 227 | // PATH A: Collection-valued association |
| 228 | $reflFieldValue = $reflField->getValue($parentObject); |
| 229 | if (isset($nonemptyComponents[$dqlAlias])) { |
| 230 | $collKey = $oid . $relationField; |
| 231 | if (isset($this->initializedCollections[$collKey])) { |
| 232 | $reflFieldValue = $this->initializedCollections[$collKey]; |
| 233 | } elseif (!isset($this->existingCollections[$collKey])) { |
| 234 | $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias); |
| 235 | } |
| 236 | $indexExists = isset($this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]); |
| 237 | $index = $indexExists ? $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : \false; |
| 238 | $indexIsValid = $index !== \false ? isset($reflFieldValue[$index]) : \false; |
| 239 | if (!$indexExists || !$indexIsValid) { |
| 240 | if (isset($this->existingCollections[$collKey])) { |
| 241 | // Collection exists, only look for the element in the identity map. |
| 242 | $element = $this->getEntityFromIdentityMap($entityName, $data); |
| 243 | if ($element) { |
| 244 | $this->resultPointers[$dqlAlias] = $element; |
| 245 | } else { |
| 246 | unset($this->resultPointers[$dqlAlias]); |
| 247 | } |
| 248 | } else { |
| 249 | $element = $this->getEntity($data, $dqlAlias); |
| 250 | if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { |
| 251 | $indexValue = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; |
| 252 | $reflFieldValue->hydrateSet($indexValue, $element); |
| 253 | $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue; |
| 254 | } else { |
| 255 | if (!$reflFieldValue->contains($element)) { |
| 256 | $reflFieldValue->hydrateAdd($element); |
| 257 | $reflFieldValue->last(); |
| 258 | } |
| 259 | $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key(); |
| 260 | } |
| 261 | // Update result pointer |
| 262 | $this->resultPointers[$dqlAlias] = $element; |
| 263 | } |
| 264 | } else { |
| 265 | // Update result pointer |
| 266 | $this->resultPointers[$dqlAlias] = $reflFieldValue[$index]; |
| 267 | } |
| 268 | } elseif (!$reflFieldValue) { |
| 269 | $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias); |
| 270 | } elseif ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === \false && !isset($this->uninitializedCollections[$oid . $relationField])) { |
| 271 | $this->uninitializedCollections[$oid . $relationField] = $reflFieldValue; |
| 272 | } |
| 273 | } else { |
| 274 | // PATH B: Single-valued association |
| 275 | $reflFieldValue = $reflField->getValue($parentObject); |
| 276 | if (!$reflFieldValue || isset($this->_hints[Query::HINT_REFRESH]) || $this->_uow->isUninitializedObject($reflFieldValue)) { |
| 277 | // we only need to take action if this value is null, |
| 278 | // we refresh the entity or its an uninitialized proxy. |
| 279 | if (isset($nonemptyComponents[$dqlAlias])) { |
| 280 | $element = $this->getEntity($data, $dqlAlias); |
| 281 | $reflField->setValue($parentObject, $element); |
| 282 | $this->_uow->setOriginalEntityProperty($oid, $relationField, $element); |
| 283 | $targetClass = $this->_metadataCache[$relation['targetEntity']]; |
| 284 | if ($relation['isOwningSide']) { |
| 285 | // TODO: Just check hints['fetched'] here? |
| 286 | // If there is an inverse mapping on the target class its bidirectional |
| 287 | if ($relation['inversedBy']) { |
| 288 | $inverseAssoc = $targetClass->associationMappings[$relation['inversedBy']]; |
| 289 | if ($inverseAssoc['type'] & ClassMetadata::TO_ONE) { |
| 290 | $targetClass->reflFields[$inverseAssoc['fieldName']]->setValue($element, $parentObject); |
| 291 | $this->_uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc['fieldName'], $parentObject); |
| 292 | } |
| 293 | } |
| 294 | } else { |
| 295 | // For sure bidirectional, as there is no inverse side in unidirectional mappings |
| 296 | $targetClass->reflFields[$relation['mappedBy']]->setValue($element, $parentObject); |
| 297 | $this->_uow->setOriginalEntityProperty(spl_object_id($element), $relation['mappedBy'], $parentObject); |
| 298 | } |
| 299 | // Update result pointer |
| 300 | $this->resultPointers[$dqlAlias] = $element; |
| 301 | } else { |
| 302 | $this->_uow->setOriginalEntityProperty($oid, $relationField, null); |
| 303 | $reflField->setValue($parentObject, null); |
| 304 | } |
| 305 | // else leave $reflFieldValue null for single-valued associations |
| 306 | } else { |
| 307 | // Update result pointer |
| 308 | $this->resultPointers[$dqlAlias] = $reflFieldValue; |
| 309 | } |
| 310 | } |
| 311 | } else { |
| 312 | // PATH C: Its a root result element |
| 313 | $this->rootAliases[$dqlAlias] = \true; |
| 314 | // Mark as root alias |
| 315 | $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0; |
| 316 | // if this row has a NULL value for the root result id then make it a null result. |
| 317 | if (!isset($nonemptyComponents[$dqlAlias])) { |
| 318 | if ($this->resultSetMapping()->isMixed) { |
| 319 | $result[] = [$entityKey => null]; |
| 320 | } else { |
| 321 | $result[] = null; |
| 322 | } |
| 323 | $resultKey = $this->resultCounter; |
| 324 | ++$this->resultCounter; |
| 325 | continue; |
| 326 | } |
| 327 | // check for existing result from the iterations before |
| 328 | if (!isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) { |
| 329 | $element = $this->getEntity($data, $dqlAlias); |
| 330 | if ($this->resultSetMapping()->isMixed) { |
| 331 | $element = [$entityKey => $element]; |
| 332 | } |
| 333 | if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { |
| 334 | $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; |
| 335 | if (isset($this->_hints['collection'])) { |
| 336 | $this->_hints['collection']->hydrateSet($resultKey, $element); |
| 337 | } |
| 338 | $result[$resultKey] = $element; |
| 339 | } else { |
| 340 | $resultKey = $this->resultCounter; |
| 341 | ++$this->resultCounter; |
| 342 | if (isset($this->_hints['collection'])) { |
| 343 | $this->_hints['collection']->hydrateAdd($element); |
| 344 | } |
| 345 | $result[] = $element; |
| 346 | } |
| 347 | $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; |
| 348 | // Update result pointer |
| 349 | $this->resultPointers[$dqlAlias] = $element; |
| 350 | } else { |
| 351 | // Update result pointer |
| 352 | $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]]; |
| 353 | $this->resultPointers[$dqlAlias] = $result[$index]; |
| 354 | $resultKey = $index; |
| 355 | } |
| 356 | } |
| 357 | if (isset($this->_hints[Query::HINT_INTERNAL_ITERATION]) && $this->_hints[Query::HINT_INTERNAL_ITERATION]) { |
| 358 | $this->_uow->hydrationComplete(); |
| 359 | } |
| 360 | } |
| 361 | if (!isset($resultKey)) { |
| 362 | $this->resultCounter++; |
| 363 | } |
| 364 | // Append scalar values to mixed result sets |
| 365 | if (isset($rowData['scalars'])) { |
| 366 | if (!isset($resultKey)) { |
| 367 | $resultKey = isset($this->resultSetMapping()->indexByMap['scalars']) ? $row[$this->resultSetMapping()->indexByMap['scalars']] : $this->resultCounter - 1; |
| 368 | } |
| 369 | foreach ($rowData['scalars'] as $name => $value) { |
| 370 | $result[$resultKey][$name] = $value; |
| 371 | } |
| 372 | } |
| 373 | // Append new object to mixed result sets |
| 374 | if (isset($rowData['newObjects'])) { |
| 375 | if (!isset($resultKey)) { |
| 376 | $resultKey = $this->resultCounter - 1; |
| 377 | } |
| 378 | $scalarCount = isset($rowData['scalars']) ? count($rowData['scalars']) : 0; |
| 379 | foreach ($rowData['newObjects'] as $objIndex => $newObject) { |
| 380 | $class = $newObject['class']; |
| 381 | $args = $newObject['args']; |
| 382 | $obj = $class->newInstanceArgs($args); |
| 383 | if ($scalarCount === 0 && count($rowData['newObjects']) === 1) { |
| 384 | $result[$resultKey] = $obj; |
| 385 | continue; |
| 386 | } |
| 387 | $result[$resultKey][$objIndex] = $obj; |
| 388 | } |
| 389 | } |
| 390 | } |
| 391 | public function onClear($eventArgs) |
| 392 | { |
| 393 | parent::onClear($eventArgs); |
| 394 | $aliases = array_keys($this->identifierMap); |
| 395 | $this->identifierMap = array_fill_keys($aliases, []); |
| 396 | } |
| 397 | } |
| 398 |