From bf6655a534a6775d30cafa67bd801276bda1d98d Mon Sep 17 00:00:00 2001 From: polo Date: Tue, 13 Aug 2024 23:45:21 +0200 Subject: =?UTF-8?q?VERSION=200.2=20doctrine=20ORM=20et=20entit=C3=A9s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/Internal/Hydration/AbstractHydrator.php | 556 +++++++++++++++++++ .../orm/src/Internal/Hydration/ArrayHydrator.php | 270 ++++++++++ .../src/Internal/Hydration/HydrationException.php | 67 +++ .../orm/src/Internal/Hydration/ObjectHydrator.php | 586 +++++++++++++++++++++ .../Internal/Hydration/ScalarColumnHydrator.php | 34 ++ .../orm/src/Internal/Hydration/ScalarHydrator.php | 35 ++ .../Internal/Hydration/SimpleObjectHydrator.php | 176 +++++++ .../Internal/Hydration/SingleScalarHydrator.php | 40 ++ .../orm/src/Internal/HydrationCompleteHandler.php | 64 +++ .../orm/src/Internal/NoUnknownNamedArguments.php | 55 ++ vendor/doctrine/orm/src/Internal/QueryType.php | 13 + .../doctrine/orm/src/Internal/SQLResultCasing.php | 30 ++ .../src/Internal/StronglyConnectedComponents.php | 159 ++++++ .../doctrine/orm/src/Internal/TopologicalSort.php | 155 ++++++ .../TopologicalSort/CycleDetectedException.php | 47 ++ 15 files changed, 2287 insertions(+) create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/AbstractHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/ArrayHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/HydrationException.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/ObjectHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/ScalarColumnHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/ScalarHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/SimpleObjectHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/Hydration/SingleScalarHydrator.php create mode 100644 vendor/doctrine/orm/src/Internal/HydrationCompleteHandler.php create mode 100644 vendor/doctrine/orm/src/Internal/NoUnknownNamedArguments.php create mode 100644 vendor/doctrine/orm/src/Internal/QueryType.php create mode 100644 vendor/doctrine/orm/src/Internal/SQLResultCasing.php create mode 100644 vendor/doctrine/orm/src/Internal/StronglyConnectedComponents.php create mode 100644 vendor/doctrine/orm/src/Internal/TopologicalSort.php create mode 100644 vendor/doctrine/orm/src/Internal/TopologicalSort/CycleDetectedException.php (limited to 'vendor/doctrine/orm/src/Internal') 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 @@ +> + */ + protected array $metadataCache = []; + + /** + * The cache used during row-by-row hydration. + * + * @var array + */ + protected array $cache = []; + + /** + * The statement that provides the data to hydrate. + */ + protected Result|null $stmt = null; + + /** + * The query hints. + * + * @var array + */ + protected array $hints = []; + + /** + * Initializes a new instance of a class derived from AbstractHydrator. + */ + public function __construct(protected EntityManagerInterface $em) + { + $this->platform = $em->getConnection()->getDatabasePlatform(); + $this->uow = $em->getUnitOfWork(); + } + + /** + * Initiates a row-by-row hydration. + * + * @psalm-param array $hints + * + * @return Generator + * + * @final + */ + final public function toIterable(Result $stmt, ResultSetMapping $resultSetMapping, array $hints = []): Generator + { + $this->stmt = $stmt; + $this->rsm = $resultSetMapping; + $this->hints = $hints; + + $evm = $this->em->getEventManager(); + + $evm->addEventListener([Events::onClear], $this); + + $this->prepare(); + + try { + while (true) { + $row = $this->statement()->fetchAssociative(); + + if ($row === false) { + break; + } + + $result = []; + + $this->hydrateRowData($row, $result); + + $this->cleanupAfterRowIteration(); + if (count($result) === 1) { + if (count($resultSetMapping->indexByMap) === 0) { + yield end($result); + } else { + yield from $result; + } + } else { + yield $result; + } + } + } finally { + $this->cleanup(); + } + } + + final protected function statement(): Result + { + if ($this->stmt === null) { + throw new LogicException('Uninitialized _stmt property'); + } + + return $this->stmt; + } + + final protected function resultSetMapping(): ResultSetMapping + { + if ($this->rsm === null) { + throw new LogicException('Uninitialized _rsm property'); + } + + return $this->rsm; + } + + /** + * Hydrates all rows returned by the passed statement instance at once. + * + * @psalm-param array $hints + */ + public function hydrateAll(Result $stmt, ResultSetMapping $resultSetMapping, array $hints = []): mixed + { + $this->stmt = $stmt; + $this->rsm = $resultSetMapping; + $this->hints = $hints; + + $this->em->getEventManager()->addEventListener([Events::onClear], $this); + $this->prepare(); + + try { + $result = $this->hydrateAllData(); + } finally { + $this->cleanup(); + } + + return $result; + } + + /** + * When executed in a hydrate() loop we have to clear internal state to + * decrease memory consumption. + */ + public function onClear(mixed $eventArgs): void + { + } + + /** + * Executes one-time preparation tasks, once each time hydration is started + * through {@link hydrateAll} or {@link toIterable()}. + */ + protected function prepare(): void + { + } + + /** + * Executes one-time cleanup tasks at the end of a hydration that was initiated + * through {@link hydrateAll} or {@link toIterable()}. + */ + protected function cleanup(): void + { + $this->statement()->free(); + + $this->stmt = null; + $this->rsm = null; + $this->cache = []; + $this->metadataCache = []; + + $this + ->em + ->getEventManager() + ->removeEventListener([Events::onClear], $this); + } + + protected function cleanupAfterRowIteration(): void + { + } + + /** + * Hydrates a single row from the current statement instance. + * + * Template method. + * + * @param mixed[] $row The row data. + * @param mixed[] $result The result to fill. + * + * @throws HydrationException + */ + protected function hydrateRowData(array $row, array &$result): void + { + throw new HydrationException('hydrateRowData() not implemented by this hydrator.'); + } + + /** + * Hydrates all rows from the current statement instance at once. + */ + abstract protected function hydrateAllData(): mixed; + + /** + * Processes a row of the result set. + * + * Used for identity-based hydration (HYDRATE_OBJECT and HYDRATE_ARRAY). + * Puts the elements of a result row into a new array, grouped by the dql alias + * they belong to. The column names in the result set are mapped to their + * field names during this procedure as well as any necessary conversions on + * the values applied. Scalar values are kept in a specific key 'scalars'. + * + * @param mixed[] $data SQL Result Row. + * @psalm-param array $id Dql-Alias => ID-Hash. + * @psalm-param array $nonemptyComponents Does this DQL-Alias has at least one non NULL value? + * + * @return array> An array with all the fields + * (name => value) of the data + * row, grouped by their + * component alias. + * @psalm-return array{ + * data: array, + * newObjects?: array, + * scalars?: array + * } + */ + protected function gatherRowData(array $data, array &$id, array &$nonemptyComponents): array + { + $rowData = ['data' => []]; + + foreach ($data as $key => $value) { + $cacheKeyInfo = $this->hydrateColumnInfo($key); + if ($cacheKeyInfo === null) { + continue; + } + + $fieldName = $cacheKeyInfo['fieldName']; + + switch (true) { + case isset($cacheKeyInfo['isNewObjectParameter']): + $argIndex = $cacheKeyInfo['argIndex']; + $objIndex = $cacheKeyInfo['objIndex']; + $type = $cacheKeyInfo['type']; + $value = $type->convertToPHPValue($value, $this->platform); + + if ($value !== null && isset($cacheKeyInfo['enumType'])) { + $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); + } + + $rowData['newObjects'][$objIndex]['class'] = $cacheKeyInfo['class']; + $rowData['newObjects'][$objIndex]['args'][$argIndex] = $value; + break; + + case isset($cacheKeyInfo['isScalar']): + $type = $cacheKeyInfo['type']; + $value = $type->convertToPHPValue($value, $this->platform); + + if ($value !== null && isset($cacheKeyInfo['enumType'])) { + $value = $this->buildEnum($value, $cacheKeyInfo['enumType']); + } + + $rowData['scalars'][$fieldName] = $value; + + break; + + //case (isset($cacheKeyInfo['isMetaColumn'])): + default: + $dqlAlias = $cacheKeyInfo['dqlAlias']; + $type = $cacheKeyInfo['type']; + + // If there are field name collisions in the child class, then we need + // to only hydrate if we are looking at the correct discriminator value + if ( + isset($cacheKeyInfo['discriminatorColumn'], $data[$cacheKeyInfo['discriminatorColumn']]) + && ! in_array((string) $data[$cacheKeyInfo['discriminatorColumn']], $cacheKeyInfo['discriminatorValues'], true) + ) { + break; + } + + // in an inheritance hierarchy the same field could be defined several times. + // We overwrite this value so long we don't have a non-null value, that value we keep. + // Per definition it cannot be that a field is defined several times and has several values. + if (isset($rowData['data'][$dqlAlias][$fieldName])) { + break; + } + + $rowData['data'][$dqlAlias][$fieldName] = $type + ? $type->convertToPHPValue($value, $this->platform) + : $value; + + if ($rowData['data'][$dqlAlias][$fieldName] !== null && isset($cacheKeyInfo['enumType'])) { + $rowData['data'][$dqlAlias][$fieldName] = $this->buildEnum($rowData['data'][$dqlAlias][$fieldName], $cacheKeyInfo['enumType']); + } + + if ($cacheKeyInfo['isIdentifier'] && $value !== null) { + $id[$dqlAlias] .= '|' . $value; + $nonemptyComponents[$dqlAlias] = true; + } + + break; + } + } + + return $rowData; + } + + /** + * Processes a row of the result set. + * + * Used for HYDRATE_SCALAR. This is a variant of _gatherRowData() that + * simply converts column names to field names and properly converts the + * values according to their types. The resulting row has the same number + * of elements as before. + * + * @param mixed[] $data + * @psalm-param array $data + * + * @return mixed[] The processed row. + * @psalm-return array + */ + protected function gatherScalarRowData(array &$data): array + { + $rowData = []; + + foreach ($data as $key => $value) { + $cacheKeyInfo = $this->hydrateColumnInfo($key); + if ($cacheKeyInfo === null) { + continue; + } + + $fieldName = $cacheKeyInfo['fieldName']; + + // WARNING: BC break! We know this is the desired behavior to type convert values, but this + // erroneous behavior exists since 2.0 and we're forced to keep compatibility. + if (! isset($cacheKeyInfo['isScalar'])) { + $type = $cacheKeyInfo['type']; + $value = $type ? $type->convertToPHPValue($value, $this->platform) : $value; + + $fieldName = $cacheKeyInfo['dqlAlias'] . '_' . $fieldName; + } + + $rowData[$fieldName] = $value; + } + + return $rowData; + } + + /** + * Retrieve column information from ResultSetMapping. + * + * @param string $key Column name + * + * @return mixed[]|null + * @psalm-return array|null + */ + protected function hydrateColumnInfo(string $key): array|null + { + if (isset($this->cache[$key])) { + return $this->cache[$key]; + } + + switch (true) { + // NOTE: Most of the times it's a field mapping, so keep it first!!! + case isset($this->rsm->fieldMappings[$key]): + $classMetadata = $this->getClassMetadata($this->rsm->declaringClasses[$key]); + $fieldName = $this->rsm->fieldMappings[$key]; + $fieldMapping = $classMetadata->fieldMappings[$fieldName]; + $ownerMap = $this->rsm->columnOwnerMap[$key]; + $columnInfo = [ + 'isIdentifier' => in_array($fieldName, $classMetadata->identifier, true), + 'fieldName' => $fieldName, + 'type' => Type::getType($fieldMapping->type), + 'dqlAlias' => $ownerMap, + 'enumType' => $this->rsm->enumMappings[$key] ?? null, + ]; + + // the current discriminator value must be saved in order to disambiguate fields hydration, + // should there be field name collisions + if ($classMetadata->parentClasses && isset($this->rsm->discriminatorColumns[$ownerMap])) { + return $this->cache[$key] = array_merge( + $columnInfo, + [ + 'discriminatorColumn' => $this->rsm->discriminatorColumns[$ownerMap], + 'discriminatorValue' => $classMetadata->discriminatorValue, + 'discriminatorValues' => $this->getDiscriminatorValues($classMetadata), + ], + ); + } + + return $this->cache[$key] = $columnInfo; + + case isset($this->rsm->newObjectMappings[$key]): + // WARNING: A NEW object is also a scalar, so it must be declared before! + $mapping = $this->rsm->newObjectMappings[$key]; + + return $this->cache[$key] = [ + 'isScalar' => true, + 'isNewObjectParameter' => true, + 'fieldName' => $this->rsm->scalarMappings[$key], + 'type' => Type::getType($this->rsm->typeMappings[$key]), + 'argIndex' => $mapping['argIndex'], + 'objIndex' => $mapping['objIndex'], + 'class' => new ReflectionClass($mapping['className']), + 'enumType' => $this->rsm->enumMappings[$key] ?? null, + ]; + + case isset($this->rsm->scalarMappings[$key], $this->hints[LimitSubqueryWalker::FORCE_DBAL_TYPE_CONVERSION]): + return $this->cache[$key] = [ + 'fieldName' => $this->rsm->scalarMappings[$key], + 'type' => Type::getType($this->rsm->typeMappings[$key]), + 'dqlAlias' => '', + 'enumType' => $this->rsm->enumMappings[$key] ?? null, + ]; + + case isset($this->rsm->scalarMappings[$key]): + return $this->cache[$key] = [ + 'isScalar' => true, + 'fieldName' => $this->rsm->scalarMappings[$key], + 'type' => Type::getType($this->rsm->typeMappings[$key]), + 'enumType' => $this->rsm->enumMappings[$key] ?? null, + ]; + + case isset($this->rsm->metaMappings[$key]): + // Meta column (has meaning in relational schema only, i.e. foreign keys or discriminator columns). + $fieldName = $this->rsm->metaMappings[$key]; + $dqlAlias = $this->rsm->columnOwnerMap[$key]; + $type = isset($this->rsm->typeMappings[$key]) + ? Type::getType($this->rsm->typeMappings[$key]) + : null; + + // Cache metadata fetch + $this->getClassMetadata($this->rsm->aliasMap[$dqlAlias]); + + return $this->cache[$key] = [ + 'isIdentifier' => isset($this->rsm->isIdentifierColumn[$dqlAlias][$key]), + 'isMetaColumn' => true, + 'fieldName' => $fieldName, + 'type' => $type, + 'dqlAlias' => $dqlAlias, + 'enumType' => $this->rsm->enumMappings[$key] ?? null, + ]; + } + + // this column is a left over, maybe from a LIMIT query hack for example in Oracle or DB2 + // maybe from an additional column that has not been defined in a NativeQuery ResultSetMapping. + return null; + } + + /** + * @return string[] + * @psalm-return non-empty-list + */ + private function getDiscriminatorValues(ClassMetadata $classMetadata): array + { + $values = array_map( + fn (string $subClass): string => (string) $this->getClassMetadata($subClass)->discriminatorValue, + $classMetadata->subClasses, + ); + + $values[] = (string) $classMetadata->discriminatorValue; + + return $values; + } + + /** + * Retrieve ClassMetadata associated to entity class name. + */ + protected function getClassMetadata(string $className): ClassMetadata + { + if (! isset($this->metadataCache[$className])) { + $this->metadataCache[$className] = $this->em->getClassMetadata($className); + } + + return $this->metadataCache[$className]; + } + + /** + * Register entity as managed in UnitOfWork. + * + * @param mixed[] $data + * + * @todo The "$id" generation is the same of UnitOfWork#createEntity. Remove this duplication somehow + */ + protected function registerManaged(ClassMetadata $class, object $entity, array $data): void + { + if ($class->isIdentifierComposite) { + $id = []; + + foreach ($class->identifier as $fieldName) { + $id[$fieldName] = isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide() + ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name] + : $data[$fieldName]; + } + } else { + $fieldName = $class->identifier[0]; + $id = [ + $fieldName => isset($class->associationMappings[$fieldName]) && $class->associationMappings[$fieldName]->isToOneOwningSide() + ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name] + : $data[$fieldName], + ]; + } + + $this->em->getUnitOfWork()->registerManaged($entity, $id, $data); + } + + /** + * @param class-string $enumType + * + * @return BackedEnum|array + */ + final protected function buildEnum(mixed $value, string $enumType): BackedEnum|array + { + if (is_array($value)) { + return array_map( + static fn ($value) => $enumType::from($value), + $value, + ); + } + + return $enumType::from($value); + } +} 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 @@ + */ + private array $rootAliases = []; + + private bool $isSimpleQuery = false; + + /** @var mixed[] */ + private array $identifierMap = []; + + /** @var mixed[] */ + private array $resultPointers = []; + + /** @var array */ + private array $idTemplate = []; + + private int $resultCounter = 0; + + protected function prepare(): void + { + $this->isSimpleQuery = count($this->resultSetMapping()->aliasMap) <= 1; + + foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) { + $this->identifierMap[$dqlAlias] = []; + $this->resultPointers[$dqlAlias] = []; + $this->idTemplate[$dqlAlias] = ''; + } + } + + /** + * {@inheritDoc} + */ + protected function hydrateAllData(): array + { + $result = []; + + while ($data = $this->statement()->fetchAssociative()) { + $this->hydrateRowData($data, $result); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + protected function hydrateRowData(array $row, array &$result): void + { + // 1) Initialize + $id = $this->idTemplate; // initialize the id-memory + $nonemptyComponents = []; + $rowData = $this->gatherRowData($row, $id, $nonemptyComponents); + + // 2) Now hydrate the data found in the current row. + foreach ($rowData['data'] as $dqlAlias => $data) { + $index = false; + + if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) { + // It's a joined result + + $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; + $path = $parent . '.' . $dqlAlias; + + // missing parent data, skipping as RIGHT JOIN hydration is not supported. + if (! isset($nonemptyComponents[$parent])) { + continue; + } + + // Get a reference to the right element in the result tree. + // This element will get the associated element attached. + if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parent])) { + $first = reset($this->resultPointers); + // TODO: Exception if $key === null ? + $baseElement =& $this->resultPointers[$parent][key($first)]; + } elseif (isset($this->resultPointers[$parent])) { + $baseElement =& $this->resultPointers[$parent]; + } else { + unset($this->resultPointers[$dqlAlias]); // Ticket #1228 + + continue; + } + + $relationAlias = $this->resultSetMapping()->relationMap[$dqlAlias]; + $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parent]]; + $relation = $parentClass->associationMappings[$relationAlias]; + + // Check the type of the relation (many or single-valued) + if (! $relation->isToOne()) { + $oneToOne = false; + + if (! isset($baseElement[$relationAlias])) { + $baseElement[$relationAlias] = []; + } + + if (isset($nonemptyComponents[$dqlAlias])) { + $indexExists = isset($this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]]); + $index = $indexExists ? $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] : false; + $indexIsValid = $index !== false ? isset($baseElement[$relationAlias][$index]) : false; + + if (! $indexExists || ! $indexIsValid) { + $element = $data; + + if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { + $baseElement[$relationAlias][$row[$this->resultSetMapping()->indexByMap[$dqlAlias]]] = $element; + } else { + $baseElement[$relationAlias][] = $element; + } + + $this->identifierMap[$path][$id[$parent]][$id[$dqlAlias]] = array_key_last($baseElement[$relationAlias]); + } + } + } else { + $oneToOne = true; + + if ( + ! isset($nonemptyComponents[$dqlAlias]) && + ( ! isset($baseElement[$relationAlias])) + ) { + $baseElement[$relationAlias] = null; + } elseif (! isset($baseElement[$relationAlias])) { + $baseElement[$relationAlias] = $data; + } + } + + $coll =& $baseElement[$relationAlias]; + + if (is_array($coll)) { + $this->updateResultPointer($coll, $index, $dqlAlias, $oneToOne); + } + } else { + // It's a root result element + + $this->rootAliases[$dqlAlias] = true; // Mark as root + $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0; + + // if this row has a NULL value for the root result id then make it a null result. + if (! isset($nonemptyComponents[$dqlAlias])) { + $result[] = $this->resultSetMapping()->isMixed + ? [$entityKey => null] + : null; + + $resultKey = $this->resultCounter; + ++$this->resultCounter; + + continue; + } + + // Check for an existing element + if ($this->isSimpleQuery || ! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) { + $element = $this->resultSetMapping()->isMixed + ? [$entityKey => $data] + : $data; + + if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { + $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; + $result[$resultKey] = $element; + } else { + $resultKey = $this->resultCounter; + $result[] = $element; + + ++$this->resultCounter; + } + + $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; + } else { + $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]]; + $resultKey = $index; + } + + $this->updateResultPointer($result, $index, $dqlAlias, false); + } + } + + if (! isset($resultKey)) { + $this->resultCounter++; + } + + // Append scalar values to mixed result sets + if (isset($rowData['scalars'])) { + if (! isset($resultKey)) { + // this only ever happens when no object is fetched (scalar result only) + $resultKey = isset($this->resultSetMapping()->indexByMap['scalars']) + ? $row[$this->resultSetMapping()->indexByMap['scalars']] + : $this->resultCounter - 1; + } + + foreach ($rowData['scalars'] as $name => $value) { + $result[$resultKey][$name] = $value; + } + } + + // Append new object to mixed result sets + if (isset($rowData['newObjects'])) { + if (! isset($resultKey)) { + $resultKey = $this->resultCounter - 1; + } + + $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0); + + foreach ($rowData['newObjects'] as $objIndex => $newObject) { + $class = $newObject['class']; + $args = $newObject['args']; + $obj = $class->newInstanceArgs($args); + + if (count($args) === $scalarCount || ($scalarCount === 0 && count($rowData['newObjects']) === 1)) { + $result[$resultKey] = $obj; + + continue; + } + + $result[$resultKey][$objIndex] = $obj; + } + } + } + + /** + * Updates the result pointer for an Entity. The result pointers point to the + * last seen instance of each Entity type. This is used for graph construction. + * + * @param mixed[]|null $coll The element. + * @param string|int|false $index Index of the element in the collection. + * @param bool $oneToOne Whether it is a single-valued association or not. + */ + private function updateResultPointer( + array|null &$coll, + string|int|false $index, + string $dqlAlias, + bool $oneToOne, + ): void { + if ($coll === null) { + unset($this->resultPointers[$dqlAlias]); // Ticket #1228 + + return; + } + + if ($oneToOne) { + $this->resultPointers[$dqlAlias] =& $coll; + + return; + } + + if ($index !== false) { + $this->resultPointers[$dqlAlias] =& $coll[$index]; + + return; + } + + if (! $coll) { + return; + } + + $this->resultPointers[$dqlAlias] =& $coll[array_key_last($coll)]; + } +} 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 @@ + $discrValues */ + public static function invalidDiscriminatorValue(string $discrValue, array $discrValues): self + { + return new self(sprintf( + 'The discriminator value "%s" is invalid. It must be one of "%s".', + $discrValue, + implode('", "', $discrValues), + )); + } +} 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 @@ + */ + private array $uninitializedCollections = []; + + /** @var mixed[] */ + private array $existingCollections = []; + + protected function prepare(): void + { + if (! isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD])) { + $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] = true; + } + + foreach ($this->resultSetMapping()->aliasMap as $dqlAlias => $className) { + $this->identifierMap[$dqlAlias] = []; + $this->idTemplate[$dqlAlias] = ''; + + // Remember which associations are "fetch joined", so that we know where to inject + // collection stubs or proxies and where not. + if (! isset($this->resultSetMapping()->relationMap[$dqlAlias])) { + continue; + } + + $parent = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; + + if (! isset($this->resultSetMapping()->aliasMap[$parent])) { + throw HydrationException::parentObjectOfRelationNotFound($dqlAlias, $parent); + } + + $sourceClassName = $this->resultSetMapping()->aliasMap[$parent]; + $sourceClass = $this->getClassMetadata($sourceClassName); + $assoc = $sourceClass->associationMappings[$this->resultSetMapping()->relationMap[$dqlAlias]]; + + $this->hints['fetched'][$parent][$assoc->fieldName] = true; + + if ($assoc->isManyToMany()) { + continue; + } + + // Mark any non-collection opposite sides as fetched, too. + if (! $assoc->isOwningSide()) { + $this->hints['fetched'][$dqlAlias][$assoc->mappedBy] = true; + + continue; + } + + // handle fetch-joined owning side bi-directional one-to-one associations + if ($assoc->inversedBy !== null) { + $class = $this->getClassMetadata($className); + $inverseAssoc = $class->associationMappings[$assoc->inversedBy]; + + if (! $inverseAssoc->isToOne()) { + continue; + } + + $this->hints['fetched'][$dqlAlias][$inverseAssoc->fieldName] = true; + } + } + } + + protected function cleanup(): void + { + $eagerLoad = isset($this->hints[UnitOfWork::HINT_DEFEREAGERLOAD]) && $this->hints[UnitOfWork::HINT_DEFEREAGERLOAD] === true; + + parent::cleanup(); + + $this->identifierMap = + $this->initializedCollections = + $this->uninitializedCollections = + $this->existingCollections = + $this->resultPointers = []; + + if ($eagerLoad) { + $this->uow->triggerEagerLoads(); + } + + $this->uow->hydrationComplete(); + } + + protected function cleanupAfterRowIteration(): void + { + $this->identifierMap = + $this->initializedCollections = + $this->uninitializedCollections = + $this->existingCollections = + $this->resultPointers = []; + } + + /** + * {@inheritDoc} + */ + protected function hydrateAllData(): array + { + $result = []; + + while ($row = $this->statement()->fetchAssociative()) { + $this->hydrateRowData($row, $result); + } + + // Take snapshots from all newly initialized collections + foreach ($this->initializedCollections as $coll) { + $coll->takeSnapshot(); + } + + foreach ($this->uninitializedCollections as $coll) { + if (! $coll->isInitialized()) { + $coll->setInitialized(true); + } + } + + return $result; + } + + /** + * Initializes a related collection. + * + * @param string $fieldName The name of the field on the entity that holds the collection. + * @param string $parentDqlAlias Alias of the parent fetch joining this collection. + */ + private function initRelatedCollection( + object $entity, + ClassMetadata $class, + string $fieldName, + string $parentDqlAlias, + ): PersistentCollection { + $oid = spl_object_id($entity); + $relation = $class->associationMappings[$fieldName]; + $value = $class->reflFields[$fieldName]->getValue($entity); + + if ($value === null || is_array($value)) { + $value = new ArrayCollection((array) $value); + } + + if (! $value instanceof PersistentCollection) { + assert($relation->isToMany()); + $value = new PersistentCollection( + $this->em, + $this->metadataCache[$relation->targetEntity], + $value, + ); + $value->setOwner($entity, $relation); + + $class->reflFields[$fieldName]->setValue($entity, $value); + $this->uow->setOriginalEntityProperty($oid, $fieldName, $value); + + $this->initializedCollections[$oid . $fieldName] = $value; + } elseif ( + isset($this->hints[Query::HINT_REFRESH]) || + isset($this->hints['fetched'][$parentDqlAlias][$fieldName]) && + ! $value->isInitialized() + ) { + // Is already PersistentCollection, but either REFRESH or FETCH-JOIN and UNINITIALIZED! + $value->setDirty(false); + $value->setInitialized(true); + $value->unwrap()->clear(); + + $this->initializedCollections[$oid . $fieldName] = $value; + } else { + // Is already PersistentCollection, and DON'T REFRESH or FETCH-JOIN! + $this->existingCollections[$oid . $fieldName] = $value; + } + + return $value; + } + + /** + * Gets an entity instance. + * + * @param string $dqlAlias The DQL alias of the entity's class. + * @psalm-param array $data The instance data. + * + * @throws HydrationException + */ + private function getEntity(array $data, string $dqlAlias): object + { + $className = $this->resultSetMapping()->aliasMap[$dqlAlias]; + + if (isset($this->resultSetMapping()->discriminatorColumns[$dqlAlias])) { + $fieldName = $this->resultSetMapping()->discriminatorColumns[$dqlAlias]; + + if (! isset($this->resultSetMapping()->metaMappings[$fieldName])) { + throw HydrationException::missingDiscriminatorMetaMappingColumn($className, $fieldName, $dqlAlias); + } + + $discrColumn = $this->resultSetMapping()->metaMappings[$fieldName]; + + if (! isset($data[$discrColumn])) { + throw HydrationException::missingDiscriminatorColumn($className, $discrColumn, $dqlAlias); + } + + if ($data[$discrColumn] === '') { + throw HydrationException::emptyDiscriminatorValue($dqlAlias); + } + + $discrMap = $this->metadataCache[$className]->discriminatorMap; + $discriminatorValue = $data[$discrColumn]; + if ($discriminatorValue instanceof BackedEnum) { + $discriminatorValue = $discriminatorValue->value; + } + + $discriminatorValue = (string) $discriminatorValue; + + if (! isset($discrMap[$discriminatorValue])) { + throw HydrationException::invalidDiscriminatorValue($discriminatorValue, array_keys($discrMap)); + } + + $className = $discrMap[$discriminatorValue]; + + unset($data[$discrColumn]); + } + + if (isset($this->hints[Query::HINT_REFRESH_ENTITY], $this->rootAliases[$dqlAlias])) { + $this->registerManaged($this->metadataCache[$className], $this->hints[Query::HINT_REFRESH_ENTITY], $data); + } + + $this->hints['fetchAlias'] = $dqlAlias; + + return $this->uow->createEntity($className, $data, $this->hints); + } + + /** + * @psalm-param class-string $className + * @psalm-param array $data + */ + private function getEntityFromIdentityMap(string $className, array $data): object|bool + { + // TODO: Abstract this code and UnitOfWork::createEntity() equivalent? + $class = $this->metadataCache[$className]; + + if ($class->isIdentifierComposite) { + $idHash = UnitOfWork::getIdHashByIdentifier( + array_map( + /** @return mixed */ + static fn (string $fieldName) => isset($class->associationMappings[$fieldName]) && assert($class->associationMappings[$fieldName]->isToOneOwningSide()) + ? $data[$class->associationMappings[$fieldName]->joinColumns[0]->name] + : $data[$fieldName], + $class->identifier, + ), + ); + + return $this->uow->tryGetByIdHash(ltrim($idHash), $class->rootEntityName); + } elseif (isset($class->associationMappings[$class->identifier[0]])) { + $association = $class->associationMappings[$class->identifier[0]]; + assert($association->isToOneOwningSide()); + + return $this->uow->tryGetByIdHash($data[$association->joinColumns[0]->name], $class->rootEntityName); + } + + return $this->uow->tryGetByIdHash($data[$class->identifier[0]], $class->rootEntityName); + } + + /** + * Hydrates a single row in an SQL result set. + * + * @internal + * First, the data of the row is split into chunks where each chunk contains data + * that belongs to a particular component/class. Afterwards, all these chunks + * are processed, one after the other. For each chunk of class data only one of the + * following code paths is executed: + * Path A: The data chunk belongs to a joined/associated object and the association + * is collection-valued. + * Path B: The data chunk belongs to a joined/associated object and the association + * is single-valued. + * Path C: The data chunk belongs to a root result element/object that appears in the topmost + * level of the hydrated result. A typical example are the objects of the type + * specified by the FROM clause in a DQL query. + * + * @param mixed[] $row The data of the row to process. + * @param mixed[] $result The result array to fill. + */ + protected function hydrateRowData(array $row, array &$result): void + { + // Initialize + $id = $this->idTemplate; // initialize the id-memory + $nonemptyComponents = []; + // Split the row data into chunks of class data. + $rowData = $this->gatherRowData($row, $id, $nonemptyComponents); + + // reset result pointers for each data row + $this->resultPointers = []; + + // Hydrate the data chunks + foreach ($rowData['data'] as $dqlAlias => $data) { + $entityName = $this->resultSetMapping()->aliasMap[$dqlAlias]; + + if (isset($this->resultSetMapping()->parentAliasMap[$dqlAlias])) { + // It's a joined result + + $parentAlias = $this->resultSetMapping()->parentAliasMap[$dqlAlias]; + // we need the $path to save into the identifier map which entities were already + // seen for this parent-child relationship + $path = $parentAlias . '.' . $dqlAlias; + + // We have a RIGHT JOIN result here. Doctrine cannot hydrate RIGHT JOIN Object-Graphs + if (! isset($nonemptyComponents[$parentAlias])) { + // TODO: Add special case code where we hydrate the right join objects into identity map at least + continue; + } + + $parentClass = $this->metadataCache[$this->resultSetMapping()->aliasMap[$parentAlias]]; + $relationField = $this->resultSetMapping()->relationMap[$dqlAlias]; + $relation = $parentClass->associationMappings[$relationField]; + $reflField = $parentClass->reflFields[$relationField]; + + // Get a reference to the parent object to which the joined element belongs. + if ($this->resultSetMapping()->isMixed && isset($this->rootAliases[$parentAlias])) { + $objectClass = $this->resultPointers[$parentAlias]; + $parentObject = $objectClass[key($objectClass)]; + } elseif (isset($this->resultPointers[$parentAlias])) { + $parentObject = $this->resultPointers[$parentAlias]; + } else { + // Parent object of relation not found, mark as not-fetched again + if (isset($nonemptyComponents[$dqlAlias])) { + $element = $this->getEntity($data, $dqlAlias); + + // Update result pointer and provide initial fetch data for parent + $this->resultPointers[$dqlAlias] = $element; + $rowData['data'][$parentAlias][$relationField] = $element; + } else { + $element = null; + } + + // Mark as not-fetched again + unset($this->hints['fetched'][$parentAlias][$relationField]); + continue; + } + + $oid = spl_object_id($parentObject); + + // Check the type of the relation (many or single-valued) + if (! $relation->isToOne()) { + // PATH A: Collection-valued association + $reflFieldValue = $reflField->getValue($parentObject); + + if (isset($nonemptyComponents[$dqlAlias])) { + $collKey = $oid . $relationField; + if (isset($this->initializedCollections[$collKey])) { + $reflFieldValue = $this->initializedCollections[$collKey]; + } elseif (! isset($this->existingCollections[$collKey])) { + $reflFieldValue = $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias); + } + + $indexExists = isset($this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]]); + $index = $indexExists ? $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] : false; + $indexIsValid = $index !== false ? isset($reflFieldValue[$index]) : false; + + if (! $indexExists || ! $indexIsValid) { + if (isset($this->existingCollections[$collKey])) { + // Collection exists, only look for the element in the identity map. + $element = $this->getEntityFromIdentityMap($entityName, $data); + if ($element) { + $this->resultPointers[$dqlAlias] = $element; + } else { + unset($this->resultPointers[$dqlAlias]); + } + } else { + $element = $this->getEntity($data, $dqlAlias); + + if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { + $indexValue = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; + $reflFieldValue->hydrateSet($indexValue, $element); + $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $indexValue; + } else { + if (! $reflFieldValue->contains($element)) { + $reflFieldValue->hydrateAdd($element); + $reflFieldValue->last(); + } + + $this->identifierMap[$path][$id[$parentAlias]][$id[$dqlAlias]] = $reflFieldValue->key(); + } + + // Update result pointer + $this->resultPointers[$dqlAlias] = $element; + } + } else { + // Update result pointer + $this->resultPointers[$dqlAlias] = $reflFieldValue[$index]; + } + } elseif (! $reflFieldValue) { + $this->initRelatedCollection($parentObject, $parentClass, $relationField, $parentAlias); + } elseif ($reflFieldValue instanceof PersistentCollection && $reflFieldValue->isInitialized() === false && ! isset($this->uninitializedCollections[$oid . $relationField])) { + $this->uninitializedCollections[$oid . $relationField] = $reflFieldValue; + } + } else { + // PATH B: Single-valued association + $reflFieldValue = $reflField->getValue($parentObject); + + if (! $reflFieldValue || isset($this->hints[Query::HINT_REFRESH]) || $this->uow->isUninitializedObject($reflFieldValue)) { + // we only need to take action if this value is null, + // we refresh the entity or its an uninitialized proxy. + if (isset($nonemptyComponents[$dqlAlias])) { + $element = $this->getEntity($data, $dqlAlias); + $reflField->setValue($parentObject, $element); + $this->uow->setOriginalEntityProperty($oid, $relationField, $element); + $targetClass = $this->metadataCache[$relation->targetEntity]; + + if ($relation->isOwningSide()) { + // TODO: Just check hints['fetched'] here? + // If there is an inverse mapping on the target class its bidirectional + if ($relation->inversedBy !== null) { + $inverseAssoc = $targetClass->associationMappings[$relation->inversedBy]; + if ($inverseAssoc->isToOne()) { + $targetClass->reflFields[$inverseAssoc->fieldName]->setValue($element, $parentObject); + $this->uow->setOriginalEntityProperty(spl_object_id($element), $inverseAssoc->fieldName, $parentObject); + } + } + } else { + // For sure bidirectional, as there is no inverse side in unidirectional mappings + $targetClass->reflFields[$relation->mappedBy]->setValue($element, $parentObject); + $this->uow->setOriginalEntityProperty(spl_object_id($element), $relation->mappedBy, $parentObject); + } + + // Update result pointer + $this->resultPointers[$dqlAlias] = $element; + } else { + $this->uow->setOriginalEntityProperty($oid, $relationField, null); + $reflField->setValue($parentObject, null); + } + // else leave $reflFieldValue null for single-valued associations + } else { + // Update result pointer + $this->resultPointers[$dqlAlias] = $reflFieldValue; + } + } + } else { + // PATH C: Its a root result element + $this->rootAliases[$dqlAlias] = true; // Mark as root alias + $entityKey = $this->resultSetMapping()->entityMappings[$dqlAlias] ?: 0; + + // if this row has a NULL value for the root result id then make it a null result. + if (! isset($nonemptyComponents[$dqlAlias])) { + if ($this->resultSetMapping()->isMixed) { + $result[] = [$entityKey => null]; + } else { + $result[] = null; + } + + $resultKey = $this->resultCounter; + ++$this->resultCounter; + continue; + } + + // check for existing result from the iterations before + if (! isset($this->identifierMap[$dqlAlias][$id[$dqlAlias]])) { + $element = $this->getEntity($data, $dqlAlias); + + if ($this->resultSetMapping()->isMixed) { + $element = [$entityKey => $element]; + } + + if (isset($this->resultSetMapping()->indexByMap[$dqlAlias])) { + $resultKey = $row[$this->resultSetMapping()->indexByMap[$dqlAlias]]; + + if (isset($this->hints['collection'])) { + $this->hints['collection']->hydrateSet($resultKey, $element); + } + + $result[$resultKey] = $element; + } else { + $resultKey = $this->resultCounter; + ++$this->resultCounter; + + if (isset($this->hints['collection'])) { + $this->hints['collection']->hydrateAdd($element); + } + + $result[] = $element; + } + + $this->identifierMap[$dqlAlias][$id[$dqlAlias]] = $resultKey; + + // Update result pointer + $this->resultPointers[$dqlAlias] = $element; + } else { + // Update result pointer + $index = $this->identifierMap[$dqlAlias][$id[$dqlAlias]]; + $this->resultPointers[$dqlAlias] = $result[$index]; + $resultKey = $index; + } + } + + if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) { + $this->uow->hydrationComplete(); + } + } + + if (! isset($resultKey)) { + $this->resultCounter++; + } + + // Append scalar values to mixed result sets + if (isset($rowData['scalars'])) { + if (! isset($resultKey)) { + $resultKey = isset($this->resultSetMapping()->indexByMap['scalars']) + ? $row[$this->resultSetMapping()->indexByMap['scalars']] + : $this->resultCounter - 1; + } + + foreach ($rowData['scalars'] as $name => $value) { + $result[$resultKey][$name] = $value; + } + } + + // Append new object to mixed result sets + if (isset($rowData['newObjects'])) { + if (! isset($resultKey)) { + $resultKey = $this->resultCounter - 1; + } + + $scalarCount = (isset($rowData['scalars']) ? count($rowData['scalars']) : 0); + + foreach ($rowData['newObjects'] as $objIndex => $newObject) { + $class = $newObject['class']; + $args = $newObject['args']; + $obj = $class->newInstanceArgs($args); + + if ($scalarCount === 0 && count($rowData['newObjects']) === 1) { + $result[$resultKey] = $obj; + + continue; + } + + $result[$resultKey][$objIndex] = $obj; + } + } + } + + /** + * When executed in a hydrate() loop we may have to clear internal state to + * decrease memory consumption. + */ + public function onClear(mixed $eventArgs): void + { + parent::onClear($eventArgs); + + $aliases = array_keys($this->identifierMap); + + $this->identifierMap = array_fill_keys($aliases, []); + } +} 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 @@ +resultSetMapping()->fieldMappings) > 1) { + throw MultipleSelectorsFoundException::create($this->resultSetMapping()->fieldMappings); + } + + $result = $this->statement()->fetchAllNumeric(); + + return array_column($result, 0); + } +} 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 @@ +statement()->fetchAssociative()) { + $this->hydrateRowData($data, $result); + } + + return $result; + } + + /** + * {@inheritDoc} + */ + protected function hydrateRowData(array $row, array &$result): void + { + $result[] = $this->gatherScalarRowData($row); + } +} 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 @@ +resultSetMapping()->aliasMap) !== 1) { + throw new RuntimeException('Cannot use SimpleObjectHydrator with a ResultSetMapping that contains more than one object result.'); + } + + if ($this->resultSetMapping()->scalarMappings) { + throw new RuntimeException('Cannot use SimpleObjectHydrator with a ResultSetMapping that contains scalar mappings.'); + } + + $this->class = $this->getClassMetadata(reset($this->resultSetMapping()->aliasMap)); + } + + protected function cleanup(): void + { + parent::cleanup(); + + $this->uow->triggerEagerLoads(); + $this->uow->hydrationComplete(); + } + + /** + * {@inheritDoc} + */ + protected function hydrateAllData(): array + { + $result = []; + + while ($row = $this->statement()->fetchAssociative()) { + $this->hydrateRowData($row, $result); + } + + $this->em->getUnitOfWork()->triggerEagerLoads(); + + return $result; + } + + /** + * {@inheritDoc} + */ + protected function hydrateRowData(array $row, array &$result): void + { + assert($this->class !== null); + $entityName = $this->class->name; + $data = []; + $discrColumnValue = null; + + // We need to find the correct entity class name if we have inheritance in resultset + if ($this->class->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { + $discrColumn = $this->class->getDiscriminatorColumn(); + $discrColumnName = $this->getSQLResultCasing($this->platform, $discrColumn->name); + + // Find mapped discriminator column from the result set. + $metaMappingDiscrColumnName = array_search($discrColumnName, $this->resultSetMapping()->metaMappings, true); + if ($metaMappingDiscrColumnName) { + $discrColumnName = $metaMappingDiscrColumnName; + } + + if (! isset($row[$discrColumnName])) { + throw HydrationException::missingDiscriminatorColumn( + $entityName, + $discrColumnName, + key($this->resultSetMapping()->aliasMap), + ); + } + + if ($row[$discrColumnName] === '') { + throw HydrationException::emptyDiscriminatorValue(key( + $this->resultSetMapping()->aliasMap, + )); + } + + $discrMap = $this->class->discriminatorMap; + + if (! isset($discrMap[$row[$discrColumnName]])) { + throw HydrationException::invalidDiscriminatorValue($row[$discrColumnName], array_keys($discrMap)); + } + + $entityName = $discrMap[$row[$discrColumnName]]; + $discrColumnValue = $row[$discrColumnName]; + + unset($row[$discrColumnName]); + } + + foreach ($row as $column => $value) { + // An ObjectHydrator should be used instead of SimpleObjectHydrator + if (isset($this->resultSetMapping()->relationMap[$column])) { + throw new Exception(sprintf('Unable to retrieve association information for column "%s"', $column)); + } + + $cacheKeyInfo = $this->hydrateColumnInfo($column); + + if (! $cacheKeyInfo) { + continue; + } + + // If we have inheritance in resultset, make sure the field belongs to the correct class + if (isset($cacheKeyInfo['discriminatorValues']) && ! in_array((string) $discrColumnValue, $cacheKeyInfo['discriminatorValues'], true)) { + continue; + } + + // Check if value is null before conversion (because some types convert null to something else) + $valueIsNull = $value === null; + + // Convert field to a valid PHP value + if (isset($cacheKeyInfo['type'])) { + $type = $cacheKeyInfo['type']; + $value = $type->convertToPHPValue($value, $this->platform); + } + + if ($value !== null && isset($cacheKeyInfo['enumType'])) { + $originalValue = $value; + try { + $value = $this->buildEnum($originalValue, $cacheKeyInfo['enumType']); + } catch (ValueError $e) { + throw MappingException::invalidEnumValue( + $entityName, + $cacheKeyInfo['fieldName'], + (string) $originalValue, + $cacheKeyInfo['enumType'], + $e, + ); + } + } + + $fieldName = $cacheKeyInfo['fieldName']; + + // Prevent overwrite in case of inherit classes using same property name (See AbstractHydrator) + if (! isset($data[$fieldName]) || ! $valueIsNull) { + $data[$fieldName] = $value; + } + } + + if (isset($this->hints[Query::HINT_REFRESH_ENTITY])) { + $this->registerManaged($this->class, $this->hints[Query::HINT_REFRESH_ENTITY], $data); + } + + $uow = $this->em->getUnitOfWork(); + $entity = $uow->createEntity($entityName, $data, $this->hints); + + $result[] = $entity; + + if (isset($this->hints[Query::HINT_INTERNAL_ITERATION]) && $this->hints[Query::HINT_INTERNAL_ITERATION]) { + $this->uow->hydrationComplete(); + } + } +} 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 @@ +statement()->fetchAllAssociative(); + $numRows = count($data); + + if ($numRows === 0) { + throw new NoResultException(); + } + + if ($numRows > 1) { + throw new NonUniqueResultException('The query returned multiple rows. Change the query or use a different result function like getScalarResult().'); + } + + $result = $this->gatherScalarRowData($data[key($data)]); + + if (count($result) > 1) { + throw new NonUniqueResultException('The query returned a row containing multiple columns. Change the query or use a different result function like getScalarResult().'); + } + + return array_shift($result); + } +} 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 @@ +listenersInvoker->getSubscribedSystems($class, Events::postLoad); + + if ($invoke === ListenersInvoker::INVOKE_NONE) { + return; + } + + $this->deferredPostLoadInvocations[] = [$class, $invoke, $entity]; + } + + /** + * This method should be called after any hydration cycle completed. + * + * Method fires all deferred invocations of postLoad events + */ + public function hydrationComplete(): void + { + $toInvoke = $this->deferredPostLoadInvocations; + $this->deferredPostLoadInvocations = []; + + foreach ($toInvoke as $classAndEntity) { + [$class, $invoke, $entity] = $classAndEntity; + + $this->listenersInvoker->invoke( + $class, + Events::postLoad, + $entity, + new PostLoadEventArgs($entity, $this->em), + $invoke, + ); + } + } +} 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 @@ + $parameter + */ + private static function validateVariadicParameter(array $parameter): void + { + if (array_is_list($parameter)) { + return; + } + + [, $trace] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + assert(isset($trace['class'])); + + $additionalArguments = array_values(array_filter( + array_keys($parameter), + is_string(...), + )); + + throw new BadMethodCallException(sprintf( + 'Invalid call to %s::%s(), unknown named arguments: %s', + $trace['class'], + $trace['function'], + implode(', ', $additionalArguments), + )); + } +} 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 @@ + + */ + private array $nodes = []; + + /** + * DFS state for the different nodes, indexed by node object id and using one of + * this class' constants as value. + * + * @var array + */ + private array $states = []; + + /** + * Edges between the nodes. The first-level key is the object id of the outgoing + * node; the second array maps the destination node by object id as key. + * + * @var array> + */ + private array $edges = []; + + /** + * DFS numbers, by object ID + * + * @var array + */ + private array $dfs = []; + + /** + * lowlink numbers, by object ID + * + * @var array + */ + private array $lowlink = []; + + private int $maxdfs = 0; + + /** + * Nodes representing the SCC another node is in, indexed by lookup-node object ID + * + * @var array + */ + private array $representingNodes = []; + + /** + * Stack with OIDs of nodes visited in the current state of the DFS + * + * @var list + */ + private array $stack = []; + + public function addNode(object $node): void + { + $id = spl_object_id($node); + $this->nodes[$id] = $node; + $this->states[$id] = self::NOT_VISITED; + $this->edges[$id] = []; + } + + public function hasNode(object $node): bool + { + return isset($this->nodes[spl_object_id($node)]); + } + + /** + * Adds a new edge between two nodes to the graph + */ + public function addEdge(object $from, object $to): void + { + $fromId = spl_object_id($from); + $toId = spl_object_id($to); + + $this->edges[$fromId][$toId] = true; + } + + public function findStronglyConnectedComponents(): void + { + foreach (array_keys($this->nodes) as $oid) { + if ($this->states[$oid] === self::NOT_VISITED) { + $this->tarjan($oid); + } + } + } + + private function tarjan(int $oid): void + { + $this->dfs[$oid] = $this->lowlink[$oid] = $this->maxdfs++; + $this->states[$oid] = self::IN_PROGRESS; + array_push($this->stack, $oid); + + foreach ($this->edges[$oid] as $adjacentId => $ignored) { + if ($this->states[$adjacentId] === self::NOT_VISITED) { + $this->tarjan($adjacentId); + $this->lowlink[$oid] = min($this->lowlink[$oid], $this->lowlink[$adjacentId]); + } elseif ($this->states[$adjacentId] === self::IN_PROGRESS) { + $this->lowlink[$oid] = min($this->lowlink[$oid], $this->dfs[$adjacentId]); + } + } + + $lowlink = $this->lowlink[$oid]; + if ($lowlink === $this->dfs[$oid]) { + $representingNode = null; + do { + $unwindOid = array_pop($this->stack); + + if (! $representingNode) { + $representingNode = $this->nodes[$unwindOid]; + } + + $this->representingNodes[$unwindOid] = $representingNode; + $this->states[$unwindOid] = self::VISITED; + } while ($unwindOid !== $oid); + } + } + + public function getNodeRepresentingStronglyConnectedComponent(object $node): object + { + $oid = spl_object_id($node); + + if (! isset($this->representingNodes[$oid])) { + throw new InvalidArgumentException('unknown node'); + } + + return $this->representingNodes[$oid]; + } +} 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 @@ + + */ + private array $nodes = []; + + /** + * DFS state for the different nodes, indexed by node object id and using one of + * this class' constants as value. + * + * @var array + */ + private array $states = []; + + /** + * Edges between the nodes. The first-level key is the object id of the outgoing + * node; the second array maps the destination node by object id as key. The final + * boolean value indicates whether the edge is optional or not. + * + * @var array> + */ + private array $edges = []; + + /** + * Builds up the result during the DFS. + * + * @var list + */ + private array $sortResult = []; + + public function addNode(object $node): void + { + $id = spl_object_id($node); + $this->nodes[$id] = $node; + $this->states[$id] = self::NOT_VISITED; + $this->edges[$id] = []; + } + + public function hasNode(object $node): bool + { + return isset($this->nodes[spl_object_id($node)]); + } + + /** + * Adds a new edge between two nodes to the graph + * + * @param bool $optional This indicates whether the edge may be ignored during the topological sort if it is necessary to break cycles. + */ + public function addEdge(object $from, object $to, bool $optional): void + { + $fromId = spl_object_id($from); + $toId = spl_object_id($to); + + if (isset($this->edges[$fromId][$toId]) && $this->edges[$fromId][$toId] === false) { + return; // we already know about this dependency, and it is not optional + } + + $this->edges[$fromId][$toId] = $optional; + } + + /** + * Returns a topological sort of all nodes. When we have an edge A->B between two nodes + * A and B, then B will be listed before A in the result. Visually speaking, when ordering + * the nodes in the result order from left to right, all edges point to the left. + * + * @return list + */ + public function sort(): array + { + foreach (array_keys($this->nodes) as $oid) { + if ($this->states[$oid] === self::NOT_VISITED) { + $this->visit($oid); + } + } + + return $this->sortResult; + } + + private function visit(int $oid): void + { + if ($this->states[$oid] === self::IN_PROGRESS) { + // This node is already on the current DFS stack. We've found a cycle! + throw new CycleDetectedException($this->nodes[$oid]); + } + + if ($this->states[$oid] === self::VISITED) { + // We've reached a node that we've already seen, including all + // other nodes that are reachable from here. We're done here, return. + return; + } + + $this->states[$oid] = self::IN_PROGRESS; + + // Continue the DFS downwards the edge list + foreach ($this->edges[$oid] as $adjacentId => $optional) { + try { + $this->visit($adjacentId); + } catch (CycleDetectedException $exception) { + if ($exception->isCycleCollected()) { + // There is a complete cycle downstream of the current node. We cannot + // do anything about that anymore. + throw $exception; + } + + if ($optional) { + // The current edge is part of a cycle, but it is optional and the closest + // such edge while backtracking. Break the cycle here by skipping the edge + // and continuing with the next one. + continue; + } + + // We have found a cycle and cannot break it at $edge. Best we can do + // is to backtrack from the current vertex, hoping that somewhere up the + // stack this can be salvaged. + $this->states[$oid] = self::NOT_VISITED; + $exception->addToCycle($this->nodes[$oid]); + + throw $exception; + } + } + + // We have traversed all edges and visited all other nodes reachable from here. + // So we're done with this vertex as well. + + $this->states[$oid] = self::VISITED; + $this->sortResult[] = $this->nodes[$oid]; + } +} 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 @@ + */ + private array $cycle; + + /** + * Do we have the complete cycle collected? + */ + private bool $cycleCollected = false; + + public function __construct(private readonly object $startNode) + { + 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.'); + + $this->cycle = [$startNode]; + } + + /** @return list */ + public function getCycle(): array + { + return $this->cycle; + } + + public function addToCycle(object $node): void + { + array_unshift($this->cycle, $node); + + if ($node === $this->startNode) { + $this->cycleCollected = true; + } + } + + public function isCycleCollected(): bool + { + return $this->cycleCollected; + } +} -- cgit v1.2.3