From 0c6b8496f15c17261ca049173e0162bbf5da6379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Mon, 7 Oct 2024 20:32:49 +0200 Subject: [PATCH 1/4] EntityPreloader: extract getCommonAncestor() --- src/EntityPreloader.php | 50 +++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/src/EntityPreloader.php b/src/EntityPreloader.php index cbed82f..61d33d4 100644 --- a/src/EntityPreloader.php +++ b/src/EntityPreloader.php @@ -44,30 +44,12 @@ public function preload( ?int $maxFetchJoinSameFieldCount = null, ): array { - $sourceEntitiesCommonAncestor = null; - - foreach ($sourceEntities as $sourceEntity) { - $sourceEntityClassName = $sourceEntity::class; - - if ($sourceEntitiesCommonAncestor === null) { - $sourceEntitiesCommonAncestor = $sourceEntityClassName; - continue; - } - - while (!is_a($sourceEntityClassName, $sourceEntitiesCommonAncestor, true)) { - $sourceEntitiesCommonAncestor = get_parent_class($sourceEntitiesCommonAncestor); - - if ($sourceEntitiesCommonAncestor === false) { - throw new LogicException('Source entities must have a common ancestor'); - } - } - } + $sourceEntitiesCommonAncestor = $this->getCommonAncestor($sourceEntities); if ($sourceEntitiesCommonAncestor === null) { return []; } - /** @var ClassMetadata $sourceClassMetadata */ $sourceClassMetadata = $this->entityManager->getClassMetadata($sourceEntitiesCommonAncestor); $associationMapping = $sourceClassMetadata->getAssociationMapping($sourcePropertyName); /** @var ClassMetadata $targetClassMetadata */ @@ -89,6 +71,36 @@ public function preload( }; } + /** + * @param list $entities + * @return class-string|null + * @template S of E + */ + private function getCommonAncestor(array $entities): ?string + { + $commonAncestor = null; + + foreach ($entities as $entity) { + $entityClassName = $entity::class; + + if ($commonAncestor === null) { + $commonAncestor = $entityClassName; + continue; + } + + while (!is_a($entityClassName, $commonAncestor, true)) { + /** @var class-string|false $commonAncestor */ + $commonAncestor = get_parent_class($commonAncestor); + + if ($commonAncestor === false) { + throw new LogicException('Given entities must have a common ancestor'); + } + } + } + + return $commonAncestor; + } + /** * @param ClassMetadata $sourceClassMetadata * @param list $sourceEntities From 1ddde37e23c05ad26e8c6a300bc3d2ef83b807fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Mon, 7 Oct 2024 20:34:52 +0200 Subject: [PATCH 2/4] EntityPreloader: refactor loadProxies() to be usable in preloadToOne() --- src/EntityPreloader.php | 69 ++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 39 deletions(-) diff --git a/src/EntityPreloader.php b/src/EntityPreloader.php index 61d33d4..0810675 100644 --- a/src/EntityPreloader.php +++ b/src/EntityPreloader.php @@ -59,9 +59,9 @@ public function preload( throw new LogicException('Preloading of indexed associations is not supported'); } - $maxFetchJoinSameFieldCount ??= 1; - $this->loadProxies($sourceClassMetadata, $sourceEntities, $batchSize, $maxFetchJoinSameFieldCount); + $maxFetchJoinSameFieldCount ??= 1; + $sourceEntities = $this->loadProxies($sourceClassMetadata, $sourceEntities, $batchSize ?? self::BATCH_SIZE, $maxFetchJoinSameFieldCount); return match ($associationMapping->type()) { ClassMetadata::ONE_TO_MANY => $this->preloadOneToMany($sourceEntities, $sourceClassMetadata, $sourcePropertyName, $targetClassMetadata, $batchSize, $maxFetchJoinSameFieldCount), @@ -102,38 +102,45 @@ private function getCommonAncestor(array $entities): ?string } /** - * @param ClassMetadata $sourceClassMetadata - * @param list $sourceEntities - * @param positive-int|null $batchSize + * @param ClassMetadata $classMetadata + * @param list $entities + * @param positive-int $batchSize * @param non-negative-int $maxFetchJoinSameFieldCount + * @return list * @template T of E */ private function loadProxies( - ClassMetadata $sourceClassMetadata, - array $sourceEntities, - ?int $batchSize, + ClassMetadata $classMetadata, + array $entities, + int $batchSize, int $maxFetchJoinSameFieldCount, - ): void + ): array { - $sourceIdentifierReflection = $sourceClassMetadata->getSingleIdReflectionProperty(); + $identifierReflection = $classMetadata->getSingleIdReflectionProperty(); // e.g. Order::$id reflection + $identifierName = $classMetadata->getSingleIdentifierFieldName(); // e.g. 'id' - if ($sourceIdentifierReflection === null) { + if ($identifierReflection === null) { throw new LogicException('Doctrine should use RuntimeReflectionService which never returns null.'); } - $proxyIds = []; + $uniqueEntities = []; + $uninitializedIds = []; + + foreach ($entities as $entity) { + $entityId = $identifierReflection->getValue($entity); + $entityKey = (string) $entityId; + $uniqueEntities[$entityKey] = $entity; - foreach ($sourceEntities as $sourceEntity) { - if ($sourceEntity instanceof Proxy && !$sourceEntity->__isInitialized()) { - $proxyIds[] = $sourceIdentifierReflection->getValue($sourceEntity); + if ($entity instanceof Proxy && !$entity->__isInitialized()) { + $uninitializedIds[$entityKey] = $entityId; } } - $batchSize ??= self::PRELOAD_COLLECTION_DEFAULT_BATCH_SIZE; - - foreach (array_chunk($proxyIds, $batchSize) as $idsChunk) { - $this->loadEntitiesBy($sourceClassMetadata, $sourceIdentifierReflection->getName(), $idsChunk, $maxFetchJoinSameFieldCount); + foreach (array_chunk($uninitializedIds, $batchSize) as $idsChunk) { + $this->loadEntitiesBy($classMetadata, $identifierName, $idsChunk, $maxFetchJoinSameFieldCount); } + + return array_values($uniqueEntities); } /** @@ -226,19 +233,12 @@ private function preloadToOne( ): array { $sourcePropertyReflection = $sourceClassMetadata->getReflectionProperty($sourcePropertyName); // e.g. Item::$order reflection - $targetIdentifierReflection = $targetClassMetadata->getSingleIdReflectionProperty(); // e.g. Order::$id reflection + $targetEntities = []; - if ($sourcePropertyReflection === null || $targetIdentifierReflection === null) { + if ($sourcePropertyReflection === null) { throw new LogicException('Doctrine should use RuntimeReflectionService which never returns null.'); } - $targetIdentifierName = $targetClassMetadata->getSingleIdentifierFieldName(); // e.g. 'id' - - $batchSize ??= self::BATCH_SIZE; - - $targetEntities = []; - $uninitializedIds = []; - foreach ($sourceEntities as $sourceEntity) { $targetEntity = $sourcePropertyReflection->getValue($sourceEntity); @@ -246,19 +246,10 @@ private function preloadToOne( continue; } - $targetEntityId = (string) $targetIdentifierReflection->getValue($targetEntity); - $targetEntities[$targetEntityId] = $targetEntity; - - if ($targetEntity instanceof Proxy && !$targetEntity->__isInitialized()) { - $uninitializedIds[$targetEntityId] = true; - } - } - - foreach (array_chunk(array_keys($uninitializedIds), $batchSize) as $idsChunk) { - $this->loadEntitiesBy($targetClassMetadata, $targetIdentifierName, $idsChunk, $maxFetchJoinSameFieldCount); + $targetEntities[] = $targetEntity; } - return array_values($targetEntities); + return $this->loadProxies($targetClassMetadata, $targetEntities, $batchSize ?? self::BATCH_SIZE, $maxFetchJoinSameFieldCount); } /** From a2fdb47911ac938da561d938ea390cea0e319c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Mon, 7 Oct 2024 20:35:13 +0200 Subject: [PATCH 3/4] EntityPreloader: disallow ordered associations --- src/EntityPreloader.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/EntityPreloader.php b/src/EntityPreloader.php index 0810675..254fbc3 100644 --- a/src/EntityPreloader.php +++ b/src/EntityPreloader.php @@ -52,6 +52,7 @@ public function preload( $sourceClassMetadata = $this->entityManager->getClassMetadata($sourceEntitiesCommonAncestor); $associationMapping = $sourceClassMetadata->getAssociationMapping($sourcePropertyName); + /** @var ClassMetadata $targetClassMetadata */ $targetClassMetadata = $this->entityManager->getClassMetadata($associationMapping->targetEntity); @@ -59,6 +60,9 @@ public function preload( throw new LogicException('Preloading of indexed associations is not supported'); } + if ($associationMapping->isOrdered()) { + throw new LogicException('Preloading of ordered associations is not supported'); + } $maxFetchJoinSameFieldCount ??= 1; $sourceEntities = $this->loadProxies($sourceClassMetadata, $sourceEntities, $batchSize ?? self::BATCH_SIZE, $maxFetchJoinSameFieldCount); From 82c490b1cf840d45c4ff4fea20aad413f53cfbf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Mon, 7 Oct 2024 20:35:36 +0200 Subject: [PATCH 4/4] QueryLogger: improve getAggregatedQueries() --- tests/Lib/QueryLogger.php | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/tests/Lib/QueryLogger.php b/tests/Lib/QueryLogger.php index 58b2adb..f0d9771 100644 --- a/tests/Lib/QueryLogger.php +++ b/tests/Lib/QueryLogger.php @@ -84,29 +84,32 @@ static function (string $query) use ( /** * @return list */ - public function getAggregatedQueries(): array + public function getAggregatedQueries( + bool $omitSelectedColumns = true, + bool $omitDiscriminatorConditions = true, + bool $multiline = false, + ): array { - $queries = $this->getQueries(); + $queries = $this->getQueries( + omitSelectedColumns: $omitSelectedColumns, + omitDiscriminatorConditions: $omitDiscriminatorConditions, + multiline: $multiline, + ); $aggregatedQueries = []; foreach ($queries as $query) { - $found = false; - foreach ($aggregatedQueries as &$aggregatedQuery) { if ($aggregatedQuery['query'] === $query) { $aggregatedQuery['count']++; - $found = true; - break; + continue 2; } } - if (!$found) { - $aggregatedQueries[] = [ - 'count' => 1, - 'query' => $query, - ]; - } + $aggregatedQueries[] = [ + 'count' => 1, + 'query' => $query, + ]; } return $aggregatedQueries;