diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 126a50f..02eb0be 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -33,7 +33,7 @@ jobs: fail-fast: false matrix: php-version: [ '8.1', '8.2', '8.3', '8.4' ] - doctrine-version: [ '^2.19', '^3.2' ] + doctrine-version: [ '^2.19', '^3' ] dependency-version: [ prefer-lowest, prefer-stable ] steps: - diff --git a/composer.json b/composer.json index 31d3919..6719de9 100644 --- a/composer.json +++ b/composer.json @@ -6,11 +6,12 @@ ], "require": { "php": "^8.1", + "doctrine/dbal": "^3.7 || ^4.0", "doctrine/orm": "^2.19.7 || ^3.2" }, "require-dev": { + "composer/semver": "^3.0", "doctrine/collections": "^2.2", - "doctrine/dbal": "^3.9 || ^4.0", "doctrine/persistence": "^3.3", "editorconfig-checker/editorconfig-checker": "^10.6.0", "ergebnis/composer-normalize": "^2.42.0", @@ -60,7 +61,7 @@ "check:dependencies": "composer-dependency-analyser", "check:ec": "ec src tests", "check:tests": "phpunit tests", - "check:types": "phpstan analyse -vvv", + "check:types": "phpstan analyse -vv --ansi", "fix:cs": "phpcbf" } } diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0b333e3..054a522 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -10,6 +10,9 @@ parameters: paths: - src - tests + excludePaths: + analyse: + - tests/Fixtures/Compat checkMissingCallableSignature: true checkUninitializedProperties: true checkTooWideReturnTypesInProtectedAndPublicMethods: true @@ -29,15 +32,15 @@ parameters: identifier: 'identical.alwaysFalse' reportUnmatched: false path: 'src/EntityPreloader.php' + - + identifier: shipmonk.defaultMatchArmWithEnum + reportUnmatched: false # only new dbal issue + path: 'src/EntityPreloader.php' - message: '#Result of \|\| is always false#' identifier: 'booleanOr.alwaysFalse' reportUnmatched: false path: 'src/EntityPreloader.php' - - - message: '#has an uninitialized property \$id#' - identifier: 'property.uninitialized' - path: 'tests/Fixtures/Blog' - identifier: 'property.onlyWritten' path: 'tests/Fixtures/Synthetic' diff --git a/src/EntityPreloader.php b/src/EntityPreloader.php index 0d7143e..c9d59ed 100644 --- a/src/EntityPreloader.php +++ b/src/EntityPreloader.php @@ -3,6 +3,9 @@ namespace ShipMonk\DoctrineEntityPreloader; use ArrayAccess; +use Doctrine\DBAL\ArrayParameterType; +use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Types\Type; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\PersistentCollection; @@ -141,7 +144,7 @@ private function loadProxies( } foreach (array_chunk($uninitializedIds, $batchSize) as $idsChunk) { - $this->loadEntitiesBy($classMetadata, $identifierName, $idsChunk, $maxFetchJoinSameFieldCount); + $this->loadEntitiesBy($classMetadata, $identifierName, $classMetadata, $idsChunk, $maxFetchJoinSameFieldCount); } return array_values($uniqueEntities); @@ -270,6 +273,7 @@ private function preloadOneToManyInner( $targetEntitiesList = $this->loadEntitiesBy( $targetClassMetadata, $targetPropertyName, + $sourceClassMetadata, $uninitializedSourceEntityIdsChunk, $maxFetchJoinSameFieldCount, $associationMapping['orderBy'] ?? [], @@ -318,12 +322,18 @@ private function preloadManyToManyInner( $sourceIdentifierName = $sourceClassMetadata->getSingleIdentifierFieldName(); $targetIdentifierName = $targetClassMetadata->getSingleIdentifierFieldName(); + $sourceIdentifierType = $this->getIdentifierFieldType($sourceClassMetadata); + $manyToManyRows = $this->entityManager->createQueryBuilder() ->select("source.{$sourceIdentifierName} AS sourceId", "target.{$targetIdentifierName} AS targetId") ->from($sourceClassMetadata->getName(), 'source') ->join("source.{$sourcePropertyName}", 'target') ->andWhere('source IN (:sourceEntityIds)') - ->setParameter('sourceEntityIds', $uninitializedSourceEntityIdsChunk) + ->setParameter( + 'sourceEntityIds', + $this->convertFieldValuesToDatabaseValues($sourceIdentifierType, $uninitializedSourceEntityIdsChunk), + $this->deduceArrayParameterType($sourceIdentifierType), + ) ->getQuery() ->getResult(); @@ -345,7 +355,7 @@ private function preloadManyToManyInner( $uninitializedTargetEntityIds[$targetEntityKey] = $targetEntityId; } - foreach ($this->loadEntitiesBy($targetClassMetadata, $targetIdentifierName, array_values($uninitializedTargetEntityIds), $maxFetchJoinSameFieldCount) as $targetEntity) { + foreach ($this->loadEntitiesBy($targetClassMetadata, $targetIdentifierName, $sourceClassMetadata, array_values($uninitializedTargetEntityIds), $maxFetchJoinSameFieldCount) as $targetEntity) { $targetEntityKey = (string) $targetIdentifierReflection->getValue($targetEntity); $targetEntities[$targetEntityKey] = $targetEntity; } @@ -404,15 +414,18 @@ private function preloadToOne( /** * @param ClassMetadata $targetClassMetadata * @param list $fieldValues + * @param ClassMetadata $referencedClassMetadata * @param non-negative-int $maxFetchJoinSameFieldCount * @param array $orderBy * @return list * * @template T of E + * @template R of E */ private function loadEntitiesBy( ClassMetadata $targetClassMetadata, string $fieldName, + ClassMetadata $referencedClassMetadata, array $fieldValues, int $maxFetchJoinSameFieldCount, array $orderBy = [], @@ -422,13 +435,18 @@ private function loadEntitiesBy( return []; } + $referencedType = $this->getIdentifierFieldType($referencedClassMetadata); $rootLevelAlias = 'e'; $queryBuilder = $this->entityManager->createQueryBuilder() ->select($rootLevelAlias) ->from($targetClassMetadata->getName(), $rootLevelAlias) ->andWhere("{$rootLevelAlias}.{$fieldName} IN (:fieldValues)") - ->setParameter('fieldValues', $fieldValues); + ->setParameter( + 'fieldValues', + $this->convertFieldValuesToDatabaseValues($referencedType, $fieldValues), + $this->deduceArrayParameterType($referencedType), + ); $this->addFetchJoinsToPreventFetchDuringHydration($rootLevelAlias, $queryBuilder, $targetClassMetadata, $maxFetchJoinSameFieldCount); @@ -439,6 +457,54 @@ private function loadEntitiesBy( return $queryBuilder->getQuery()->getResult(); } + private function deduceArrayParameterType(Type $dbalType): ArrayParameterType|int|null // @phpstan-ignore return.unusedType (old dbal compat) + { + return match ($dbalType->getBindingType()) { + ParameterType::INTEGER => ArrayParameterType::INTEGER, + ParameterType::STRING => ArrayParameterType::STRING, + ParameterType::ASCII => ArrayParameterType::ASCII, + ParameterType::BINARY => ArrayParameterType::BINARY, + default => null, + }; + } + + /** + * @param array $fieldValues + * @return list + */ + private function convertFieldValuesToDatabaseValues( + Type $dbalType, + array $fieldValues, + ): array + { + $connection = $this->entityManager->getConnection(); + $platform = $connection->getDatabasePlatform(); + + $convertedValues = []; + foreach ($fieldValues as $value) { + $convertedValues[] = $dbalType->convertToDatabaseValue($value, $platform); + } + + return $convertedValues; + } + + /** + * @param ClassMetadata $classMetadata + * + * @template C of E + */ + private function getIdentifierFieldType(ClassMetadata $classMetadata): Type + { + $identifierName = $classMetadata->getSingleIdentifierFieldName(); + $sourceIdTypeName = $classMetadata->getTypeOfField($identifierName); + + if ($sourceIdTypeName === null) { + throw new LogicException("Identifier field '{$identifierName}' for class '{$classMetadata->getName()}' has unknown field type."); + } + + return Type::getType($sourceIdTypeName); + } + /** * @param ClassMetadata $sourceClassMetadata * @param array> $alreadyPreloadedJoins diff --git a/tests/EntityPreloadBlogManyHasManyInversedTest.php b/tests/EntityPreloadBlogManyHasManyInversedTest.php index 0d72ab7..e6f48e7 100644 --- a/tests/EntityPreloadBlogManyHasManyInversedTest.php +++ b/tests/EntityPreloadBlogManyHasManyInversedTest.php @@ -2,16 +2,19 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\Mapping\ClassMetadata; +use PHPUnit\Framework\Attributes\DataProvider; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Tag; use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase; class EntityPreloadBlogManyHasManyInversedTest extends TestCase { - public function testManyHasManyInversedUnoptimized(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasManyInversedUnoptimized(DbalType $primaryKey): void { - $this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, articleInEachCategoryCount: 5, tagForEachArticleCount: 5); $tags = $this->getEntityManager()->getRepository(Tag::class)->findAll(); @@ -23,9 +26,10 @@ public function testManyHasManyInversedUnoptimized(): void ]); } - public function testManyHasManyInversedWithFetchJoin(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasManyInversedWithFetchJoin(DbalType $primaryKey): void { - $this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, articleInEachCategoryCount: 5, tagForEachArticleCount: 5); $tags = $this->getEntityManager()->createQueryBuilder() ->select('tag', 'article') @@ -41,9 +45,10 @@ public function testManyHasManyInversedWithFetchJoin(): void ]); } - public function testManyHasManyInversedWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasManyInversedWithEagerFetchMode(DbalType $primaryKey): void { - $this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, articleInEachCategoryCount: 5, tagForEachArticleCount: 5); // for eagerly loaded Many-To-Many associations one query has to be made for each collection // https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/working-with-objects.html#by-eager-loading @@ -62,9 +67,10 @@ public function testManyHasManyInversedWithEagerFetchMode(): void ]); } - public function testManyHasManyInversedWithPreload(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasManyInversedWithPreload(DbalType $primaryKey): void { - $this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, articleInEachCategoryCount: 5, tagForEachArticleCount: 5); $tags = $this->getEntityManager()->getRepository(Tag::class)->findAll(); $this->getEntityPreloader()->preload($tags, 'articles'); diff --git a/tests/EntityPreloadBlogManyHasManyTest.php b/tests/EntityPreloadBlogManyHasManyTest.php index 34f82dc..0869ca9 100644 --- a/tests/EntityPreloadBlogManyHasManyTest.php +++ b/tests/EntityPreloadBlogManyHasManyTest.php @@ -2,16 +2,20 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\Mapping\ClassMetadata; +use PHPUnit\Framework\Attributes\DataProvider; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase; +use function array_map; class EntityPreloadBlogManyHasManyTest extends TestCase { - public function testManyHasManyUnoptimized(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasManyUnoptimized(DbalType $primaryKey): void { - $this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, articleInEachCategoryCount: 5, tagForEachArticleCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); @@ -23,19 +27,25 @@ public function testManyHasManyUnoptimized(): void ]); } - public function testOneHasManyWithWithManualPreloadUsingPartial(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyWithWithManualPreloadUsingPartial(DbalType $primaryKey): void { $this->skipIfPartialEntitiesAreNotSupported(); - $this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, articleInEachCategoryCount: 5, tagForEachArticleCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); + $platform = $this->getEntityManager()->getConnection()->getDatabasePlatform(); + $rawArticleIds = array_map( + static fn (Article $article) => $primaryKey->convertToDatabaseValue($article->getId(), $platform), + $articles, + ); $this->getEntityManager()->createQueryBuilder() ->select('PARTIAL article.{id}', 'tag') ->from(Article::class, 'article') ->leftJoin('article.tags', 'tag') ->where('article IN (:articles)') - ->setParameter('articles', $articles) + ->setParameter('articles', $rawArticleIds, $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); @@ -47,9 +57,10 @@ public function testOneHasManyWithWithManualPreloadUsingPartial(): void ]); } - public function testManyHasManyWithFetchJoin(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasManyWithFetchJoin(DbalType $primaryKey): void { - $this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, articleInEachCategoryCount: 5, tagForEachArticleCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() ->select('article', 'tag') @@ -65,9 +76,10 @@ public function testManyHasManyWithFetchJoin(): void ]); } - public function testManyHasManyWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasManyWithEagerFetchMode(DbalType $primaryKey): void { - $this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, articleInEachCategoryCount: 5, tagForEachArticleCount: 5); // for eagerly loaded Many-To-Many associations one query has to be made for each collection // https://www.doctrine-project.org/projects/doctrine-orm/en/3.2/reference/working-with-objects.html#by-eager-loading @@ -86,9 +98,10 @@ public function testManyHasManyWithEagerFetchMode(): void ]); } - public function testManyHasManyWithPreload(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasManyWithPreload(DbalType $primaryKey): void { - $this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, articleInEachCategoryCount: 5, tagForEachArticleCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); $this->getEntityPreloader()->preload($articles, 'tags'); diff --git a/tests/EntityPreloadBlogManyHasOneDeepTest.php b/tests/EntityPreloadBlogManyHasOneDeepTest.php index 34733eb..9762b16 100644 --- a/tests/EntityPreloadBlogManyHasOneDeepTest.php +++ b/tests/EntityPreloadBlogManyHasOneDeepTest.php @@ -2,7 +2,9 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\Mapping\ClassMetadata; +use PHPUnit\Framework\Attributes\DataProvider; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category; use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase; @@ -15,9 +17,10 @@ class EntityPreloadBlogManyHasOneDeepTest extends TestCase { - public function testManyHasOneDeepUnoptimized(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneDeepUnoptimized(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); @@ -29,33 +32,35 @@ public function testManyHasOneDeepUnoptimized(): void ]); } - public function testManyHasOneDeepWithManualPreload(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneDeepWithManualPreload(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); + $platform = $this->getEntityManager()->getConnection()->getDatabasePlatform(); - $categoryIds = array_map(static fn (Article $article) => $article->getCategory()?->getId(), $articles); - $categoryIds = array_filter($categoryIds, static fn (?int $id) => $id !== null); + $categoryIds = array_map(static fn (Article $article) => $primaryKey->convertToDatabaseValue($article->getCategory()?->getId(), $platform), $articles); + $categoryIds = array_filter($categoryIds, static fn (?string $id) => $id !== null); if (count($categoryIds) > 0) { $categories = $this->getEntityManager()->createQueryBuilder() ->select('category') ->from(Category::class, 'category') ->where('category.id IN (:ids)') - ->setParameter('ids', array_values(array_unique($categoryIds))) + ->setParameter('ids', array_values(array_unique($categoryIds)), $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); - $parentCategoryIds = array_map(static fn (Category $category) => $category->getParent()?->getId(), $categories); - $parentCategoryIds = array_filter($parentCategoryIds, static fn (?int $id) => $id !== null); + $parentCategoryIds = array_map(static fn (Category $category) => $primaryKey->convertToDatabaseValue($category->getParent()?->getId(), $platform), $categories); + $parentCategoryIds = array_filter($parentCategoryIds, static fn (?string $id) => $id !== null); if (count($parentCategoryIds) > 0) { $this->getEntityManager()->createQueryBuilder() ->select('category') ->from(Category::class, 'category') ->where('category.id IN (:ids)') - ->setParameter('ids', array_values(array_unique($parentCategoryIds))) + ->setParameter('ids', array_values(array_unique($parentCategoryIds)), $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); } @@ -69,9 +74,10 @@ public function testManyHasOneDeepWithManualPreload(): void ]); } - public function testManyHasOneDeepWithFetchJoin(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneDeepWithFetchJoin(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() ->select('article', 'category', 'parentCategory') @@ -88,9 +94,12 @@ public function testManyHasOneDeepWithFetchJoin(): void ]); } - public function testManyHasOneDeepWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneDeepWithEagerFetchMode(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); + $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); + $this->skipIfDoctrineOrmHasBrokenEagerFetch($primaryKey); + $this->createDummyBlogData($primaryKey, categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() ->select('article') @@ -109,9 +118,10 @@ public function testManyHasOneDeepWithEagerFetchMode(): void ]); } - public function testManyHasOneDeepWithPreload(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneDeepWithPreload(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); $categories = $this->getEntityPreloader()->preload($articles, 'category'); diff --git a/tests/EntityPreloadBlogManyHasOneTest.php b/tests/EntityPreloadBlogManyHasOneTest.php index 8ce7ff9..1e0082d 100644 --- a/tests/EntityPreloadBlogManyHasOneTest.php +++ b/tests/EntityPreloadBlogManyHasOneTest.php @@ -2,7 +2,9 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\Mapping\ClassMetadata; +use PHPUnit\Framework\Attributes\DataProvider; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category; use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase; @@ -15,9 +17,10 @@ class EntityPreloadBlogManyHasOneTest extends TestCase { - public function testManyHasOneUnoptimized(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneUnoptimized(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); @@ -29,21 +32,23 @@ public function testManyHasOneUnoptimized(): void ]); } - public function testManyHasOneWithManualPreload(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneWithManualPreload(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); + $platform = $this->getEntityManager()->getConnection()->getDatabasePlatform(); - $categoryIds = array_map(static fn (Article $article) => $article->getCategory()?->getId(), $articles); - $categoryIds = array_filter($categoryIds, static fn (?int $id) => $id !== null); + $categoryIds = array_map(static fn (Article $article) => $primaryKey->convertToDatabaseValue($article->getCategory()?->getId(), $platform), $articles); + $categoryIds = array_filter($categoryIds, static fn (?string $id) => $id !== null); if (count($categoryIds) > 0) { $this->getEntityManager()->createQueryBuilder() ->select('category') ->from(Category::class, 'category') ->where('category.id IN (:ids)') - ->setParameter('ids', array_values(array_unique($categoryIds))) + ->setParameter('ids', array_values(array_unique($categoryIds)), $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); } @@ -56,9 +61,10 @@ public function testManyHasOneWithManualPreload(): void ]); } - public function testManyHasOneWithFetchJoin(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneWithFetchJoin(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() ->select('article', 'category') @@ -74,9 +80,12 @@ public function testManyHasOneWithFetchJoin(): void ]); } - public function testManyHasOneWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneWithEagerFetchMode(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); + $this->skipIfDoctrineOrmHasBrokenEagerFetch($primaryKey); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() ->select('article') @@ -93,9 +102,10 @@ public function testManyHasOneWithEagerFetchMode(): void ]); } - public function testManyHasOneWithPreload(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneWithPreload(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); $this->getEntityPreloader()->preload($articles, 'category'); diff --git a/tests/EntityPreloadBlogOneHasManyAbstractTest.php b/tests/EntityPreloadBlogOneHasManyAbstractTest.php index f048377..f1486d7 100644 --- a/tests/EntityPreloadBlogOneHasManyAbstractTest.php +++ b/tests/EntityPreloadBlogOneHasManyAbstractTest.php @@ -2,7 +2,9 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\Mapping\ClassMetadata; +use PHPUnit\Framework\Attributes\DataProvider; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Comment; use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase; @@ -10,9 +12,10 @@ class EntityPreloadBlogOneHasManyAbstractTest extends TestCase { - public function testOneHasManyAbstractUnoptimized(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyAbstractUnoptimized(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); @@ -25,9 +28,10 @@ public function testOneHasManyAbstractUnoptimized(): void ]); } - public function testOneHasManyAbstractWithFetchJoin(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyAbstractWithFetchJoin(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() ->select('article', 'comment', 'author') @@ -44,9 +48,12 @@ public function testOneHasManyAbstractWithFetchJoin(): void ]); } - public function testOneHasManyAbstractWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyAbstractWithEagerFetchMode(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); + $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); + $this->skipIfDoctrineOrmHasBrokenEagerFetch($primaryKey); + $this->createDummyBlogData($primaryKey, categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() ->select('article') @@ -65,9 +72,10 @@ public function testOneHasManyAbstractWithEagerFetchMode(): void ]); } - public function testOneHasManyAbstractWithPreload(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyAbstractWithPreload(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); $this->getEntityPreloader()->preload($articles, 'comments'); diff --git a/tests/EntityPreloadBlogOneHasManyDeepTest.php b/tests/EntityPreloadBlogOneHasManyDeepTest.php index 8fda67a..72dcac5 100644 --- a/tests/EntityPreloadBlogOneHasManyDeepTest.php +++ b/tests/EntityPreloadBlogOneHasManyDeepTest.php @@ -2,7 +2,9 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\Mapping\ClassMetadata; +use PHPUnit\Framework\Attributes\DataProvider; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category; use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase; use function array_map; @@ -11,9 +13,10 @@ class EntityPreloadBlogOneHasManyDeepTest extends TestCase { - public function testOneHasManyDeepUnoptimized(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyDeepUnoptimized(DbalType $primaryKey): void { - $this->createCategoryTree(depth: 5, branchingFactor: 5); + $this->createCategoryTree($primaryKey, depth: 5, branchingFactor: 5); $rootCategories = $this->getEntityManager()->createQueryBuilder() ->select('category') @@ -30,10 +33,11 @@ public function testOneHasManyDeepUnoptimized(): void ]); } - public function testOneHasManyDeepWithWithManualPreloadUsingPartial(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyDeepWithWithManualPreloadUsingPartial(DbalType $primaryKey): void { $this->skipIfPartialEntitiesAreNotSupported(); - $this->createCategoryTree(depth: 5, branchingFactor: 5); + $this->createCategoryTree($primaryKey, depth: 5, branchingFactor: 5); $rootCategories = $this->getEntityManager()->createQueryBuilder() ->select('category') @@ -42,22 +46,27 @@ public function testOneHasManyDeepWithWithManualPreloadUsingPartial(): void ->getQuery() ->getResult(); + $platform = $this->getEntityManager()->getConnection()->getDatabasePlatform(); + $rawRootCategoryIds = array_map(static fn (Category $category) => $primaryKey->convertToDatabaseValue($category->getId(), $platform), $rootCategories); + $this->getEntityManager()->createQueryBuilder() ->select('PARTIAL category.{id}', 'subCategory') ->from(Category::class, 'category') ->leftJoin('category.children', 'subCategory') ->where('category IN (:categories)') - ->setParameter('categories', $rootCategories) + ->setParameter('categories', $rawRootCategoryIds, $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); $subCategories = array_merge(...array_map(static fn (Category $category) => $category->getChildren()->toArray(), $rootCategories)); + $rawSubCategoryIds = array_map(static fn (Category $category) => $primaryKey->convertToDatabaseValue($category->getId(), $platform), $subCategories); + $this->getEntityManager()->createQueryBuilder() ->select('PARTIAL subCategory.{id}', 'subSubCategory') ->from(Category::class, 'subCategory') ->leftJoin('subCategory.children', 'subSubCategory') ->where('subCategory IN (:subCategories)') - ->setParameter('subCategories', $subCategories) + ->setParameter('subCategories', $rawSubCategoryIds, $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); @@ -70,9 +79,10 @@ public function testOneHasManyDeepWithWithManualPreloadUsingPartial(): void ]); } - public function testOneHasManyDeepWithFetchJoin(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyDeepWithFetchJoin(DbalType $primaryKey): void { - $this->createCategoryTree(depth: 5, branchingFactor: 5); + $this->createCategoryTree($primaryKey, depth: 5, branchingFactor: 5); $rootCategories = $this->getEntityManager()->createQueryBuilder() ->select('category', 'subCategories', 'subSubCategories') @@ -90,9 +100,12 @@ public function testOneHasManyDeepWithFetchJoin(): void ]); } - public function testOneHasManyDeepWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyDeepWithEagerFetchMode(DbalType $primaryKey): void { - $this->createCategoryTree(depth: 5, branchingFactor: 5); + $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); + $this->skipIfDoctrineOrmHasBrokenEagerFetch($primaryKey); + $this->createCategoryTree($primaryKey, depth: 5, branchingFactor: 5); $rootCategories = $this->getEntityManager()->createQueryBuilder() ->select('category') @@ -111,9 +124,10 @@ public function testOneHasManyDeepWithEagerFetchMode(): void ]); } - public function testOneHasManyDeepWithPreload(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyDeepWithPreload(DbalType $primaryKey): void { - $this->createCategoryTree(depth: 5, branchingFactor: 5); + $this->createCategoryTree($primaryKey, depth: 5, branchingFactor: 5); $rootCategories = $this->getEntityManager()->createQueryBuilder() ->select('category') @@ -135,17 +149,20 @@ public function testOneHasManyDeepWithPreload(): void } private function createCategoryTree( + DbalType $primaryKey, int $depth, int $branchingFactor, ?Category $parent = null, ): void { + $this->initializeEntityManager($primaryKey, $this->getQueryLogger()); + for ($i = 0; $i < $branchingFactor; $i++) { $category = new Category("Category $depth-$i", $parent); $this->getEntityManager()->persist($category); if ($depth > 1) { - $this->createCategoryTree($depth - 1, $branchingFactor, $category); + $this->createCategoryTree($primaryKey, $depth - 1, $branchingFactor, $category); } } diff --git a/tests/EntityPreloadBlogOneHasManyTest.php b/tests/EntityPreloadBlogOneHasManyTest.php index f28aa60..ac02ffd 100644 --- a/tests/EntityPreloadBlogOneHasManyTest.php +++ b/tests/EntityPreloadBlogOneHasManyTest.php @@ -2,17 +2,21 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\Mapping\ClassMetadata; +use PHPUnit\Framework\Attributes\DataProvider; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category; use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase; +use function array_map; class EntityPreloadBlogOneHasManyTest extends TestCase { - public function testOneHasManyUnoptimized(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyUnoptimized(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $categories = $this->getEntityManager()->getRepository(Category::class)->findAll(); @@ -24,9 +28,10 @@ public function testOneHasManyUnoptimized(): void ]); } - public function testOneHasManyWithWithManualPreload(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyWithWithManualPreload(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $categories = $this->getEntityManager()->getRepository(Category::class)->findAll(); @@ -47,19 +52,25 @@ public function testOneHasManyWithWithManualPreload(): void ]); } - public function testOneHasManyWithWithManualPreloadUsingPartial(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyWithWithManualPreloadUsingPartial(DbalType $primaryKey): void { $this->skipIfPartialEntitiesAreNotSupported(); - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $categories = $this->getEntityManager()->getRepository(Category::class)->findAll(); + $platform = $this->getEntityManager()->getConnection()->getDatabasePlatform(); + $rawCategoryIds = array_map( + static fn (Category $category) => $primaryKey->convertToDatabaseValue($category->getId(), $platform), + $categories, + ); $this->getEntityManager()->createQueryBuilder() ->select('PARTIAL category.{id}', 'article') ->from(Category::class, 'category') ->leftJoin('category.articles', 'article') ->where('category IN (:categories)') - ->setParameter('categories', $categories) + ->setParameter('categories', $rawCategoryIds, $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); @@ -71,9 +82,10 @@ public function testOneHasManyWithWithManualPreloadUsingPartial(): void ]); } - public function testOneHasManyWithFetchJoin(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyWithFetchJoin(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $categories = $this->getEntityManager()->createQueryBuilder() ->select('category', 'article') @@ -89,9 +101,12 @@ public function testOneHasManyWithFetchJoin(): void ]); } - public function testOneHasManyWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyWithEagerFetchMode(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); + $this->skipIfDoctrineOrmHasBrokenEagerFetch($primaryKey); // here the test it green, but emits PHP warning + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $categories = $this->getEntityManager()->createQueryBuilder() ->select('category') @@ -108,9 +123,10 @@ public function testOneHasManyWithEagerFetchMode(): void ]); } - public function testOneHasManyWithPreload(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyWithPreload(DbalType $primaryKey): void { - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $categories = $this->getEntityManager()->getRepository(Category::class)->findAll(); $this->getEntityPreloader()->preload($categories, 'articles'); diff --git a/tests/EntityPreloadSyntheticTest.php b/tests/EntityPreloadSyntheticTest.php index 82a1ab3..13192a4 100644 --- a/tests/EntityPreloadSyntheticTest.php +++ b/tests/EntityPreloadSyntheticTest.php @@ -2,6 +2,7 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\Types\IntegerType; use PHPUnit\Framework\Attributes\DataProvider; use ShipMonk\DoctrineEntityPreloader\Exception\LogicException; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Synthetic\AbstractEntityWithNoRelations; @@ -33,6 +34,13 @@ class EntityPreloadSyntheticTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + + $this->initializeEntityManager(new IntegerType(), $this->getQueryLogger()); + } + public function testManyToOne(): void { $entityWithNoRelations = $this->givenEntityWithNoRelations(); diff --git a/tests/Fixtures/Blog/Article.php b/tests/Fixtures/Blog/Article.php index 88626a0..767ea7d 100644 --- a/tests/Fixtures/Blog/Article.php +++ b/tests/Fixtures/Blog/Article.php @@ -7,22 +7,15 @@ use Doctrine\Common\Collections\ReadableCollection; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; -use Doctrine\ORM\Mapping\GeneratedValue; -use Doctrine\ORM\Mapping\Id; use Doctrine\ORM\Mapping\ManyToMany; use Doctrine\ORM\Mapping\ManyToOne; use Doctrine\ORM\Mapping\OneToMany; use Doctrine\ORM\Mapping\OrderBy; #[Entity] -class Article +class Article extends TestEntityWithCustomPrimaryKey { - #[Id] - #[Column] - #[GeneratedValue] - private int $id; - #[Column] private string $title; @@ -51,6 +44,7 @@ public function __construct( ?Category $category = null, ) { + parent::__construct(); $this->title = $title; $this->content = $content; $this->category = $category; @@ -60,11 +54,6 @@ public function __construct( $category?->addArticle($this); } - public function getId(): int - { - return $this->id; - } - public function getTitle(): string { return $this->title; diff --git a/tests/Fixtures/Blog/BotPromptVersion.php b/tests/Fixtures/Blog/BotPromptVersion.php index 22cdc6b..8dd277c 100644 --- a/tests/Fixtures/Blog/BotPromptVersion.php +++ b/tests/Fixtures/Blog/BotPromptVersion.php @@ -4,19 +4,12 @@ use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; -use Doctrine\ORM\Mapping\GeneratedValue; -use Doctrine\ORM\Mapping\Id; use Doctrine\ORM\Mapping\OneToOne; #[Entity] -class BotPromptVersion +class BotPromptVersion extends TestEntityWithCustomPrimaryKey { - #[Id] - #[Column] - #[GeneratedValue] - private int $id; - #[Column] private int $version; @@ -34,6 +27,7 @@ public function __construct( ?self $prevScript = null, ) { + parent::__construct(); $this->version = ($prevScript->version ?? 0) + 1; $this->prompt = $prompt; $this->prevVersion = $prevScript; @@ -44,11 +38,6 @@ public function __construct( } } - public function getId(): int - { - return $this->id; - } - public function getVersion(): int { return $this->version; diff --git a/tests/Fixtures/Blog/Category.php b/tests/Fixtures/Blog/Category.php index 0316615..0df7ec7 100644 --- a/tests/Fixtures/Blog/Category.php +++ b/tests/Fixtures/Blog/Category.php @@ -7,20 +7,13 @@ use Doctrine\Common\Collections\ReadableCollection; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; -use Doctrine\ORM\Mapping\GeneratedValue; -use Doctrine\ORM\Mapping\Id; use Doctrine\ORM\Mapping\ManyToOne; use Doctrine\ORM\Mapping\OneToMany; #[Entity] -class Category +class Category extends TestEntityWithCustomPrimaryKey { - #[Id] - #[Column] - #[GeneratedValue] - private int $id; - #[Column] private string $name; @@ -44,6 +37,7 @@ public function __construct( ?self $parent = null, ) { + parent::__construct(); $this->name = $name; $this->parent = $parent; $this->children = new ArrayCollection(); @@ -52,11 +46,6 @@ public function __construct( $parent?->addChild($this); } - public function getId(): int - { - return $this->id; - } - public function getName(): string { return $this->name; diff --git a/tests/Fixtures/Blog/Comment.php b/tests/Fixtures/Blog/Comment.php index 8ae603b..246ceab 100644 --- a/tests/Fixtures/Blog/Comment.php +++ b/tests/Fixtures/Blog/Comment.php @@ -4,19 +4,12 @@ use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; -use Doctrine\ORM\Mapping\GeneratedValue; -use Doctrine\ORM\Mapping\Id; use Doctrine\ORM\Mapping\ManyToOne; #[Entity] -class Comment +class Comment extends TestEntityWithCustomPrimaryKey { - #[Id] - #[Column] - #[GeneratedValue] - private int $id; - #[ManyToOne(targetEntity: Article::class, inversedBy: 'comments')] private Article $article; @@ -32,6 +25,7 @@ public function __construct( string $content, ) { + parent::__construct(); $this->article = $article; $this->author = $author; $this->content = $content; @@ -40,11 +34,6 @@ public function __construct( $author->addComment($this); } - public function getId(): int - { - return $this->id; - } - public function getArticle(): Article { return $this->article; diff --git a/tests/Fixtures/Blog/Contributor.php b/tests/Fixtures/Blog/Contributor.php index 66dfc30..da1d227 100644 --- a/tests/Fixtures/Blog/Contributor.php +++ b/tests/Fixtures/Blog/Contributor.php @@ -7,22 +7,15 @@ use Doctrine\Common\Collections\ReadableCollection; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; -use Doctrine\ORM\Mapping\GeneratedValue; -use Doctrine\ORM\Mapping\Id; use Doctrine\ORM\Mapping\InheritanceType; use Doctrine\ORM\Mapping\OneToMany; use LogicException; #[Entity] #[InheritanceType('SINGLE_TABLE')] -abstract class Contributor +abstract class Contributor extends TestEntityWithCustomPrimaryKey { - #[Id] - #[Column] - #[GeneratedValue] - private int $id; - #[Column] private string $name; @@ -34,15 +27,11 @@ abstract class Contributor public function __construct(string $name) { + parent::__construct(); $this->name = $name; $this->comments = new ArrayCollection(); } - public function getId(): int - { - return $this->id; - } - public function getName(): string { return $this->name; diff --git a/tests/Fixtures/Blog/PrimaryKey.php b/tests/Fixtures/Blog/PrimaryKey.php new file mode 100644 index 0000000..210d934 --- /dev/null +++ b/tests/Fixtures/Blog/PrimaryKey.php @@ -0,0 +1,39 @@ +data = $data; + } + + public static function new(): self + { + $bits = self::LENGTH_BYTES * 8; + $maxValue = (1 << $bits) - 1; + + return new self(random_int(0, $maxValue)); + } + + public function getData(): int + { + return $this->data; + } + + public function __toString(): string + { + return base64_encode((string) $this->data); // intentionally not matching any internal PK representation + } + +} diff --git a/tests/Fixtures/Blog/Tag.php b/tests/Fixtures/Blog/Tag.php index 62264ad..945547a 100644 --- a/tests/Fixtures/Blog/Tag.php +++ b/tests/Fixtures/Blog/Tag.php @@ -7,19 +7,12 @@ use Doctrine\Common\Collections\ReadableCollection; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; -use Doctrine\ORM\Mapping\GeneratedValue; -use Doctrine\ORM\Mapping\Id; use Doctrine\ORM\Mapping\ManyToMany; #[Entity] -class Tag +class Tag extends TestEntityWithCustomPrimaryKey { - #[Id] - #[Column] - #[GeneratedValue] - private int $id; - #[Column] private string $label; @@ -31,15 +24,11 @@ class Tag public function __construct(string $label) { + parent::__construct(); $this->label = $label; $this->articles = new ArrayCollection(); } - public function getId(): int - { - return $this->id; - } - public function getLabel(): string { return $this->label; diff --git a/tests/Fixtures/Blog/TestEntityWithCustomPrimaryKey.php b/tests/Fixtures/Blog/TestEntityWithCustomPrimaryKey.php new file mode 100644 index 0000000..43270a3 --- /dev/null +++ b/tests/Fixtures/Blog/TestEntityWithCustomPrimaryKey.php @@ -0,0 +1,27 @@ +id = PrimaryKey::new(); + } + + public function getId(): PrimaryKey + { + return $this->id; + } + +} diff --git a/tests/Fixtures/Blog/Type/PrimaryKeyBase64StringType.php b/tests/Fixtures/Blog/Type/PrimaryKeyBase64StringType.php new file mode 100644 index 0000000..2484dd0 --- /dev/null +++ b/tests/Fixtures/Blog/Type/PrimaryKeyBase64StringType.php @@ -0,0 +1,75 @@ +getData()); + + } else { + throw new LogicException('Unexpected value: ' . $value); + } + } + + public function getSQLDeclaration( + array $column, + AbstractPlatform $platform, + ): string + { + return $platform->getStringTypeDeclarationSQL([]); + } + + public function getName(): string + { + return PrimaryKey::DOCTRINE_TYPE_NAME; + } + + public function doGetBindingType(): ParameterType|int // @phpstan-ignore return.unusedType (old dbal compat) + { + return ParameterType::STRING; + } + +} diff --git a/tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php b/tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php new file mode 100644 index 0000000..b43e9a3 --- /dev/null +++ b/tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php @@ -0,0 +1,74 @@ +getData()); + + } else { + throw new LogicException('Unexpected value: ' . $value); + } + } + + public function getSQLDeclaration( + array $column, + AbstractPlatform $platform, + ): string + { + return $platform->getBinaryTypeDeclarationSQL([ + 'length' => PrimaryKey::LENGTH_BYTES, + 'fixed' => true, + ]); + } + + public function getName(): string + { + return PrimaryKey::DOCTRINE_TYPE_NAME; + } + + public function doGetBindingType(): ParameterType|int // @phpstan-ignore return.unusedType (old dbal compat) + { + return ParameterType::BINARY; + } + +} diff --git a/tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php b/tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php new file mode 100644 index 0000000..e89eea9 --- /dev/null +++ b/tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php @@ -0,0 +1,71 @@ +getData(); + + } else { + throw new LogicException('Unexpected value: ' . $value); + } + } + + public function getSQLDeclaration( + array $column, + AbstractPlatform $platform, + ): string + { + return $platform->getIntegerTypeDeclarationSQL([ + 'unsigned' => true, + ]); + } + + public function getName(): string + { + return PrimaryKey::DOCTRINE_TYPE_NAME; + } + + public function doGetBindingType(): ParameterType|int // @phpstan-ignore return.unusedType (old dbal compat) + { + return ParameterType::INTEGER; + } + +} diff --git a/tests/Fixtures/Blog/Type/PrimaryKeyStringType.php b/tests/Fixtures/Blog/Type/PrimaryKeyStringType.php new file mode 100644 index 0000000..11b75db --- /dev/null +++ b/tests/Fixtures/Blog/Type/PrimaryKeyStringType.php @@ -0,0 +1,69 @@ +getData(); + + } else { + throw new LogicException('Unexpected value: ' . $value); + } + } + + public function getSQLDeclaration( + array $column, + AbstractPlatform $platform, + ): string + { + return $platform->getStringTypeDeclarationSQL([]); + } + + public function getName(): string + { + return PrimaryKey::DOCTRINE_TYPE_NAME; + } + + public function doGetBindingType(): ParameterType|int // @phpstan-ignore return.unusedType (old dbal compat) + { + return ParameterType::STRING; + } + +} diff --git a/tests/Fixtures/Compat/CompatibilityType.php b/tests/Fixtures/Compat/CompatibilityType.php new file mode 100644 index 0000000..ed591d6 --- /dev/null +++ b/tests/Fixtures/Compat/CompatibilityType.php @@ -0,0 +1,41 @@ +doGetBindingType(); + } + + private function doGetBindingType(): int|ParameterType + { + return parent::getBindingType(); + } + + } +} else { + trait CompatibilityType + { + + public function getBindingType(): ParameterType + { + return $this->doGetBindingType(); + } + + private function doGetBindingType(): int|ParameterType + { + return parent::getBindingType(); + } + + } +} diff --git a/tests/Lib/TestCase.php b/tests/Lib/TestCase.php index 8d9ee48..df0c3c1 100644 --- a/tests/Lib/TestCase.php +++ b/tests/Lib/TestCase.php @@ -3,8 +3,13 @@ namespace ShipMonkTests\DoctrineEntityPreloader\Lib; use Composer\InstalledVersions; +use Composer\Semver\VersionParser; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Logging\Middleware; +use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\UnderscoreNamingStrategy; @@ -20,7 +25,12 @@ use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Bot; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Comment; +use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\PrimaryKey; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Tag; +use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Type\PrimaryKeyBase64StringType; +use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Type\PrimaryKeyBinaryType; +use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Type\PrimaryKeyIntegerType; +use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Type\PrimaryKeyStringType; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\User; use Throwable; use function unlink; @@ -38,6 +48,17 @@ abstract class TestCase extends PhpUnitTestCase */ private ?EntityPreloader $entityPreloader = null; + /** + * @return iterable + */ + public static function providePrimaryKeyTypes(): iterable + { + yield 'binary' => [new PrimaryKeyBinaryType()]; + yield 'string' => [new PrimaryKeyStringType()]; + yield 'integer' => [new PrimaryKeyIntegerType()]; + yield 'base64string' => [new PrimaryKeyBase64StringType()]; + } + protected function setUp(): void { parent::setUp(); @@ -91,6 +112,7 @@ protected function assertAggregatedQueries( } protected function createDummyBlogData( + DbalType $dbalType, int $categoryCount = 1, int $categoryParentsCount = 0, int $articleInEachCategoryCount = 1, @@ -99,6 +121,7 @@ protected function createDummyBlogData( int $promptChangeCount = 0, ): void { + $this->initializeEntityManager($dbalType, $this->getQueryLogger()); $entityManager = $this->getEntityManager(); for ($h = 0; $h < $categoryCount; $h++) { @@ -185,15 +208,6 @@ protected function refreshExistingEntity(object $entity): object return $freshEntity; } - protected function skipIfPartialEntitiesAreNotSupported(): void - { - $ormVersion = InstalledVersions::getVersion('doctrine/orm') ?? '0.0.0'; - - if (version_compare($ormVersion, '3.0.0', '>=') && version_compare($ormVersion, '3.3.0', '<')) { - self::markTestSkipped('Partial entities are not supported in Doctrine ORM versions 3.0 to 3.2'); - } - } - protected function getQueryLogger(): QueryLogger { return $this->queryLogger ??= $this->createQueryLogger(); @@ -201,7 +215,10 @@ protected function getQueryLogger(): QueryLogger protected function getEntityManager(): EntityManagerInterface { - return $this->entityManager ??= $this->createEntityManager($this->getQueryLogger()); + if ($this->entityManager === null) { + throw new LogicException('EntityManager is not initialized. Call createEntityManager() with DbalType before using it.'); + } + return $this->entityManager; } /** @@ -218,6 +235,7 @@ private function createQueryLogger(): QueryLogger } private function createEntityManager( + DbalType $primaryKey, LoggerInterface $logger, bool $inMemory = true, ): EntityManagerInterface @@ -238,6 +256,12 @@ private function createEntityManager( $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite'] + $driverOptions, $config); $entityManager = new EntityManager($connection, $config); + if (DbalType::hasType(PrimaryKey::DOCTRINE_TYPE_NAME)) { + DbalType::overrideType(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey::class); + } else { + DbalType::addType(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey::class); + } + $schemaTool = new SchemaTool($entityManager); $schemaTool->createSchema($entityManager->getMetadataFactory()->getAllMetadata()); @@ -255,4 +279,52 @@ private function createEntityPreloader(EntityManagerInterface $entityManager): E return new EntityPreloader($entityManager); } + protected function skipIfDoctrineOrmHasBrokenUnhandledMatchCase(): void + { + if (!InstalledVersions::satisfies(new VersionParser(), 'doctrine/orm', '^3.5.1')) { + self::markTestSkipped('Unable to run test due to https://github.com/doctrine/orm/pull/12062'); + } + } + + protected function skipIfDoctrineOrmHasBrokenEagerFetch(DbalType $primaryKey): void + { + if (!$primaryKey instanceof PrimaryKeyBase64StringType) { + self::markTestSkipped('Unable to run test due to https://github.com/doctrine/orm/pull/12130'); + } + } + + protected function skipIfPartialEntitiesAreNotSupported(): void + { + $ormVersion = InstalledVersions::getVersion('doctrine/orm') ?? '0.0.0'; + + if (version_compare($ormVersion, '3.0.0', '>=') && version_compare($ormVersion, '3.3.0', '<')) { + self::markTestSkipped('Partial entities are not supported in Doctrine ORM versions 3.0 to 3.2'); + } + } + + protected function initializeEntityManager( + DbalType $primaryKey, + QueryLogger $queryLogger, + ): void + { + if ($this->entityManager === null) { + $this->entityManager = $this->createEntityManager($primaryKey, $queryLogger); + } + } + + protected function deduceArrayParameterType(Type $dbalType): ArrayParameterType|int + { + if ($dbalType->getBindingType() === ParameterType::INTEGER) { + return ArrayParameterType::INTEGER; + } elseif ($dbalType->getBindingType() === ParameterType::STRING) { + return ArrayParameterType::STRING; + } elseif ($dbalType->getBindingType() === ParameterType::ASCII) { + return ArrayParameterType::ASCII; + } elseif ($dbalType->getBindingType() === ParameterType::BINARY) { + return ArrayParameterType::BINARY; + } else { + throw new LogicException('Unexpected binding type.'); + } + } + }