summaryrefslogtreecommitdiff
path: root/vendor/doctrine/orm/src/Internal
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/doctrine/orm/src/Internal')
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php556
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php270
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php67
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php586
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php34
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php35
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php176
-rw-r--r--vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php40
-rw-r--r--vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php64
-rw-r--r--vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php55
-rw-r--r--vendor/doctrine/orm/src/Internal/QueryType.php13
-rw-r--r--vendor/doctrine/orm/src/Internal/SQLResultCasing.php30
-rw-r--r--vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php159
-rw-r--r--vendor/doctrine/orm/src/Internal/TopologicalSort.php155
-rw-r--r--vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php47
15 files changed, 2287 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php
new file mode 100644
index 0000000..d8bffe4
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php
@@ -0,0 +1,556 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use BackedEnum;
8use Doctrine\DBAL\Platforms\AbstractPlatform;
9use Doctrine\DBAL\Result;
10use Doctrine\DBAL\Types\Type;
11use Doctrine\ORM\EntityManagerInterface;
12use Doctrine\ORM\Events;
13use Doctrine\ORM\Mapping\ClassMetadata;
14use Doctrine\ORM\Query\ResultSetMapping;
15use Doctrine\ORM\Tools\Pagination\LimitSubqueryWalker;
16use Doctrine\ORM\UnitOfWork;
17use Generator;
18use LogicException;
19use ReflectionClass;
20
21use function array_map;
22use function array_merge;
23use function count;
24use function end;
25use function in_array;
26use function is_array;
27
28/**
29 * Base class for all hydrators. A hydrator is a class that provides some form
30 * of transformation of an SQL result set into another structure.
31 *
32 * @psalm-consistent-constructor
33 */
34abstract class AbstractHydrator
35{
36 /**
37 * The ResultSetMapping.
38 */
39 protected ResultSetMapping|null $rsm = null;
40
41 /**
42 * The dbms Platform instance.
43 */
44 protected AbstractPlatform $platform;
45
46 /**
47 * The UnitOfWork of the associated EntityManager.
48 */
49 protected UnitOfWork $uow;
50
51 /**
52 * Local ClassMetadata cache to avoid going to the EntityManager all the time.
53 *
54 * @var array<string, ClassMetadata<object>>
55 */
56 protected array $metadataCache = [];
57
58 /**
59 * The cache used during row-by-row hydration.
60 *
61 * @var array<string, mixed[]|null>
62 */
63 protected array $cache = [];
64
65 /**
66 * The statement that provides the data to hydrate.
67 */
68 protected Result|null $stmt = null;
69
70 /**
71 * The query hints.
72 *
73 * @var array<string, mixed>
74 */
75 protected array $hints = [];
76
77 /**
78 * Initializes a new instance of a class derived from <tt>AbstractHydrator</tt>.
79 */
80 public function __construct(protected EntityManagerInterface $em)
81 {
82 $this->platform = $em->getConnection()->getDatabasePlatform();
83 $this->uow = $em->getUnitOfWork();
84 }
85
86 /**
87 * Initiates a row-by-row hydration.
88 *
89 * @psalm-param array<string, mixed> $hints
90 *
91 * @return Generator<array-key, mixed>
92 *
93 * @final
94 */
95 final public function toIterable(Result $stmt, ResultSetMapping $resultSetMapping, array $hints = []): Generator
96 {
97 $this->stmt = $stmt;
98 $this->rsm = $resultSetMapping;
99 $this->hints = $hints;
100
101 $evm = $this->em->getEventManager();
102
103 $evm->addEventListener([Events::onClear], $this);
104
105 $this->prepare();
106
107 try {
108 while (true) {
109 $row = $this->statement()->fetchAssociative();
110
111 if ($row === false) {
112 break;
113 }
114
115 $result = [];
116
117 $this->hydrateRowData($row, $result);
118
119 $this->cleanupAfterRowIteration();
120 if (count($result) === 1) {
121 if (count($resultSetMapping->indexByMap) === 0) {
122 yield end($result);
123 } else {
124 yield from $result;
125 }
126 } else {
127 yield $result;
128 }
129 }
130 } finally {
131 $this->cleanup();
132 }
133 }
134
135 final protected function statement(): Result
136 {
137 if ($this->stmt === null) {
138 throw new LogicException('Uninitialized _stmt property');
139 }
140
141 return $this->stmt;
142 }
143
144 final protected function resultSetMapping(): ResultSetMapping
145 {
146 if ($this->rsm === null) {
147 throw new LogicException('Uninitialized _rsm property');
148 }
149
150 return $this->rsm;
151 }
152
153 /**
154 * Hydrates all rows returned by the passed statement instance at once.
155 *
156 * @psalm-param array<string, string> $hints
157 */
158 public function hydrateAll(Result $stmt, ResultSetMapping $resultSetMapping, array $hints = []): mixed
159 {
160 $this->stmt = $stmt;
161 $this->rsm = $resultSetMapping;
162 $this->hints = $hints;
163
164 $this->em->getEventManager()->addEventListener([Events::onClear], $this);
165 $this->prepare();
166
167 try {
168 $result = $this->hydrateAllData();
169 } finally {
170 $this->cleanup();
171 }
172
173 return $result;
174 }
175
176 /**
177 * When executed in a hydrate() loop we have to clear internal state to
178 * decrease memory consumption.
179 */
180 public function onClear(mixed $eventArgs): void
181 {
182 }
183
184 /**
185 * Executes one-time preparation tasks, once each time hydration is started
186 * through {@link hydrateAll} or {@link toIterable()}.
187 */
188 protected function prepare(): void
189 {
190 }
191
192 /**
193 * Executes one-time cleanup tasks at the end of a hydration that was initiated
194 * through {@link hydrateAll} or {@link toIterable()}.
195 */
196 protected function cleanup(): void
197 {
198 $this->statement()->free();
199
200 $this->stmt = null;
201 $this->rsm = null;
202 $this->cache = [];
203 $this->metadataCache = [];
204
205 $this
206 ->em
207 ->getEventManager()
208 ->removeEventListener([Events::onClear], $this);
209 }
210
211 protected function cleanupAfterRowIteration(): void
212 {
213 }
214
215 /**
216 * Hydrates a single row from the current statement instance.
217 *
218 * Template method.
219 *
220 * @param mixed[] $row The row data.
221 * @param mixed[] $result The result to fill.
222 *
223 * @throws HydrationException
224 */
225 protected function hydrateRowData(array $row, array &$result): void
226 {
227 throw new HydrationException('hydrateRowData() not implemented by this hydrator.');
228 }
229
230 /**
231 * Hydrates all rows from the current statement instance at once.
232 */
233 abstract protected function hydrateAllData(): mixed;
234
235 /**
236 * Processes a row of the result set.
237 *
238 * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY).
239 * Puts the elements of a result row into a new array, grouped by the dql alias
240 * they belong to. The column names in the result set are mapped to their
241 * field names during this procedure as well as any necessary conversions on
242 * the values applied. Scalar values are kept in a specific key 'scalars'.
243 *
244 * @param mixed[] $data SQL Result Row.
245 * @psalm-param array<string, string> $id Dql-Alias => ID-Hash.
246 * @psalm-param array<string, bool> $nonemptyComponents Does this DQL-Alias has at least one non NULL value?
247 *
248 * @return array<string, array<string, mixed>> An array with all the fields
249 * (name => value) of the data
250 * row, grouped by their
251 * component alias.
252 * @psalm-return array{
253 * data: array<array-key, array>,
254 * newObjects?: array<array-key, array{
255 * class: mixed,
256 * args?: array
257 * }>,
258 * scalars?: array
259 * }
260 */
261 protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array
262 {
263 $rowData = ['data' => []];
264
265 foreach ($data as $key => $value) {
266 $cacheKeyInfo = $this->hydrateColumnInfo($key);
267 if ($cacheKeyInfo === null) {
268 continue;
269 }
270
271 $fieldName = $cacheKeyInfo['fieldName'];
272
273 switch (true) {
274 case isset($cacheKeyInfo['isNewObjectParameter']):
275 $argIndex = $cacheKeyInfo['argIndex'];
276 $objIndex = $cacheKeyInfo['objIndex'];
277 $type = $cacheKeyInfo['type'];
278 $value = $type->convertToPHPValue($value, $this->platform);
279
280 if ($value !== null && isset($cacheKeyInfo['enumType'])) {
281 $value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
282 }
283
284 $rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class'];
285 $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value;
286 break;
287
288 case isset($cacheKeyInfo['isScalar']):
289 $type = $cacheKeyInfo['type'];
290 $value = $type->convertToPHPValue($value, $this->platform);
291
292 if ($value !== null && isset($cacheKeyInfo['enumType'])) {
293 $value = $this->buildEnum($value, $cacheKeyInfo['enumType']);
294 }
295
296 $rowData['scalars'][$fieldName] = $value;
297
298 break;
299
300 //case (isset($cacheKeyInfo['isMetaColumn'])):
301 default:
302 $dqlAlias = $cacheKeyInfo['dqlAlias'];
303 $type = $cacheKeyInfo['type'];
304
305 // If there are field name collisions in the child class, then we need
306 // to only hydrate if we are looking at the correct discriminator value
307 if (
308 isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']])
309 && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true)
310 ) {
311 break;
312 }
313
314 // in an inheritance hierarchy the same field could be defined several times.
315 // We overwrite this value so long we don't have a non-null value, that value we keep.
316 // Per definition it cannot be that a field is defined several times and has several values.
317 if (isset($rowData['data'][$dqlAlias][$fieldName])) {
318 break;
319 }
320
321 $rowData['data'][$dqlAlias][$fieldName] = $type
322 ? $type->convertToPHPValue($value, $this->platform)
323 : $value;
324
325 if ($rowData['data'][$dqlAlias][$fieldName] !== null && isset($cacheKeyInfo['enumType'])) {
326 $rowData['data'][$dqlAlias][$fieldName] = $this->buildEnum($rowData['data'][$dqlAlias][$fieldName], $cacheKeyInfo['enumType']);
327 }
328
329 if ($cacheKeyInfo['isIdentifier'] && $value !== null) {
330 $id[$dqlAlias] .= '|' . $value;
331 $nonemptyComponents[$dqlAlias] = true;
332 }
333
334 break;
335 }
336 }
337
338 return $rowData;
339 }
340
341 /**
342 * Processes a row of the result set.
343 *
344 * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that
345 * simply converts column names to field names and properly converts the
346 * values according to their types. The resulting row has the same number
347 * of elements as before.
348 *
349 * @param mixed[] $data
350 * @psalm-param array<string, mixed> $data
351 *
352 * @return mixed[] The processed row.
353 * @psalm-return array<string, mixed>
354 */
355 protected function gatherScalarRowData(array &$data): array
356 {
357 $rowData = [];
358
359 foreach ($data as $key => $value) {
360 $cacheKeyInfo = $this->hydrateColumnInfo($key);
361 if ($cacheKeyInfo === null) {
362 continue;
363 }
364
365 $fieldName = $cacheKeyInfo['fieldName'];
366
367 // WARNING: BC break! We know this is the desired behavior to type convert values, but this
368 // erroneous behavior exists since 2.0 and we're forced to keep compatibility.
369 if (! isset($cacheKeyInfo['isScalar'])) {
370 $type = $cacheKeyInfo['type'];
371 $value = $type ? $type->convertToPHPValue($value, $this->platform) : $value;
372
373 $fieldName = $cacheKeyInfo['dqlAlias'] . '_' . $fieldName;
374 }
375
376 $rowData[$fieldName] = $value;
377 }
378
379 return $rowData;
380 }
381
382 /**
383 * Retrieve column information from ResultSetMapping.
384 *
385 * @param string $key Column name
386 *
387 * @return mixed[]|null
388 * @psalm-return array<string, mixed>|null
389 */
390 protected function hydrateColumnInfo(string $key): array|null
391 {
392 if (isset($this->cache[$key])) {
393 return $this->cache[$key];
394 }
395
396 switch (true) {
397 // NOTE: Most of the times it's a field mapping, so keep it first!!!
398 case isset($this->rsm->fieldMappings[$key]):
399 $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]);
400 $fieldName = $this->rsm->fieldMappings[$key];
401 $fieldMapping = $classMetadata->fieldMappings[$fieldName];
402 $ownerMap = $this->rsm->columnOwnerMap[$key];
403 $columnInfo = [
404 'isIdentifier' => in_array($fieldName, $classMetadata->identifier, true),
405 'fieldName' => $fieldName,
406 'type' => Type::getType($fieldMapping->type),
407 'dqlAlias' => $ownerMap,
408 'enumType' => $this->rsm->enumMappings[$key] ?? null,
409 ];
410
411 // the current discriminator value must be saved in order to disambiguate fields hydration,
412 // should there be field name collisions
413 if ($classMetadata->parentClasses && isset($this->rsm->discriminatorColumns[$ownerMap])) {
414 return $this->cache[$key] = array_merge(
415 $columnInfo,
416 [
417 'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap],
418 'discriminatorValue' => $classMetadata->discriminatorValue,
419 'discriminatorValues' => $this->getDiscriminatorValues($classMetadata),
420 ],
421 );
422 }
423
424 return $this->cache[$key] = $columnInfo;
425
426 case isset($this->rsm->newObjectMappings[$key]):
427 // WARNING: A NEW object is also a scalar, so it must be declared before!
428 $mapping = $this->rsm->newObjectMappings[$key];
429
430 return $this->cache[$key] = [
431 'isScalar' => true,
432 'isNewObjectParameter' => true,
433 'fieldName' => $this->rsm->scalarMappings[$key],
434 'type' => Type::getType($this->rsm->typeMappings[$key]),
435 'argIndex' => $mapping['argIndex'],
436 'objIndex' => $mapping['objIndex'],
437 'class' => new ReflectionClass($mapping['className']),
438 'enumType' => $this->rsm->enumMappings[$key] ?? null,
439 ];
440
441 case isset($this->rsm->scalarMappings[$key], $this->hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]):
442 return $this->cache[$key] = [
443 'fieldName' => $this->rsm->scalarMappings[$key],
444 'type' => Type::getType($this->rsm->typeMappings[$key]),
445 'dqlAlias' => '',
446 'enumType' => $this->rsm->enumMappings[$key] ?? null,
447 ];
448
449 case isset($this->rsm->scalarMappings[$key]):
450 return $this->cache[$key] = [
451 'isScalar' => true,
452 'fieldName' => $this->rsm->scalarMappings[$key],
453 'type' => Type::getType($this->rsm->typeMappings[$key]),
454 'enumType' => $this->rsm->enumMappings[$key] ?? null,
455 ];
456
457 case isset($this->rsm->metaMappings[$key]):
458 // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns).
459 $fieldName = $this->rsm->metaMappings[$key];
460 $dqlAlias = $this->rsm->columnOwnerMap[$key];
461 $type = isset($this->rsm->typeMappings[$key])
462 ? Type::getType($this->rsm->typeMappings[$key])
463 : null;
464
465 // Cache metadata fetch
466 $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]);
467
468 return $this->cache[$key] = [
469 'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]),
470 'isMetaColumn' => true,
471 'fieldName' => $fieldName,
472 'type' => $type,
473 'dqlAlias' => $dqlAlias,
474 'enumType' => $this->rsm->enumMappings[$key] ?? null,
475 ];
476 }
477
478 // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2
479 // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping.
480 return null;
481 }
482
483 /**
484 * @return string[]
485 * @psalm-return non-empty-list<string>
486 */
487 private function getDiscriminatorValues(ClassMetadata $classMetadata): array
488 {
489 $values = array_map(
490 fn (string $subClass): string => (string) $this->getClassMetadata($subClass)->discriminatorValue,
491 $classMetadata->subClasses,
492 );
493
494 $values[] = (string) $classMetadata->discriminatorValue;
495
496 return $values;
497 }
498
499 /**
500 * Retrieve ClassMetadata associated to entity class name.
501 */
502 protected function getClassMetadata(string $className): ClassMetadata
503 {
504 if (! isset($this->metadataCache[$className])) {
505 $this->metadataCache[$className] = $this->em->getClassMetadata($className);
506 }
507
508 return $this->metadataCache[$className];
509 }
510
511 /**
512 * Register entity as managed in UnitOfWork.
513 *
514 * @param mixed[] $data
515 *
516 * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow
517 */
518 protected function registerManaged(ClassMetadata $class, object $entity, array $data): void
519 {
520 if ($class->isIdentifierComposite) {
521 $id = [];
522
523 foreach ($class->identifier as $fieldName) {
524 $id[$fieldName] = isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide()
525 ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name]
526 : $data[$fieldName];
527 }
528 } else {
529 $fieldName = $class->identifier[0];
530 $id = [
531 $fieldName => isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide()
532 ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name]
533 : $data[$fieldName],
534 ];
535 }
536
537 $this->em->getUnitOfWork()->registerManaged($entity, $id, $data);
538 }
539
540 /**
541 * @param class-string<BackedEnum> $enumType
542 *
543 * @return BackedEnum|array<BackedEnum>
544 */
545 final protected function buildEnum(mixed $value, string $enumType): BackedEnum|array
546 {
547 if (is_array($value)) {
548 return array_map(
549 static fn ($value) => $enumType::from($value),
550 $value,
551 );
552 }
553
554 return $enumType::from($value);
555 }
556}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php
new file mode 100644
index 0000000..7115c16
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php
@@ -0,0 +1,270 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use function array_key_last;
8use function count;
9use function is_array;
10use function key;
11use function reset;
12
13/**
14 * The ArrayHydrator produces a nested array "graph" that is often (not always)
15 * interchangeable with the corresponding object graph for read-only access.
16 */
17class ArrayHydrator extends AbstractHydrator
18{
19 /** @var array<string,bool> */
20 private array $rootAliases = [];
21
22 private bool $isSimpleQuery = false;
23
24 /** @var mixed[] */
25 private array $identifierMap = [];
26
27 /** @var mixed[] */
28 private array $resultPointers = [];
29
30 /** @var array<string,string> */
31 private array $idTemplate = [];
32
33 private int $resultCounter = 0;
34
35 protected function prepare(): void
36 {
37 $this->isSimpleQuery = count($this->resultSetMapping()->aliasMap) <= 1;
38
39 foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) {
40 $this->identifierMap[$dqlAlias] = [];
41 $this->resultPointers[$dqlAlias] = [];
42 $this->idTemplate[$dqlAlias] = '';
43 }
44 }
45
46 /**
47 * {@inheritDoc}
48 */
49 protected function hydrateAllData(): array
50 {
51 $result = [];
52
53 while ($data = $this->statement()->fetchAssociative()) {
54 $this->hydrateRowData($data, $result);
55 }
56
57 return $result;
58 }
59
60 /**
61 * {@inheritDoc}
62 */
63 protected function hydrateRowData(array $row, array &$result): void
64 {
65 // 1) Initialize
66 $id = $this->idTemplate; // initialize the id-memory
67 $nonemptyComponents = [];
68 $rowData = $this->gatherRowData($row, $id, $nonemptyComponents);
69
70 // 2) Now hydrate the data found in the current row.
71 foreach ($rowData['data'] as $dqlAlias => $data) {
72 $index = false;
73
74 if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) {
75 // It's a joined result
76
77 $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
78 $path = $parent . '.' . $dqlAlias;
79
80 // missing parent data, skipping as RIGHT JOIN hydration is not supported.
81 if (! isset($nonemptyComponents[$parent])) {
82 continue;
83 }
84
85 // Get a reference to the right element in the result tree.
86 // This element will get the associated element attached.
87 if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parent])) {
88 $first = reset($this->resultPointers);
89 // TODO: Exception if $key === null ?
90 $baseElement =& $this->resultPointers[$parent][key($first)];
91 } elseif (isset($this->resultPointers[$parent])) {
92 $baseElement =& $this->resultPointers[$parent];
93 } else {
94 unset($this->resultPointers[$dqlAlias]); // Ticket #1228
95
96 continue;
97 }
98
99 $relationAlias = $this->resultSetMapping()->relationMap[$dqlAlias];
100 $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parent]];
101 $relation = $parentClass->associationMappings[$relationAlias];
102
103 // Check the type of the relation (many or single-valued)
104 if (! $relation->isToOne()) {
105 $oneToOne = false;
106
107 if (! isset($baseElement[$relationAlias])) {
108 $baseElement[$relationAlias] = [];
109 }
110
111 if (isset($nonemptyComponents[$dqlAlias])) {
112 $indexExists = isset($this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]]);
113 $index = $indexExists ? $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false;
114 $indexIsValid = $index !== false ? isset($baseElement[$relationAlias][$index]) : false;
115
116 if (! $indexExists || ! $indexIsValid) {
117 $element = $data;
118
119 if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) {
120 $baseElement[$relationAlias][$row[$this->resultSetMapping()->indexByMap[$dqlAlias]]] = $element;
121 } else {
122 $baseElement[$relationAlias][] = $element;
123 }
124
125 $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = array_key_last($baseElement[$relationAlias]);
126 }
127 }
128 } else {
129 $oneToOne = true;
130
131 if (
132 ! isset($nonemptyComponents[$dqlAlias]) &&
133 ( ! isset($baseElement[$relationAlias]))
134 ) {
135 $baseElement[$relationAlias] = null;
136 } elseif (! isset($baseElement[$relationAlias])) {
137 $baseElement[$relationAlias] = $data;
138 }
139 }
140
141 $coll =& $baseElement[$relationAlias];
142
143 if (is_array($coll)) {
144 $this->updateResultPointer($coll, $index, $dqlAlias, $oneToOne);
145 }
146 } else {
147 // It's a root result element
148
149 $this->rootAliases[$dqlAlias] = true; // Mark as root
150 $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0;
151
152 // if this row has a NULL value for the root result id then make it a null result.
153 if (! isset($nonemptyComponents[$dqlAlias])) {
154 $result[] = $this->resultSetMapping()->isMixed
155 ? [$entityKey => null]
156 : null;
157
158 $resultKey = $this->resultCounter;
159 ++$this->resultCounter;
160
161 continue;
162 }
163
164 // Check for an existing element
165 if ($this->isSimpleQuery || ! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) {
166 $element = $this->resultSetMapping()->isMixed
167 ? [$entityKey => $data]
168 : $data;
169
170 if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) {
171 $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]];
172 $result[$resultKey] = $element;
173 } else {
174 $resultKey = $this->resultCounter;
175 $result[] = $element;
176
177 ++$this->resultCounter;
178 }
179
180 $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey;
181 } else {
182 $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]];
183 $resultKey = $index;
184 }
185
186 $this->updateResultPointer($result, $index, $dqlAlias, false);
187 }
188 }
189
190 if (! isset($resultKey)) {
191 $this->resultCounter++;
192 }
193
194 // Append scalar values to mixed result sets
195 if (isset($rowData['scalars'])) {
196 if (! isset($resultKey)) {
197 // this only ever happens when no object is fetched (scalar result only)
198 $resultKey = isset($this->resultSetMapping()->indexByMap['scalars'])
199 ? $row[$this->resultSetMapping()->indexByMap['scalars']]
200 : $this->resultCounter - 1;
201 }
202
203 foreach ($rowData['scalars'] as $name => $value) {
204 $result[$resultKey][$name] = $value;
205 }
206 }
207
208 // Append new object to mixed result sets
209 if (isset($rowData['newObjects'])) {
210 if (! isset($resultKey)) {
211 $resultKey = $this->resultCounter - 1;
212 }
213
214 $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
215
216 foreach ($rowData['newObjects'] as $objIndex => $newObject) {
217 $class = $newObject['class'];
218 $args = $newObject['args'];
219 $obj = $class->newInstanceArgs($args);
220
221 if (count($args) === $scalarCount || ($scalarCount === 0 && count($rowData['newObjects']) === 1)) {
222 $result[$resultKey] = $obj;
223
224 continue;
225 }
226
227 $result[$resultKey][$objIndex] = $obj;
228 }
229 }
230 }
231
232 /**
233 * Updates the result pointer for an Entity. The result pointers point to the
234 * last seen instance of each Entity type. This is used for graph construction.
235 *
236 * @param mixed[]|null $coll The element.
237 * @param string|int|false $index Index of the element in the collection.
238 * @param bool $oneToOne Whether it is a single-valued association or not.
239 */
240 private function updateResultPointer(
241 array|null &$coll,
242 string|int|false $index,
243 string $dqlAlias,
244 bool $oneToOne,
245 ): void {
246 if ($coll === null) {
247 unset($this->resultPointers[$dqlAlias]); // Ticket #1228
248
249 return;
250 }
251
252 if ($oneToOne) {
253 $this->resultPointers[$dqlAlias] =& $coll;
254
255 return;
256 }
257
258 if ($index !== false) {
259 $this->resultPointers[$dqlAlias] =& $coll[$index];
260
261 return;
262 }
263
264 if (! $coll) {
265 return;
266 }
267
268 $this->resultPointers[$dqlAlias] =& $coll[array_key_last($coll)];
269 }
270}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php b/vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php
new file mode 100644
index 0000000..710114f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php
@@ -0,0 +1,67 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use Doctrine\ORM\Exception\ORMException;
8use Exception;
9
10use function implode;
11use function sprintf;
12
13class HydrationException extends Exception implements ORMException
14{
15 public static function nonUniqueResult(): self
16 {
17 return new self('The result returned by the query was not unique.');
18 }
19
20 public static function parentObjectOfRelationNotFound(string $alias, string $parentAlias): self
21 {
22 return new self(sprintf(
23 "The parent object of entity result with alias '%s' was not found."
24 . " The parent alias is '%s'.",
25 $alias,
26 $parentAlias,
27 ));
28 }
29
30 public static function emptyDiscriminatorValue(string $dqlAlias): self
31 {
32 return new self("The DQL alias '" . $dqlAlias . "' contains an entity " .
33 'of an inheritance hierarchy with an empty discriminator value. This means ' .
34 'that the database contains inconsistent data with an empty ' .
35 'discriminator value in a table row.');
36 }
37
38 public static function missingDiscriminatorColumn(string $entityName, string $discrColumnName, string $dqlAlias): self
39 {
40 return new self(sprintf(
41 'The discriminator column "%s" is missing for "%s" using the DQL alias "%s".',
42 $discrColumnName,
43 $entityName,
44 $dqlAlias,
45 ));
46 }
47
48 public static function missingDiscriminatorMetaMappingColumn(string $entityName, string $discrColumnName, string $dqlAlias): self
49 {
50 return new self(sprintf(
51 'The meta mapping for the discriminator column "%s" is missing for "%s" using the DQL alias "%s".',
52 $discrColumnName,
53 $entityName,
54 $dqlAlias,
55 ));
56 }
57
58 /** @param list<int|string> $discrValues */
59 public static function invalidDiscriminatorValue(string $discrValue, array $discrValues): self
60 {
61 return new self(sprintf(
62 'The discriminator value "%s" is invalid. It must be one of "%s".',
63 $discrValue,
64 implode('", "', $discrValues),
65 ));
66 }
67}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php
new file mode 100644
index 0000000..d0fc101
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php
@@ -0,0 +1,586 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use BackedEnum;
8use Doctrine\Common\Collections\ArrayCollection;
9use Doctrine\ORM\Mapping\ClassMetadata;
10use Doctrine\ORM\PersistentCollection;
11use Doctrine\ORM\Query;
12use Doctrine\ORM\UnitOfWork;
13
14use function array_fill_keys;
15use function array_keys;
16use function array_map;
17use function assert;
18use function count;
19use function is_array;
20use function key;
21use function ltrim;
22use function spl_object_id;
23
24/**
25 * The ObjectHydrator constructs an object graph out of an SQL result set.
26 *
27 * Internal note: Highly performance-sensitive code.
28 */
29class ObjectHydrator extends AbstractHydrator
30{
31 /** @var mixed[] */
32 private array $identifierMap = [];
33
34 /** @var mixed[] */
35 private array $resultPointers = [];
36
37 /** @var mixed[] */
38 private array $idTemplate = [];
39
40 private int $resultCounter = 0;
41
42 /** @var mixed[] */
43 private array $rootAliases = [];
44
45 /** @var mixed[] */
46 private array $initializedCollections = [];
47
48 /** @var array<string, PersistentCollection> */
49 private array $uninitializedCollections = [];
50
51 /** @var mixed[] */
52 private array $existingCollections = [];
53
54 protected function prepare(): void
55 {
56 if (! isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD])) {
57 $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] = true;
58 }
59
60 foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) {
61 $this->identifierMap[$dqlAlias] = [];
62 $this->idTemplate[$dqlAlias] = '';
63
64 // Remember which associations are "fetch joined", so that we know where to inject
65 // collection stubs or proxies and where not.
66 if (! isset($this->resultSetMapping()->relationMap[$dqlAlias])) {
67 continue;
68 }
69
70 $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
71
72 if (! isset($this->resultSetMapping()->aliasMap[$parent])) {
73 throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent);
74 }
75
76 $sourceClassName = $this->resultSetMapping()->aliasMap[$parent];
77 $sourceClass = $this->getClassMetadata($sourceClassName);
78 $assoc = $sourceClass->associationMappings[$this->resultSetMapping()->relationMap[$dqlAlias]];
79
80 $this->hints['fetched'][$parent][$assoc->fieldName] = true;
81
82 if ($assoc->isManyToMany()) {
83 continue;
84 }
85
86 // Mark any non-collection opposite sides as fetched, too.
87 if (! $assoc->isOwningSide()) {
88 $this->hints['fetched'][$dqlAlias][$assoc->mappedBy] = true;
89
90 continue;
91 }
92
93 // handle fetch-joined owning side bi-directional one-to-one associations
94 if ($assoc->inversedBy !== null) {
95 $class = $this->getClassMetadata($className);
96 $inverseAssoc = $class->associationMappings[$assoc->inversedBy];
97
98 if (! $inverseAssoc->isToOne()) {
99 continue;
100 }
101
102 $this->hints['fetched'][$dqlAlias][$inverseAssoc->fieldName] = true;
103 }
104 }
105 }
106
107 protected function cleanup(): void
108 {
109 $eagerLoad = isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD]) && $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] === true;
110
111 parent::cleanup();
112
113 $this->identifierMap =
114 $this->initializedCollections =
115 $this->uninitializedCollections =
116 $this->existingCollections =
117 $this->resultPointers = [];
118
119 if ($eagerLoad) {
120 $this->uow->triggerEagerLoads();
121 }
122
123 $this->uow->hydrationComplete();
124 }
125
126 protected function cleanupAfterRowIteration(): void
127 {
128 $this->identifierMap =
129 $this->initializedCollections =
130 $this->uninitializedCollections =
131 $this->existingCollections =
132 $this->resultPointers = [];
133 }
134
135 /**
136 * {@inheritDoc}
137 */
138 protected function hydrateAllData(): array
139 {
140 $result = [];
141
142 while ($row = $this->statement()->fetchAssociative()) {
143 $this->hydrateRowData($row, $result);
144 }
145
146 // Take snapshots from all newly initialized collections
147 foreach ($this->initializedCollections as $coll) {
148 $coll->takeSnapshot();
149 }
150
151 foreach ($this->uninitializedCollections as $coll) {
152 if (! $coll->isInitialized()) {
153 $coll->setInitialized(true);
154 }
155 }
156
157 return $result;
158 }
159
160 /**
161 * Initializes a related collection.
162 *
163 * @param string $fieldName The name of the field on the entity that holds the collection.
164 * @param string $parentDqlAlias Alias of the parent fetch joining this collection.
165 */
166 private function initRelatedCollection(
167 object $entity,
168 ClassMetadata $class,
169 string $fieldName,
170 string $parentDqlAlias,
171 ): PersistentCollection {
172 $oid = spl_object_id($entity);
173 $relation = $class->associationMappings[$fieldName];
174 $value = $class->reflFields[$fieldName]->getValue($entity);
175
176 if ($value === null || is_array($value)) {
177 $value = new ArrayCollection((array) $value);
178 }
179
180 if (! $value instanceof PersistentCollection) {
181 assert($relation->isToMany());
182 $value = new PersistentCollection(
183 $this->em,
184 $this->metadataCache[$relation->targetEntity],
185 $value,
186 );
187 $value->setOwner($entity, $relation);
188
189 $class->reflFields[$fieldName]->setValue($entity, $value);
190 $this->uow->setOriginalEntityProperty($oid, $fieldName, $value);
191
192 $this->initializedCollections[$oid . $fieldName] = $value;
193 } elseif (
194 isset($this->hints[Query::HINT_REFRESH]) ||
195 isset($this->hints['fetched'][$parentDqlAlias][$fieldName]) &&
196 ! $value->isInitialized()
197 ) {
198 // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED!
199 $value->setDirty(false);
200 $value->setInitialized(true);
201 $value->unwrap()->clear();
202
203 $this->initializedCollections[$oid . $fieldName] = $value;
204 } else {
205 // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN!
206 $this->existingCollections[$oid . $fieldName] = $value;
207 }
208
209 return $value;
210 }
211
212 /**
213 * Gets an entity instance.
214 *
215 * @param string $dqlAlias The DQL alias of the entity's class.
216 * @psalm-param array<string, mixed> $data The instance data.
217 *
218 * @throws HydrationException
219 */
220 private function getEntity(array $data, string $dqlAlias): object
221 {
222 $className = $this->resultSetMapping()->aliasMap[$dqlAlias];
223
224 if (isset($this->resultSetMapping()->discriminatorColumns[$dqlAlias])) {
225 $fieldName = $this->resultSetMapping()->discriminatorColumns[$dqlAlias];
226
227 if (! isset($this->resultSetMapping()->metaMappings[$fieldName])) {
228 throw HydrationException::missingDiscriminatorMetaMappingColumn($className, $fieldName, $dqlAlias);
229 }
230
231 $discrColumn = $this->resultSetMapping()->metaMappings[$fieldName];
232
233 if (! isset($data[$discrColumn])) {
234 throw HydrationException::missingDiscriminatorColumn($className, $discrColumn, $dqlAlias);
235 }
236
237 if ($data[$discrColumn] === '') {
238 throw HydrationException::emptyDiscriminatorValue($dqlAlias);
239 }
240
241 $discrMap = $this->metadataCache[$className]->discriminatorMap;
242 $discriminatorValue = $data[$discrColumn];
243 if ($discriminatorValue instanceof BackedEnum) {
244 $discriminatorValue = $discriminatorValue->value;
245 }
246
247 $discriminatorValue = (string) $discriminatorValue;
248
249 if (! isset($discrMap[$discriminatorValue])) {
250 throw HydrationException::invalidDiscriminatorValue($discriminatorValue, array_keys($discrMap));
251 }
252
253 $className = $discrMap[$discriminatorValue];
254
255 unset($data[$discrColumn]);
256 }
257
258 if (isset($this->hints[Query::HINT_REFRESH_ENTITY], $this->rootAliases[$dqlAlias])) {
259 $this->registerManaged($this->metadataCache[$className], $this->hints[Query::HINT_REFRESH_ENTITY], $data);
260 }
261
262 $this->hints['fetchAlias'] = $dqlAlias;
263
264 return $this->uow->createEntity($className, $data, $this->hints);
265 }
266
267 /**
268 * @psalm-param class-string $className
269 * @psalm-param array<string, mixed> $data
270 */
271 private function getEntityFromIdentityMap(string $className, array $data): object|bool
272 {
273 // TODO: Abstract this code and UnitOfWork::createEntity() equivalent?
274 $class = $this->metadataCache[$className];
275
276 if ($class->isIdentifierComposite) {
277 $idHash = UnitOfWork::getIdHashByIdentifier(
278 array_map(
279 /** @return mixed */
280 static fn (string $fieldName) => isset($class->associationMappings[$fieldName]) && assert($class->associationMappings[$fieldName]->isToOneOwningSide())
281 ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name]
282 : $data[$fieldName],
283 $class->identifier,
284 ),
285 );
286
287 return $this->uow->tryGetByIdHash(ltrim($idHash), $class->rootEntityName);
288 } elseif (isset($class->associationMappings[$class->identifier[0]])) {
289 $association = $class->associationMappings[$class->identifier[0]];
290 assert($association->isToOneOwningSide());
291
292 return $this->uow->tryGetByIdHash($data[$association->joinColumns[0]->name], $class->rootEntityName);
293 }
294
295 return $this->uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName);
296 }
297
298 /**
299 * Hydrates a single row in an SQL result set.
300 *
301 * @internal
302 * First, the data of the row is split into chunks where each chunk contains data
303 * that belongs to a particular component/class. Afterwards, all these chunks
304 * are processed, one after the other. For each chunk of class data only one of the
305 * following code paths is executed:
306 * Path A: The data chunk belongs to a joined/associated object and the association
307 * is collection-valued.
308 * Path B: The data chunk belongs to a joined/associated object and the association
309 * is single-valued.
310 * Path C: The data chunk belongs to a root result element/object that appears in the topmost
311 * level of the hydrated result. A typical example are the objects of the type
312 * specified by the FROM clause in a DQL query.
313 *
314 * @param mixed[] $row The data of the row to process.
315 * @param mixed[] $result The result array to fill.
316 */
317 protected function hydrateRowData(array $row, array &$result): void
318 {
319 // Initialize
320 $id = $this->idTemplate; // initialize the id-memory
321 $nonemptyComponents = [];
322 // Split the row data into chunks of class data.
323 $rowData = $this->gatherRowData($row, $id, $nonemptyComponents);
324
325 // reset result pointers for each data row
326 $this->resultPointers = [];
327
328 // Hydrate the data chunks
329 foreach ($rowData['data'] as $dqlAlias => $data) {
330 $entityName = $this->resultSetMapping()->aliasMap[$dqlAlias];
331
332 if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) {
333 // It's a joined result
334
335 $parentAlias = $this->resultSetMapping()->parentAliasMap[$dqlAlias];
336 // we need the $path to save into the identifier map which entities were already
337 // seen for this parent-child relationship
338 $path = $parentAlias . '.' . $dqlAlias;
339
340 // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs
341 if (! isset($nonemptyComponents[$parentAlias])) {
342 // TODO: Add special case code where we hydrate the right join objects into identity map at least
343 continue;
344 }
345
346 $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parentAlias]];
347 $relationField = $this->resultSetMapping()->relationMap[$dqlAlias];
348 $relation = $parentClass->associationMappings[$relationField];
349 $reflField = $parentClass->reflFields[$relationField];
350
351 // Get a reference to the parent object to which the joined element belongs.
352 if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parentAlias])) {
353 $objectClass = $this->resultPointers[$parentAlias];
354 $parentObject = $objectClass[key($objectClass)];
355 } elseif (isset($this->resultPointers[$parentAlias])) {
356 $parentObject = $this->resultPointers[$parentAlias];
357 } else {
358 // Parent object of relation not found, mark as not-fetched again
359 if (isset($nonemptyComponents[$dqlAlias])) {
360 $element = $this->getEntity($data, $dqlAlias);
361
362 // Update result pointer and provide initial fetch data for parent
363 $this->resultPointers[$dqlAlias] = $element;
364 $rowData['data'][$parentAlias][$relationField] = $element;
365 } else {
366 $element = null;
367 }
368
369 // Mark as not-fetched again
370 unset($this->hints['fetched'][$parentAlias][$relationField]);
371 continue;
372 }
373
374 $oid = spl_object_id($parentObject);
375
376 // Check the type of the relation (many or single-valued)
377 if (! $relation->isToOne()) {
378 // PATH A: Collection-valued association
379 $reflFieldValue = $reflField->getValue($parentObject);
380
381 if (isset($nonemptyComponents[$dqlAlias])) {
382 $collKey = $oid . $relationField;
383 if (isset($this->initializedCollections[$collKey])) {
384 $reflFieldValue = $this->initializedCollections[$collKey];
385 } elseif (! isset($this->existingCollections[$collKey])) {
386 $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
387 }
388
389 $indexExists = isset($this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]);
390 $index = $indexExists ? $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false;
391 $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false;
392
393 if (! $indexExists || ! $indexIsValid) {
394 if (isset($this->existingCollections[$collKey])) {
395 // Collection exists, only look for the element in the identity map.
396 $element = $this->getEntityFromIdentityMap($entityName, $data);
397 if ($element) {
398 $this->resultPointers[$dqlAlias] = $element;
399 } else {
400 unset($this->resultPointers[$dqlAlias]);
401 }
402 } else {
403 $element = $this->getEntity($data, $dqlAlias);
404
405 if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) {
406 $indexValue = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]];
407 $reflFieldValue->hydrateSet($indexValue, $element);
408 $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue;
409 } else {
410 if (! $reflFieldValue->contains($element)) {
411 $reflFieldValue->hydrateAdd($element);
412 $reflFieldValue->last();
413 }
414
415 $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key();
416 }
417
418 // Update result pointer
419 $this->resultPointers[$dqlAlias] = $element;
420 }
421 } else {
422 // Update result pointer
423 $this->resultPointers[$dqlAlias] = $reflFieldValue[$index];
424 }
425 } elseif (! $reflFieldValue) {
426 $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias);
427 } elseif ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false && ! isset($this->uninitializedCollections[$oid . $relationField])) {
428 $this->uninitializedCollections[$oid . $relationField] = $reflFieldValue;
429 }
430 } else {
431 // PATH B: Single-valued association
432 $reflFieldValue = $reflField->getValue($parentObject);
433
434 if (! $reflFieldValue || isset($this->hints[Query::HINT_REFRESH]) || $this->uow->isUninitializedObject($reflFieldValue)) {
435 // we only need to take action if this value is null,
436 // we refresh the entity or its an uninitialized proxy.
437 if (isset($nonemptyComponents[$dqlAlias])) {
438 $element = $this->getEntity($data, $dqlAlias);
439 $reflField->setValue($parentObject, $element);
440 $this->uow->setOriginalEntityProperty($oid, $relationField, $element);
441 $targetClass = $this->metadataCache[$relation->targetEntity];
442
443 if ($relation->isOwningSide()) {
444 // TODO: Just check hints['fetched'] here?
445 // If there is an inverse mapping on the target class its bidirectional
446 if ($relation->inversedBy !== null) {
447 $inverseAssoc = $targetClass->associationMappings[$relation->inversedBy];
448 if ($inverseAssoc->isToOne()) {
449 $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject);
450 $this->uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc->fieldName, $parentObject);
451 }
452 }
453 } else {
454 // For sure bidirectional, as there is no inverse side in unidirectional mappings
455 $targetClass->reflFields[$relation->mappedBy]->setValue($element, $parentObject);
456 $this->uow->setOriginalEntityProperty(spl_object_id($element), $relation->mappedBy, $parentObject);
457 }
458
459 // Update result pointer
460 $this->resultPointers[$dqlAlias] = $element;
461 } else {
462 $this->uow->setOriginalEntityProperty($oid, $relationField, null);
463 $reflField->setValue($parentObject, null);
464 }
465 // else leave $reflFieldValue null for single-valued associations
466 } else {
467 // Update result pointer
468 $this->resultPointers[$dqlAlias] = $reflFieldValue;
469 }
470 }
471 } else {
472 // PATH C: Its a root result element
473 $this->rootAliases[$dqlAlias] = true; // Mark as root alias
474 $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0;
475
476 // if this row has a NULL value for the root result id then make it a null result.
477 if (! isset($nonemptyComponents[$dqlAlias])) {
478 if ($this->resultSetMapping()->isMixed) {
479 $result[] = [$entityKey => null];
480 } else {
481 $result[] = null;
482 }
483
484 $resultKey = $this->resultCounter;
485 ++$this->resultCounter;
486 continue;
487 }
488
489 // check for existing result from the iterations before
490 if (! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) {
491 $element = $this->getEntity($data, $dqlAlias);
492
493 if ($this->resultSetMapping()->isMixed) {
494 $element = [$entityKey => $element];
495 }
496
497 if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) {
498 $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]];
499
500 if (isset($this->hints['collection'])) {
501 $this->hints['collection']->hydrateSet($resultKey, $element);
502 }
503
504 $result[$resultKey] = $element;
505 } else {
506 $resultKey = $this->resultCounter;
507 ++$this->resultCounter;
508
509 if (isset($this->hints['collection'])) {
510 $this->hints['collection']->hydrateAdd($element);
511 }
512
513 $result[] = $element;
514 }
515
516 $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey;
517
518 // Update result pointer
519 $this->resultPointers[$dqlAlias] = $element;
520 } else {
521 // Update result pointer
522 $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]];
523 $this->resultPointers[$dqlAlias] = $result[$index];
524 $resultKey = $index;
525 }
526 }
527
528 if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) {
529 $this->uow->hydrationComplete();
530 }
531 }
532
533 if (! isset($resultKey)) {
534 $this->resultCounter++;
535 }
536
537 // Append scalar values to mixed result sets
538 if (isset($rowData['scalars'])) {
539 if (! isset($resultKey)) {
540 $resultKey = isset($this->resultSetMapping()->indexByMap['scalars'])
541 ? $row[$this->resultSetMapping()->indexByMap['scalars']]
542 : $this->resultCounter - 1;
543 }
544
545 foreach ($rowData['scalars'] as $name => $value) {
546 $result[$resultKey][$name] = $value;
547 }
548 }
549
550 // Append new object to mixed result sets
551 if (isset($rowData['newObjects'])) {
552 if (! isset($resultKey)) {
553 $resultKey = $this->resultCounter - 1;
554 }
555
556 $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0);
557
558 foreach ($rowData['newObjects'] as $objIndex => $newObject) {
559 $class = $newObject['class'];
560 $args = $newObject['args'];
561 $obj = $class->newInstanceArgs($args);
562
563 if ($scalarCount === 0 && count($rowData['newObjects']) === 1) {
564 $result[$resultKey] = $obj;
565
566 continue;
567 }
568
569 $result[$resultKey][$objIndex] = $obj;
570 }
571 }
572 }
573
574 /**
575 * When executed in a hydrate() loop we may have to clear internal state to
576 * decrease memory consumption.
577 */
578 public function onClear(mixed $eventArgs): void
579 {
580 parent::onClear($eventArgs);
581
582 $aliases = array_keys($this->identifierMap);
583
584 $this->identifierMap = array_fill_keys($aliases, []);
585 }
586}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php
new file mode 100644
index 0000000..0f10fb4
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php
@@ -0,0 +1,34 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use Doctrine\DBAL\Driver\Exception;
8use Doctrine\ORM\Exception\MultipleSelectorsFoundException;
9
10use function array_column;
11use function count;
12
13/**
14 * Hydrator that produces one-dimensional array.
15 */
16final class ScalarColumnHydrator extends AbstractHydrator
17{
18 /**
19 * {@inheritDoc}
20 *
21 * @throws MultipleSelectorsFoundException
22 * @throws Exception
23 */
24 protected function hydrateAllData(): array
25 {
26 if (count($this->resultSetMapping()->fieldMappings) > 1) {
27 throw MultipleSelectorsFoundException::create($this->resultSetMapping()->fieldMappings);
28 }
29
30 $result = $this->statement()->fetchAllNumeric();
31
32 return array_column($result, 0);
33 }
34}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php
new file mode 100644
index 0000000..15f3e7e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php
@@ -0,0 +1,35 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7/**
8 * Hydrator that produces flat, rectangular results of scalar data.
9 * The created result is almost the same as a regular SQL result set, except
10 * that column names are mapped to field names and data type conversions take place.
11 */
12class ScalarHydrator extends AbstractHydrator
13{
14 /**
15 * {@inheritDoc}
16 */
17 protected function hydrateAllData(): array
18 {
19 $result = [];
20
21 while ($data = $this->statement()->fetchAssociative()) {
22 $this->hydrateRowData($data, $result);
23 }
24
25 return $result;
26 }
27
28 /**
29 * {@inheritDoc}
30 */
31 protected function hydrateRowData(array $row, array &$result): void
32 {
33 $result[] = $this->gatherScalarRowData($row);
34 }
35}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php
new file mode 100644
index 0000000..eab7b9b
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php
@@ -0,0 +1,176 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use Doctrine\ORM\Internal\SQLResultCasing;
8use Doctrine\ORM\Mapping\ClassMetadata;
9use Doctrine\ORM\Mapping\MappingException;
10use Doctrine\ORM\Query;
11use Exception;
12use RuntimeException;
13use ValueError;
14
15use function array_keys;
16use function array_search;
17use function assert;
18use function count;
19use function in_array;
20use function key;
21use function reset;
22use function sprintf;
23
24class SimpleObjectHydrator extends AbstractHydrator
25{
26 use SQLResultCasing;
27
28 private ClassMetadata|null $class = null;
29
30 protected function prepare(): void
31 {
32 if (count($this->resultSetMapping()->aliasMap) !== 1) {
33 throw new RuntimeException('Cannot use SimpleObjectHydrator with a ResultSetMapping that contains more than one object result.');
34 }
35
36 if ($this->resultSetMapping()->scalarMappings) {
37 throw new RuntimeException('Cannot use SimpleObjectHydrator with a ResultSetMapping that contains scalar mappings.');
38 }
39
40 $this->class = $this->getClassMetadata(reset($this->resultSetMapping()->aliasMap));
41 }
42
43 protected function cleanup(): void
44 {
45 parent::cleanup();
46
47 $this->uow->triggerEagerLoads();
48 $this->uow->hydrationComplete();
49 }
50
51 /**
52 * {@inheritDoc}
53 */
54 protected function hydrateAllData(): array
55 {
56 $result = [];
57
58 while ($row = $this->statement()->fetchAssociative()) {
59 $this->hydrateRowData($row, $result);
60 }
61
62 $this->em->getUnitOfWork()->triggerEagerLoads();
63
64 return $result;
65 }
66
67 /**
68 * {@inheritDoc}
69 */
70 protected function hydrateRowData(array $row, array &$result): void
71 {
72 assert($this->class !== null);
73 $entityName = $this->class->name;
74 $data = [];
75 $discrColumnValue = null;
76
77 // We need to find the correct entity class name if we have inheritance in resultset
78 if ($this->class->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
79 $discrColumn = $this->class->getDiscriminatorColumn();
80 $discrColumnName = $this->getSQLResultCasing($this->platform, $discrColumn->name);
81
82 // Find mapped discriminator column from the result set.
83 $metaMappingDiscrColumnName = array_search($discrColumnName, $this->resultSetMapping()->metaMappings, true);
84 if ($metaMappingDiscrColumnName) {
85 $discrColumnName = $metaMappingDiscrColumnName;
86 }
87
88 if (! isset($row[$discrColumnName])) {
89 throw HydrationException::missingDiscriminatorColumn(
90 $entityName,
91 $discrColumnName,
92 key($this->resultSetMapping()->aliasMap),
93 );
94 }
95
96 if ($row[$discrColumnName] === '') {
97 throw HydrationException::emptyDiscriminatorValue(key(
98 $this->resultSetMapping()->aliasMap,
99 ));
100 }
101
102 $discrMap = $this->class->discriminatorMap;
103
104 if (! isset($discrMap[$row[$discrColumnName]])) {
105 throw HydrationException::invalidDiscriminatorValue($row[$discrColumnName], array_keys($discrMap));
106 }
107
108 $entityName = $discrMap[$row[$discrColumnName]];
109 $discrColumnValue = $row[$discrColumnName];
110
111 unset($row[$discrColumnName]);
112 }
113
114 foreach ($row as $column => $value) {
115 // An ObjectHydrator should be used instead of SimpleObjectHydrator
116 if (isset($this->resultSetMapping()->relationMap[$column])) {
117 throw new Exception(sprintf('Unable to retrieve association information for column "%s"', $column));
118 }
119
120 $cacheKeyInfo = $this->hydrateColumnInfo($column);
121
122 if (! $cacheKeyInfo) {
123 continue;
124 }
125
126 // If we have inheritance in resultset, make sure the field belongs to the correct class
127 if (isset($cacheKeyInfo['discriminatorValues']) && ! in_array((string) $discrColumnValue, $cacheKeyInfo['discriminatorValues'], true)) {
128 continue;
129 }
130
131 // Check if value is null before conversion (because some types convert null to something else)
132 $valueIsNull = $value === null;
133
134 // Convert field to a valid PHP value
135 if (isset($cacheKeyInfo['type'])) {
136 $type = $cacheKeyInfo['type'];
137 $value = $type->convertToPHPValue($value, $this->platform);
138 }
139
140 if ($value !== null && isset($cacheKeyInfo['enumType'])) {
141 $originalValue = $value;
142 try {
143 $value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']);
144 } catch (ValueError $e) {
145 throw MappingException::invalidEnumValue(
146 $entityName,
147 $cacheKeyInfo['fieldName'],
148 (string) $originalValue,
149 $cacheKeyInfo['enumType'],
150 $e,
151 );
152 }
153 }
154
155 $fieldName = $cacheKeyInfo['fieldName'];
156
157 // Prevent overwrite in case of inherit classes using same property name (See AbstractHydrator)
158 if (! isset($data[$fieldName]) || ! $valueIsNull) {
159 $data[$fieldName] = $value;
160 }
161 }
162
163 if (isset($this->hints[Query::HINT_REFRESH_ENTITY])) {
164 $this->registerManaged($this->class, $this->hints[Query::HINT_REFRESH_ENTITY], $data);
165 }
166
167 $uow = $this->em->getUnitOfWork();
168 $entity = $uow->createEntity($entityName, $data, $this->hints);
169
170 $result[] = $entity;
171
172 if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) {
173 $this->uow->hydrationComplete();
174 }
175 }
176}
diff --git a/vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php b/vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php
new file mode 100644
index 0000000..2787bbc
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php
@@ -0,0 +1,40 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\Hydration;
6
7use Doctrine\ORM\NonUniqueResultException;
8use Doctrine\ORM\NoResultException;
9
10use function array_shift;
11use function count;
12use function key;
13
14/**
15 * Hydrator that hydrates a single scalar value from the result set.
16 */
17class SingleScalarHydrator extends AbstractHydrator
18{
19 protected function hydrateAllData(): mixed
20 {
21 $data = $this->statement()->fetchAllAssociative();
22 $numRows = count($data);
23
24 if ($numRows === 0) {
25 throw new NoResultException();
26 }
27
28 if ($numRows > 1) {
29 throw new NonUniqueResultException('The query returned multiple rows. Change the query or use a different result function like getScalarResult().');
30 }
31
32 $result = $this->gatherScalarRowData($data[key($data)]);
33
34 if (count($result) > 1) {
35 throw new NonUniqueResultException('The query returned a row containing multiple columns. Change the query or use a different result function like getScalarResult().');
36 }
37
38 return array_shift($result);
39 }
40}
diff --git a/vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php b/vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php
new file mode 100644
index 0000000..e0fe342
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php
@@ -0,0 +1,64 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7use Doctrine\ORM\EntityManagerInterface;
8use Doctrine\ORM\Event\ListenersInvoker;
9use Doctrine\ORM\Event\PostLoadEventArgs;
10use Doctrine\ORM\Events;
11use Doctrine\ORM\Mapping\ClassMetadata;
12
13/**
14 * Class, which can handle completion of hydration cycle and produce some of tasks.
15 * In current implementation triggers deferred postLoad event.
16 */
17final class HydrationCompleteHandler
18{
19 /** @var mixed[][] */
20 private array $deferredPostLoadInvocations = [];
21
22 public function __construct(
23 private readonly ListenersInvoker $listenersInvoker,
24 private readonly EntityManagerInterface $em,
25 ) {
26 }
27
28 /**
29 * Method schedules invoking of postLoad entity to the very end of current hydration cycle.
30 */
31 public function deferPostLoadInvoking(ClassMetadata $class, object $entity): void
32 {
33 $invoke = $this->listenersInvoker->getSubscribedSystems($class, Events::postLoad);
34
35 if ($invoke === ListenersInvoker::INVOKE_NONE) {
36 return;
37 }
38
39 $this->deferredPostLoadInvocations[] = [$class, $invoke, $entity];
40 }
41
42 /**
43 * This method should be called after any hydration cycle completed.
44 *
45 * Method fires all deferred invocations of postLoad events
46 */
47 public function hydrationComplete(): void
48 {
49 $toInvoke = $this->deferredPostLoadInvocations;
50 $this->deferredPostLoadInvocations = [];
51
52 foreach ($toInvoke as $classAndEntity) {
53 [$class, $invoke, $entity] = $classAndEntity;
54
55 $this->listenersInvoker->invoke(
56 $class,
57 Events::postLoad,
58 $entity,
59 new PostLoadEventArgs($entity, $this->em),
60 $invoke,
61 );
62 }
63 }
64}
diff --git a/vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php b/vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php
new file mode 100644
index 0000000..7584744
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php
@@ -0,0 +1,55 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7use BadMethodCallException;
8
9use function array_filter;
10use function array_is_list;
11use function array_keys;
12use function array_values;
13use function assert;
14use function debug_backtrace;
15use function implode;
16use function is_string;
17use function sprintf;
18
19use const DEBUG_BACKTRACE_IGNORE_ARGS;
20
21/**
22 * Checks if a variadic parameter contains unexpected named arguments.
23 *
24 * @internal
25 */
26trait NoUnknownNamedArguments
27{
28 /**
29 * @param TItem[] $parameter
30 *
31 * @template TItem
32 * @psalm-assert list<TItem> $parameter
33 */
34 private static function validateVariadicParameter(array $parameter): void
35 {
36 if (array_is_list($parameter)) {
37 return;
38 }
39
40 [, $trace] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
41 assert(isset($trace['class']));
42
43 $additionalArguments = array_values(array_filter(
44 array_keys($parameter),
45 is_string(...),
46 ));
47
48 throw new BadMethodCallException(sprintf(
49 'Invalid call to %s::%s(), unknown named arguments: %s',
50 $trace['class'],
51 $trace['function'],
52 implode(', ', $additionalArguments),
53 ));
54 }
55}
diff --git a/vendor/doctrine/orm/src/Internal/QueryType.php b/vendor/doctrine/orm/src/Internal/QueryType.php
new file mode 100644
index 0000000..b5e60c7
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/QueryType.php
@@ -0,0 +1,13 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7/** @internal To be used inside the QueryBuilder only. */
8enum QueryType
9{
10 case Select;
11 case Delete;
12 case Update;
13}
diff --git a/vendor/doctrine/orm/src/Internal/SQLResultCasing.php b/vendor/doctrine/orm/src/Internal/SQLResultCasing.php
new file mode 100644
index 0000000..53b412e
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/SQLResultCasing.php
@@ -0,0 +1,30 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7use Doctrine\DBAL\Platforms\AbstractPlatform;
8use Doctrine\DBAL\Platforms\DB2Platform;
9use Doctrine\DBAL\Platforms\OraclePlatform;
10use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
11
12use function strtolower;
13use function strtoupper;
14
15/** @internal */
16trait SQLResultCasing
17{
18 private function getSQLResultCasing(AbstractPlatform $platform, string $column): string
19 {
20 if ($platform instanceof DB2Platform || $platform instanceof OraclePlatform) {
21 return strtoupper($column);
22 }
23
24 if ($platform instanceof PostgreSQLPlatform) {
25 return strtolower($column);
26 }
27
28 return $column;
29 }
30}
diff --git a/vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php b/vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php
new file mode 100644
index 0000000..dd4fc98
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php
@@ -0,0 +1,159 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7use InvalidArgumentException;
8
9use function array_keys;
10use function array_pop;
11use function array_push;
12use function min;
13use function spl_object_id;
14
15/**
16 * StronglyConnectedComponents implements Tarjan's algorithm to find strongly connected
17 * components (SCC) in a directed graph. This algorithm has a linear running time based on
18 * nodes (V) and edges between the nodes (E), resulting in a computational complexity
19 * of O(V + E).
20 *
21 * See https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
22 * for an explanation and the meaning of the DFS and lowlink numbers.
23 *
24 * @internal
25 */
26final class StronglyConnectedComponents
27{
28 private const NOT_VISITED = 1;
29 private const IN_PROGRESS = 2;
30 private const VISITED = 3;
31
32 /**
33 * Array of all nodes, indexed by object ids.
34 *
35 * @var array<int, object>
36 */
37 private array $nodes = [];
38
39 /**
40 * DFS state for the different nodes, indexed by node object id and using one of
41 * this class' constants as value.
42 *
43 * @var array<int, self::*>
44 */
45 private array $states = [];
46
47 /**
48 * Edges between the nodes. The first-level key is the object id of the outgoing
49 * node; the second array maps the destination node by object id as key.
50 *
51 * @var array<int, array<int, bool>>
52 */
53 private array $edges = [];
54
55 /**
56 * DFS numbers, by object ID
57 *
58 * @var array<int, int>
59 */
60 private array $dfs = [];
61
62 /**
63 * lowlink numbers, by object ID
64 *
65 * @var array<int, int>
66 */
67 private array $lowlink = [];
68
69 private int $maxdfs = 0;
70
71 /**
72 * Nodes representing the SCC another node is in, indexed by lookup-node object ID
73 *
74 * @var array<int, object>
75 */
76 private array $representingNodes = [];
77
78 /**
79 * Stack with OIDs of nodes visited in the current state of the DFS
80 *
81 * @var list<int>
82 */
83 private array $stack = [];
84
85 public function addNode(object $node): void
86 {
87 $id = spl_object_id($node);
88 $this->nodes[$id] = $node;
89 $this->states[$id] = self::NOT_VISITED;
90 $this->edges[$id] = [];
91 }
92
93 public function hasNode(object $node): bool
94 {
95 return isset($this->nodes[spl_object_id($node)]);
96 }
97
98 /**
99 * Adds a new edge between two nodes to the graph
100 */
101 public function addEdge(object $from, object $to): void
102 {
103 $fromId = spl_object_id($from);
104 $toId = spl_object_id($to);
105
106 $this->edges[$fromId][$toId] = true;
107 }
108
109 public function findStronglyConnectedComponents(): void
110 {
111 foreach (array_keys($this->nodes) as $oid) {
112 if ($this->states[$oid] === self::NOT_VISITED) {
113 $this->tarjan($oid);
114 }
115 }
116 }
117
118 private function tarjan(int $oid): void
119 {
120 $this->dfs[$oid] = $this->lowlink[$oid] = $this->maxdfs++;
121 $this->states[$oid] = self::IN_PROGRESS;
122 array_push($this->stack, $oid);
123
124 foreach ($this->edges[$oid] as $adjacentId => $ignored) {
125 if ($this->states[$adjacentId] === self::NOT_VISITED) {
126 $this->tarjan($adjacentId);
127 $this->lowlink[$oid] = min($this->lowlink[$oid], $this->lowlink[$adjacentId]);
128 } elseif ($this->states[$adjacentId] === self::IN_PROGRESS) {
129 $this->lowlink[$oid] = min($this->lowlink[$oid], $this->dfs[$adjacentId]);
130 }
131 }
132
133 $lowlink = $this->lowlink[$oid];
134 if ($lowlink === $this->dfs[$oid]) {
135 $representingNode = null;
136 do {
137 $unwindOid = array_pop($this->stack);
138
139 if (! $representingNode) {
140 $representingNode = $this->nodes[$unwindOid];
141 }
142
143 $this->representingNodes[$unwindOid] = $representingNode;
144 $this->states[$unwindOid] = self::VISITED;
145 } while ($unwindOid !== $oid);
146 }
147 }
148
149 public function getNodeRepresentingStronglyConnectedComponent(object $node): object
150 {
151 $oid = spl_object_id($node);
152
153 if (! isset($this->representingNodes[$oid])) {
154 throw new InvalidArgumentException('unknown node');
155 }
156
157 return $this->representingNodes[$oid];
158 }
159}
diff --git a/vendor/doctrine/orm/src/Internal/TopologicalSort.php b/vendor/doctrine/orm/src/Internal/TopologicalSort.php
new file mode 100644
index 0000000..808bc0f
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/TopologicalSort.php
@@ -0,0 +1,155 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal;
6
7use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
8
9use function array_keys;
10use function spl_object_id;
11
12/**
13 * TopologicalSort implements topological sorting, which is an ordering
14 * algorithm for directed graphs (DG) using a depth-first searching (DFS)
15 * to traverse the graph built in memory.
16 * This algorithm has a linear running time based on nodes (V) and edges
17 * between the nodes (E), resulting in a computational complexity of O(V + E).
18 *
19 * @internal
20 */
21final class TopologicalSort
22{
23 private const NOT_VISITED = 1;
24 private const IN_PROGRESS = 2;
25 private const VISITED = 3;
26
27 /**
28 * Array of all nodes, indexed by object ids.
29 *
30 * @var array<int, object>
31 */
32 private array $nodes = [];
33
34 /**
35 * DFS state for the different nodes, indexed by node object id and using one of
36 * this class' constants as value.
37 *
38 * @var array<int, self::*>
39 */
40 private array $states = [];
41
42 /**
43 * Edges between the nodes. The first-level key is the object id of the outgoing
44 * node; the second array maps the destination node by object id as key. The final
45 * boolean value indicates whether the edge is optional or not.
46 *
47 * @var array<int, array<int, bool>>
48 */
49 private array $edges = [];
50
51 /**
52 * Builds up the result during the DFS.
53 *
54 * @var list<object>
55 */
56 private array $sortResult = [];
57
58 public function addNode(object $node): void
59 {
60 $id = spl_object_id($node);
61 $this->nodes[$id] = $node;
62 $this->states[$id] = self::NOT_VISITED;
63 $this->edges[$id] = [];
64 }
65
66 public function hasNode(object $node): bool
67 {
68 return isset($this->nodes[spl_object_id($node)]);
69 }
70
71 /**
72 * Adds a new edge between two nodes to the graph
73 *
74 * @param bool $optional This indicates whether the edge may be ignored during the topological sort if it is necessary to break cycles.
75 */
76 public function addEdge(object $from, object $to, bool $optional): void
77 {
78 $fromId = spl_object_id($from);
79 $toId = spl_object_id($to);
80
81 if (isset($this->edges[$fromId][$toId]) && $this->edges[$fromId][$toId] === false) {
82 return; // we already know about this dependency, and it is not optional
83 }
84
85 $this->edges[$fromId][$toId] = $optional;
86 }
87
88 /**
89 * Returns a topological sort of all nodes. When we have an edge A->B between two nodes
90 * A and B, then B will be listed before A in the result. Visually speaking, when ordering
91 * the nodes in the result order from left to right, all edges point to the left.
92 *
93 * @return list<object>
94 */
95 public function sort(): array
96 {
97 foreach (array_keys($this->nodes) as $oid) {
98 if ($this->states[$oid] === self::NOT_VISITED) {
99 $this->visit($oid);
100 }
101 }
102
103 return $this->sortResult;
104 }
105
106 private function visit(int $oid): void
107 {
108 if ($this->states[$oid] === self::IN_PROGRESS) {
109 // This node is already on the current DFS stack. We've found a cycle!
110 throw new CycleDetectedException($this->nodes[$oid]);
111 }
112
113 if ($this->states[$oid] === self::VISITED) {
114 // We've reached a node that we've already seen, including all
115 // other nodes that are reachable from here. We're done here, return.
116 return;
117 }
118
119 $this->states[$oid] = self::IN_PROGRESS;
120
121 // Continue the DFS downwards the edge list
122 foreach ($this->edges[$oid] as $adjacentId => $optional) {
123 try {
124 $this->visit($adjacentId);
125 } catch (CycleDetectedException $exception) {
126 if ($exception->isCycleCollected()) {
127 // There is a complete cycle downstream of the current node. We cannot
128 // do anything about that anymore.
129 throw $exception;
130 }
131
132 if ($optional) {
133 // The current edge is part of a cycle, but it is optional and the closest
134 // such edge while backtracking. Break the cycle here by skipping the edge
135 // and continuing with the next one.
136 continue;
137 }
138
139 // We have found a cycle and cannot break it at $edge. Best we can do
140 // is to backtrack from the current vertex, hoping that somewhere up the
141 // stack this can be salvaged.
142 $this->states[$oid] = self::NOT_VISITED;
143 $exception->addToCycle($this->nodes[$oid]);
144
145 throw $exception;
146 }
147 }
148
149 // We have traversed all edges and visited all other nodes reachable from here.
150 // So we're done with this vertex as well.
151
152 $this->states[$oid] = self::VISITED;
153 $this->sortResult[] = $this->nodes[$oid];
154 }
155}
diff --git a/vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php b/vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php
new file mode 100644
index 0000000..3af5329
--- /dev/null
+++ b/vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php
@@ -0,0 +1,47 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Doctrine\ORM\Internal\TopologicalSort;
6
7use RuntimeException;
8
9use function array_unshift;
10
11class CycleDetectedException extends RuntimeException
12{
13 /** @var list<object> */
14 private array $cycle;
15
16 /**
17 * Do we have the complete cycle collected?
18 */
19 private bool $cycleCollected = false;
20
21 public function __construct(private readonly object $startNode)
22 {
23 parent::__construct('A cycle has been detected, so a topological sort is not possible. The getCycle() method provides the list of nodes that form the cycle.');
24
25 $this->cycle = [$startNode];
26 }
27
28 /** @return list<object> */
29 public function getCycle(): array
30 {
31 return $this->cycle;
32 }
33
34 public function addToCycle(object $node): void
35 {
36 array_unshift($this->cycle, $node);
37
38 if ($node === $this->startNode) {
39 $this->cycleCollected = true;
40 }
41 }
42
43 public function isCycleCollected(): bool
44 {
45 return $this->cycleCollected;
46 }
47}