From c1cad52d0a94d67764552e624b44fbec98d8d392 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 30 Jun 2025 15:42:15 +0200 Subject: [PATCH 01/17] Support UUIDs in PK stored in binary form --- composer.json | 2 +- phpstan.neon.dist | 4 - src/EntityPreloader.php | 74 ++++++++++++++++++- tests/EntityPreloadBlogManyHasManyTest.php | 8 +- tests/EntityPreloadBlogManyHasOneDeepTest.php | 13 ++-- tests/EntityPreloadBlogManyHasOneTest.php | 7 +- tests/EntityPreloadBlogOneHasManyDeepTest.php | 9 ++- tests/EntityPreloadBlogOneHasManyTest.php | 8 +- tests/Fixtures/Blog/Article.php | 15 +--- tests/Fixtures/Blog/BinaryId.php | 46 ++++++++++++ tests/Fixtures/Blog/BinaryIdType.php | 63 ++++++++++++++++ tests/Fixtures/Blog/BotPromptVersion.php | 15 +--- tests/Fixtures/Blog/Category.php | 15 +--- tests/Fixtures/Blog/Comment.php | 15 +--- tests/Fixtures/Blog/Contributor.php | 15 +--- tests/Fixtures/Blog/Tag.php | 15 +--- .../Fixtures/Blog/TestEntityWithBinaryId.php | 27 +++++++ tests/Lib/TestCase.php | 6 ++ 18 files changed, 257 insertions(+), 100 deletions(-) create mode 100644 tests/Fixtures/Blog/BinaryId.php create mode 100644 tests/Fixtures/Blog/BinaryIdType.php create mode 100644 tests/Fixtures/Blog/TestEntityWithBinaryId.php diff --git a/composer.json b/composer.json index 31d3919..9fdf02f 100644 --- a/composer.json +++ b/composer.json @@ -6,11 +6,11 @@ ], "require": { "php": "^8.1", + "doctrine/dbal": "^3.9 || ^4.0", "doctrine/orm": "^2.19.7 || ^3.2" }, "require-dev": { "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", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0b333e3..bd4f265 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -34,10 +34,6 @@ parameters: 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..20a8fce 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 + { + return match ($dbalType->getBindingType()) { + ParameterType::INTEGER => ArrayParameterType::INTEGER, + ParameterType::STRING => ArrayParameterType::STRING, + ParameterType::ASCII => ArrayParameterType::ASCII, + ParameterType::BINARY => ArrayParameterType::BINARY, + default => null, // @phpstan-ignore shipmonk.defaultMatchArmWithEnum + }; + } + + /** + * @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/EntityPreloadBlogManyHasManyTest.php b/tests/EntityPreloadBlogManyHasManyTest.php index 34f82dc..870acc6 100644 --- a/tests/EntityPreloadBlogManyHasManyTest.php +++ b/tests/EntityPreloadBlogManyHasManyTest.php @@ -2,9 +2,11 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\ORM\Mapping\ClassMetadata; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase; +use function array_map; class EntityPreloadBlogManyHasManyTest extends TestCase { @@ -29,13 +31,17 @@ public function testOneHasManyWithWithManualPreloadUsingPartial(): void $this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5); $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); + $rawArticleIds = array_map( + static fn (Article $article): string => $article->getId()->getBytes(), + $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, ArrayParameterType::BINARY) ->getQuery() ->getResult(); diff --git a/tests/EntityPreloadBlogManyHasOneDeepTest.php b/tests/EntityPreloadBlogManyHasOneDeepTest.php index 34733eb..a00a469 100644 --- a/tests/EntityPreloadBlogManyHasOneDeepTest.php +++ b/tests/EntityPreloadBlogManyHasOneDeepTest.php @@ -2,6 +2,7 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\ORM\Mapping\ClassMetadata; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category; @@ -35,27 +36,27 @@ public function testManyHasOneDeepWithManualPreload(): void $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); - $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) => $article->getCategory()?->getId()->getBytes(), $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)), ArrayParameterType::BINARY) ->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) => $category->getParent()?->getId()->getBytes(), $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)), ArrayParameterType::BINARY) ->getQuery() ->getResult(); } diff --git a/tests/EntityPreloadBlogManyHasOneTest.php b/tests/EntityPreloadBlogManyHasOneTest.php index 8ce7ff9..5ef3afb 100644 --- a/tests/EntityPreloadBlogManyHasOneTest.php +++ b/tests/EntityPreloadBlogManyHasOneTest.php @@ -2,6 +2,7 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\ORM\Mapping\ClassMetadata; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category; @@ -35,15 +36,15 @@ public function testManyHasOneWithManualPreload(): void $articles = $this->getEntityManager()->getRepository(Article::class)->findAll(); - $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): ?string => $article->getCategory()?->getId()->getBytes(), $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)), ArrayParameterType::BINARY) ->getQuery() ->getResult(); } diff --git a/tests/EntityPreloadBlogOneHasManyDeepTest.php b/tests/EntityPreloadBlogOneHasManyDeepTest.php index 8fda67a..f0e5090 100644 --- a/tests/EntityPreloadBlogOneHasManyDeepTest.php +++ b/tests/EntityPreloadBlogOneHasManyDeepTest.php @@ -2,6 +2,7 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\ORM\Mapping\ClassMetadata; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category; use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase; @@ -42,22 +43,26 @@ public function testOneHasManyDeepWithWithManualPreloadUsingPartial(): void ->getQuery() ->getResult(); + $rawRootCategoryIds = array_map(static fn (Category $category) => $category->getId()->getBytes(), $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, ArrayParameterType::BINARY) ->getQuery() ->getResult(); $subCategories = array_merge(...array_map(static fn (Category $category) => $category->getChildren()->toArray(), $rootCategories)); + $rawSubCategoryIds = array_map(static fn (Category $category) => $category->getId()->getBytes(), $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, ArrayParameterType::BINARY) ->getQuery() ->getResult(); diff --git a/tests/EntityPreloadBlogOneHasManyTest.php b/tests/EntityPreloadBlogOneHasManyTest.php index f28aa60..a33c2a4 100644 --- a/tests/EntityPreloadBlogOneHasManyTest.php +++ b/tests/EntityPreloadBlogOneHasManyTest.php @@ -2,10 +2,12 @@ namespace ShipMonkTests\DoctrineEntityPreloader; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\ORM\Mapping\ClassMetadata; 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 { @@ -53,13 +55,17 @@ public function testOneHasManyWithWithManualPreloadUsingPartial(): void $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); $categories = $this->getEntityManager()->getRepository(Category::class)->findAll(); + $rawCategoryIds = array_map( + static fn (Category $category): string => $category->getId()->getBytes(), + $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, ArrayParameterType::BINARY) ->getQuery() ->getResult(); diff --git a/tests/Fixtures/Blog/Article.php b/tests/Fixtures/Blog/Article.php index 88626a0..6359c7a 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 TestEntityWithBinaryId { - #[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/BinaryId.php b/tests/Fixtures/Blog/BinaryId.php new file mode 100644 index 0000000..d531e1f --- /dev/null +++ b/tests/Fixtures/Blog/BinaryId.php @@ -0,0 +1,46 @@ +hexId = $data; + } + + public static function new(): self + { + return new self(bin2hex(random_bytes(self::LENGTH))); + } + + public static function fromBytes(string $value): self + { + return new self(bin2hex($value)); + } + + public function getBytes(): string + { + $binary = hex2bin($this->hexId); + if ($binary === false) { + throw new LogicException('Cannot convert hex to binary: ' . $this->hexId); + } + return $binary; + } + + public function __toString(): string + { + return $this->getBytes(); + } + +} diff --git a/tests/Fixtures/Blog/BinaryIdType.php b/tests/Fixtures/Blog/BinaryIdType.php new file mode 100644 index 0000000..0dd0ade --- /dev/null +++ b/tests/Fixtures/Blog/BinaryIdType.php @@ -0,0 +1,63 @@ +getBytes(); + + } else { + throw new LogicException('Unexpected value: ' . $value); + } + } + + public function getSQLDeclaration( + array $column, + AbstractPlatform $platform, + ): string + { + return $platform->getBinaryTypeDeclarationSQL([ + 'length' => BinaryId::LENGTH, + 'fixed' => true, + ]); + } + + public function getBindingType(): ParameterType + { + return ParameterType::BINARY; + } + +} diff --git a/tests/Fixtures/Blog/BotPromptVersion.php b/tests/Fixtures/Blog/BotPromptVersion.php index 22cdc6b..a6f7c36 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 TestEntityWithBinaryId { - #[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..d2efa81 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 TestEntityWithBinaryId { - #[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..b84ad6f 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 TestEntityWithBinaryId { - #[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..c0d921a 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 TestEntityWithBinaryId { - #[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/Tag.php b/tests/Fixtures/Blog/Tag.php index 62264ad..a0c849f 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 TestEntityWithBinaryId { - #[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/TestEntityWithBinaryId.php b/tests/Fixtures/Blog/TestEntityWithBinaryId.php new file mode 100644 index 0000000..753e22d --- /dev/null +++ b/tests/Fixtures/Blog/TestEntityWithBinaryId.php @@ -0,0 +1,27 @@ +id = BinaryId::new(); + } + + public function getId(): BinaryId + { + return $this->id; + } + +} diff --git a/tests/Lib/TestCase.php b/tests/Lib/TestCase.php index 8d9ee48..4b94213 100644 --- a/tests/Lib/TestCase.php +++ b/tests/Lib/TestCase.php @@ -5,6 +5,7 @@ use Composer\InstalledVersions; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Logging\Middleware; +use Doctrine\DBAL\Types\Type as DbalType; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\UnderscoreNamingStrategy; @@ -17,6 +18,7 @@ use ShipMonk\DoctrineEntityPreloader\EntityPreloader; use ShipMonk\DoctrineEntityPreloader\Exception\LogicException; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; +use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\BinaryIdType; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Bot; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Comment; @@ -238,6 +240,10 @@ private function createEntityManager( $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite'] + $driverOptions, $config); $entityManager = new EntityManager($connection, $config); + if (!DbalType::hasType(BinaryIdType::NAME)) { + DbalType::addType(BinaryIdType::NAME, BinaryIdType::class); + } + $schemaTool = new SchemaTool($entityManager); $schemaTool->createSchema($entityManager->getMetadataFactory()->getAllMetadata()); From ae158999be969e5a4489ceffbeb257883746a3ee Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 30 Jun 2025 17:19:46 +0200 Subject: [PATCH 02/17] Add BinaryIdType::getName for old dbal --- tests/Fixtures/Blog/BinaryIdType.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Fixtures/Blog/BinaryIdType.php b/tests/Fixtures/Blog/BinaryIdType.php index 0dd0ade..b4a4985 100644 --- a/tests/Fixtures/Blog/BinaryIdType.php +++ b/tests/Fixtures/Blog/BinaryIdType.php @@ -55,6 +55,11 @@ public function getSQLDeclaration( ]); } + public function getName(): string + { + return self::NAME; + } + public function getBindingType(): ParameterType { return ParameterType::BINARY; From 9844e79196bcb90eb7573798ed1feabdb931c86c Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 30 Jun 2025 17:58:02 +0200 Subject: [PATCH 03/17] Compat with old dbal --- phpstan.neon.dist | 7 ++++ src/EntityPreloader.php | 4 +-- tests/Fixtures/Blog/BinaryIdType.php | 5 ++- tests/Fixtures/Compat/CompatibilityType.php | 38 +++++++++++++++++++++ 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/Fixtures/Compat/CompatibilityType.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bd4f265..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,6 +32,10 @@ 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' diff --git a/src/EntityPreloader.php b/src/EntityPreloader.php index 20a8fce..c9d59ed 100644 --- a/src/EntityPreloader.php +++ b/src/EntityPreloader.php @@ -457,14 +457,14 @@ private function loadEntitiesBy( return $queryBuilder->getQuery()->getResult(); } - private function deduceArrayParameterType(Type $dbalType): ?ArrayParameterType + 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, // @phpstan-ignore shipmonk.defaultMatchArmWithEnum + default => null, }; } diff --git a/tests/Fixtures/Blog/BinaryIdType.php b/tests/Fixtures/Blog/BinaryIdType.php index b4a4985..0eebea4 100644 --- a/tests/Fixtures/Blog/BinaryIdType.php +++ b/tests/Fixtures/Blog/BinaryIdType.php @@ -6,10 +6,13 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; use LogicException; +use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Compat\CompatibilityType; final class BinaryIdType extends Type { + use CompatibilityType; + public const NAME = 'binary_id'; public function convertToPHPValue( @@ -60,7 +63,7 @@ public function getName(): string return self::NAME; } - public function getBindingType(): ParameterType + public function doGetBindingType(): ParameterType|int // @phpstan-ignore return.unusedType (old dbal compat) { return ParameterType::BINARY; } diff --git a/tests/Fixtures/Compat/CompatibilityType.php b/tests/Fixtures/Compat/CompatibilityType.php new file mode 100644 index 0000000..dae98b2 --- /dev/null +++ b/tests/Fixtures/Compat/CompatibilityType.php @@ -0,0 +1,38 @@ +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(); + } + + } +} From dc59b919485e69f6cc3678348bfb4f4a604e4a7e Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 30 Jun 2025 18:01:13 +0200 Subject: [PATCH 04/17] Fix cs in compat file --- tests/Fixtures/Compat/CompatibilityType.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Fixtures/Compat/CompatibilityType.php b/tests/Fixtures/Compat/CompatibilityType.php index dae98b2..ed591d6 100644 --- a/tests/Fixtures/Compat/CompatibilityType.php +++ b/tests/Fixtures/Compat/CompatibilityType.php @@ -5,6 +5,9 @@ use Doctrine\DBAL\ParameterType; use function enum_exists; +// phpcs:disable Generic.Classes.DuplicateClassName.Found +// phpcs:disable Generic.Files.OneTraitPerFile.MultipleFound + if (!enum_exists(ParameterType::class)) { trait CompatibilityType { From df6f5cc9bd3c02ead8feb023f2f9a635d6e5d3a5 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 1 Jul 2025 10:24:36 +0200 Subject: [PATCH 05/17] Lowest dbal at 3.7 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9fdf02f..4f7469b 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ ], "require": { "php": "^8.1", - "doctrine/dbal": "^3.9 || ^4.0", + "doctrine/dbal": "^3.7 || ^4.0", "doctrine/orm": "^2.19.7 || ^3.2" }, "require-dev": { From 6dbcf1a31512cd06e5cd769a9822c3609ad8eb3b Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 5 Aug 2025 10:02:40 +0200 Subject: [PATCH 06/17] Bump ORM to avoid Unhandled match case of type Doctrine\DBAL\ParameterType --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 4f7469b..bdfdb18 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ "require": { "php": "^8.1", "doctrine/dbal": "^3.7 || ^4.0", - "doctrine/orm": "^2.19.7 || ^3.2" + "doctrine/orm": "^2.19.7 || ^3.5.1" }, "require-dev": { "doctrine/collections": "^2.2", From 792663a3b686961b7049662394113f59b7fa37c8 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 5 Aug 2025 10:09:04 +0200 Subject: [PATCH 07/17] Less confusion in CI tests with ORM 3 --- .github/workflows/checks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: - From a3082e4928c639e9421d897afdbab7b28e479616 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Tue, 5 Aug 2025 14:56:32 +0200 Subject: [PATCH 08/17] Keep lowest orm at 3.2 as 3.5 is very painful to use --- composer.json | 5 +++-- tests/EntityPreloadBlogManyHasOneDeepTest.php | 1 + tests/EntityPreloadBlogManyHasOneTest.php | 1 + tests/EntityPreloadBlogOneHasManyAbstractTest.php | 1 + tests/EntityPreloadBlogOneHasManyDeepTest.php | 1 + tests/EntityPreloadBlogOneHasManyTest.php | 1 + tests/Lib/TestCase.php | 8 ++++++++ 7 files changed, 16 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index bdfdb18..6719de9 100644 --- a/composer.json +++ b/composer.json @@ -7,9 +7,10 @@ "require": { "php": "^8.1", "doctrine/dbal": "^3.7 || ^4.0", - "doctrine/orm": "^2.19.7 || ^3.5.1" + "doctrine/orm": "^2.19.7 || ^3.2" }, "require-dev": { + "composer/semver": "^3.0", "doctrine/collections": "^2.2", "doctrine/persistence": "^3.3", "editorconfig-checker/editorconfig-checker": "^10.6.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/tests/EntityPreloadBlogManyHasOneDeepTest.php b/tests/EntityPreloadBlogManyHasOneDeepTest.php index a00a469..729b71d 100644 --- a/tests/EntityPreloadBlogManyHasOneDeepTest.php +++ b/tests/EntityPreloadBlogManyHasOneDeepTest.php @@ -91,6 +91,7 @@ public function testManyHasOneDeepWithFetchJoin(): void public function testManyHasOneDeepWithEagerFetchMode(): void { + $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); $this->createDummyBlogData(categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() diff --git a/tests/EntityPreloadBlogManyHasOneTest.php b/tests/EntityPreloadBlogManyHasOneTest.php index 5ef3afb..6c6fde2 100644 --- a/tests/EntityPreloadBlogManyHasOneTest.php +++ b/tests/EntityPreloadBlogManyHasOneTest.php @@ -77,6 +77,7 @@ public function testManyHasOneWithFetchJoin(): void public function testManyHasOneWithEagerFetchMode(): void { + $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() diff --git a/tests/EntityPreloadBlogOneHasManyAbstractTest.php b/tests/EntityPreloadBlogOneHasManyAbstractTest.php index f048377..e21b3f1 100644 --- a/tests/EntityPreloadBlogOneHasManyAbstractTest.php +++ b/tests/EntityPreloadBlogOneHasManyAbstractTest.php @@ -46,6 +46,7 @@ public function testOneHasManyAbstractWithFetchJoin(): void public function testOneHasManyAbstractWithEagerFetchMode(): void { + $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); $this->createDummyBlogData(categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() diff --git a/tests/EntityPreloadBlogOneHasManyDeepTest.php b/tests/EntityPreloadBlogOneHasManyDeepTest.php index f0e5090..24a5e44 100644 --- a/tests/EntityPreloadBlogOneHasManyDeepTest.php +++ b/tests/EntityPreloadBlogOneHasManyDeepTest.php @@ -97,6 +97,7 @@ public function testOneHasManyDeepWithFetchJoin(): void public function testOneHasManyDeepWithEagerFetchMode(): void { + $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); $this->createCategoryTree(depth: 5, branchingFactor: 5); $rootCategories = $this->getEntityManager()->createQueryBuilder() diff --git a/tests/EntityPreloadBlogOneHasManyTest.php b/tests/EntityPreloadBlogOneHasManyTest.php index a33c2a4..e84ad55 100644 --- a/tests/EntityPreloadBlogOneHasManyTest.php +++ b/tests/EntityPreloadBlogOneHasManyTest.php @@ -97,6 +97,7 @@ public function testOneHasManyWithFetchJoin(): void public function testOneHasManyWithEagerFetchMode(): void { + $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); $categories = $this->getEntityManager()->createQueryBuilder() diff --git a/tests/Lib/TestCase.php b/tests/Lib/TestCase.php index 4b94213..0cf83dd 100644 --- a/tests/Lib/TestCase.php +++ b/tests/Lib/TestCase.php @@ -3,6 +3,7 @@ namespace ShipMonkTests\DoctrineEntityPreloader\Lib; use Composer\InstalledVersions; +use Composer\Semver\VersionParser; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Logging\Middleware; use Doctrine\DBAL\Types\Type as DbalType; @@ -261,4 +262,11 @@ 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'); + } + } + } From a132276f889c20bfdbb005b4f6adc828f17459c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Tvrd=C3=ADk?= Date: Thu, 7 Aug 2025 16:29:58 +0200 Subject: [PATCH 09/17] remove convertFieldValuesToDatabaseValues() --- src/EntityPreloader.php | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/EntityPreloader.php b/src/EntityPreloader.php index c9d59ed..9da7b03 100644 --- a/src/EntityPreloader.php +++ b/src/EntityPreloader.php @@ -331,7 +331,7 @@ private function preloadManyToManyInner( ->andWhere('source IN (:sourceEntityIds)') ->setParameter( 'sourceEntityIds', - $this->convertFieldValuesToDatabaseValues($sourceIdentifierType, $uninitializedSourceEntityIdsChunk), + $uninitializedSourceEntityIdsChunk, $this->deduceArrayParameterType($sourceIdentifierType), ) ->getQuery() @@ -444,7 +444,7 @@ private function loadEntitiesBy( ->andWhere("{$rootLevelAlias}.{$fieldName} IN (:fieldValues)") ->setParameter( 'fieldValues', - $this->convertFieldValuesToDatabaseValues($referencedType, $fieldValues), + $fieldValues, $this->deduceArrayParameterType($referencedType), ); @@ -468,26 +468,6 @@ private function deduceArrayParameterType(Type $dbalType): ArrayParameterType|in }; } - /** - * @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 * From 855b62f4d2d44e6fa45a06dd3990ad1134962269 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 15 Aug 2025 12:46:43 +0200 Subject: [PATCH 10/17] Test custom PKs stored as binary/string/integer --- ...tityPreloadBlogManyHasManyInversedTest.php | 22 +++--- tests/EntityPreloadBlogManyHasManyTest.php | 33 +++++---- tests/EntityPreloadBlogManyHasOneDeepTest.php | 37 ++++++---- tests/EntityPreloadBlogManyHasOneTest.php | 33 +++++---- ...ntityPreloadBlogOneHasManyAbstractTest.php | 22 +++--- tests/EntityPreloadBlogOneHasManyDeepTest.php | 42 ++++++----- tests/EntityPreloadBlogOneHasManyTest.php | 38 ++++++---- tests/EntityPreloadSyntheticTest.php | 8 +++ tests/Fixtures/Blog/Article.php | 2 +- tests/Fixtures/Blog/BinaryId.php | 46 ------------- tests/Fixtures/Blog/BotPromptVersion.php | 2 +- tests/Fixtures/Blog/Category.php | 2 +- tests/Fixtures/Blog/Comment.php | 2 +- tests/Fixtures/Blog/Contributor.php | 2 +- tests/Fixtures/Blog/PrimaryKey.php | 39 +++++++++++ tests/Fixtures/Blog/Tag.php | 2 +- ...php => TestEntityWithCustomPrimaryKey.php} | 10 +-- .../PrimaryKeyBinaryType.php} | 23 ++++--- .../Blog/Type/PrimaryKeyIntegerType.php | 69 +++++++++++++++++++ .../Blog/Type/PrimaryKeyStringType.php | 67 ++++++++++++++++++ tests/Lib/TestCase.php | 58 ++++++++++++++-- 21 files changed, 399 insertions(+), 160 deletions(-) delete mode 100644 tests/Fixtures/Blog/BinaryId.php create mode 100644 tests/Fixtures/Blog/PrimaryKey.php rename tests/Fixtures/Blog/{TestEntityWithBinaryId.php => TestEntityWithCustomPrimaryKey.php} (59%) rename tests/Fixtures/Blog/{BinaryIdType.php => Type/PrimaryKeyBinaryType.php} (67%) create mode 100644 tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php create mode 100644 tests/Fixtures/Blog/Type/PrimaryKeyStringType.php 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 870acc6..0869ca9 100644 --- a/tests/EntityPreloadBlogManyHasManyTest.php +++ b/tests/EntityPreloadBlogManyHasManyTest.php @@ -2,8 +2,9 @@ namespace ShipMonkTests\DoctrineEntityPreloader; -use Doctrine\DBAL\ArrayParameterType; +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; @@ -11,9 +12,10 @@ 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(); @@ -25,14 +27,16 @@ 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): string => $article->getId()->getBytes(), + static fn (Article $article) => $primaryKey->convertToDatabaseValue($article->getId(), $platform), $articles, ); @@ -41,7 +45,7 @@ public function testOneHasManyWithWithManualPreloadUsingPartial(): void ->from(Article::class, 'article') ->leftJoin('article.tags', 'tag') ->where('article IN (:articles)') - ->setParameter('articles', $rawArticleIds, ArrayParameterType::BINARY) + ->setParameter('articles', $rawArticleIds, $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); @@ -53,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') @@ -71,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 @@ -92,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 729b71d..5e54275 100644 --- a/tests/EntityPreloadBlogManyHasOneDeepTest.php +++ b/tests/EntityPreloadBlogManyHasOneDeepTest.php @@ -2,8 +2,9 @@ namespace ShipMonkTests\DoctrineEntityPreloader; -use Doctrine\DBAL\ArrayParameterType; +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; @@ -16,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(); @@ -30,13 +32,15 @@ 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()->getBytes(), $articles); + $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) { @@ -44,11 +48,11 @@ public function testManyHasOneDeepWithManualPreload(): void ->select('category') ->from(Category::class, 'category') ->where('category.id IN (:ids)') - ->setParameter('ids', array_values(array_unique($categoryIds)), ArrayParameterType::BINARY) + ->setParameter('ids', array_values(array_unique($categoryIds)), $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); - $parentCategoryIds = array_map(static fn (Category $category) => $category->getParent()?->getId()->getBytes(), $categories); + $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) { @@ -56,7 +60,7 @@ public function testManyHasOneDeepWithManualPreload(): void ->select('category') ->from(Category::class, 'category') ->where('category.id IN (:ids)') - ->setParameter('ids', array_values(array_unique($parentCategoryIds)), ArrayParameterType::BINARY) + ->setParameter('ids', array_values(array_unique($parentCategoryIds)), $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); } @@ -70,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') @@ -89,10 +94,11 @@ public function testManyHasOneDeepWithFetchJoin(): void ]); } - public function testManyHasOneDeepWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneDeepWithEagerFetchMode(DbalType $primaryKey): void { $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); - $this->createDummyBlogData(categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() ->select('article') @@ -111,9 +117,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 6c6fde2..178611b 100644 --- a/tests/EntityPreloadBlogManyHasOneTest.php +++ b/tests/EntityPreloadBlogManyHasOneTest.php @@ -2,8 +2,9 @@ namespace ShipMonkTests\DoctrineEntityPreloader; -use Doctrine\DBAL\ArrayParameterType; +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; @@ -16,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(); @@ -30,13 +32,15 @@ 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): ?string => $article->getCategory()?->getId()->getBytes(), $articles); + $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) { @@ -44,7 +48,7 @@ public function testManyHasOneWithManualPreload(): void ->select('category') ->from(Category::class, 'category') ->where('category.id IN (:ids)') - ->setParameter('ids', array_values(array_unique($categoryIds)), ArrayParameterType::BINARY) + ->setParameter('ids', array_values(array_unique($categoryIds)), $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); } @@ -57,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') @@ -75,10 +80,11 @@ public function testManyHasOneWithFetchJoin(): void ]); } - public function testManyHasOneWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testManyHasOneWithEagerFetchMode(DbalType $primaryKey): void { $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() ->select('article') @@ -95,9 +101,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 e21b3f1..bb0199e 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,10 +48,11 @@ public function testOneHasManyAbstractWithFetchJoin(): void ]); } - public function testOneHasManyAbstractWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyAbstractWithEagerFetchMode(DbalType $primaryKey): void { $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); - $this->createDummyBlogData(categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() ->select('article') @@ -66,9 +71,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 24a5e44..109f26c 100644 --- a/tests/EntityPreloadBlogOneHasManyDeepTest.php +++ b/tests/EntityPreloadBlogOneHasManyDeepTest.php @@ -2,8 +2,9 @@ namespace ShipMonkTests\DoctrineEntityPreloader; -use Doctrine\DBAL\ArrayParameterType; +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; @@ -12,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') @@ -31,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') @@ -43,26 +46,27 @@ public function testOneHasManyDeepWithWithManualPreloadUsingPartial(): void ->getQuery() ->getResult(); - $rawRootCategoryIds = array_map(static fn (Category $category) => $category->getId()->getBytes(), $rootCategories); + $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', $rawRootCategoryIds, ArrayParameterType::BINARY) + ->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) => $category->getId()->getBytes(), $subCategories); + $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', $rawSubCategoryIds, ArrayParameterType::BINARY) + ->setParameter('subCategories', $rawSubCategoryIds, $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); @@ -75,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') @@ -95,10 +100,11 @@ public function testOneHasManyDeepWithFetchJoin(): void ]); } - public function testOneHasManyDeepWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyDeepWithEagerFetchMode(DbalType $primaryKey): void { $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); - $this->createCategoryTree(depth: 5, branchingFactor: 5); + $this->createCategoryTree($primaryKey, depth: 5, branchingFactor: 5); $rootCategories = $this->getEntityManager()->createQueryBuilder() ->select('category') @@ -117,9 +123,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') @@ -141,17 +148,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 e84ad55..a1a8079 100644 --- a/tests/EntityPreloadBlogOneHasManyTest.php +++ b/tests/EntityPreloadBlogOneHasManyTest.php @@ -2,8 +2,9 @@ namespace ShipMonkTests\DoctrineEntityPreloader; -use Doctrine\DBAL\ArrayParameterType; +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; @@ -12,9 +13,10 @@ 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(); @@ -26,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(); @@ -49,14 +52,16 @@ 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): string => $category->getId()->getBytes(), + static fn (Category $category) => $primaryKey->convertToDatabaseValue($category->getId(), $platform), $categories, ); @@ -65,7 +70,7 @@ public function testOneHasManyWithWithManualPreloadUsingPartial(): void ->from(Category::class, 'category') ->leftJoin('category.articles', 'article') ->where('category IN (:categories)') - ->setParameter('categories', $rawCategoryIds, ArrayParameterType::BINARY) + ->setParameter('categories', $rawCategoryIds, $this->deduceArrayParameterType($primaryKey)) ->getQuery() ->getResult(); @@ -77,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') @@ -95,10 +101,11 @@ public function testOneHasManyWithFetchJoin(): void ]); } - public function testOneHasManyWithEagerFetchMode(): void + #[DataProvider('providePrimaryKeyTypes')] + public function testOneHasManyWithEagerFetchMode(DbalType $primaryKey): void { $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); - $this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5); + $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $categories = $this->getEntityManager()->createQueryBuilder() ->select('category') @@ -115,9 +122,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 6359c7a..767ea7d 100644 --- a/tests/Fixtures/Blog/Article.php +++ b/tests/Fixtures/Blog/Article.php @@ -13,7 +13,7 @@ use Doctrine\ORM\Mapping\OrderBy; #[Entity] -class Article extends TestEntityWithBinaryId +class Article extends TestEntityWithCustomPrimaryKey { #[Column] diff --git a/tests/Fixtures/Blog/BinaryId.php b/tests/Fixtures/Blog/BinaryId.php deleted file mode 100644 index d531e1f..0000000 --- a/tests/Fixtures/Blog/BinaryId.php +++ /dev/null @@ -1,46 +0,0 @@ -hexId = $data; - } - - public static function new(): self - { - return new self(bin2hex(random_bytes(self::LENGTH))); - } - - public static function fromBytes(string $value): self - { - return new self(bin2hex($value)); - } - - public function getBytes(): string - { - $binary = hex2bin($this->hexId); - if ($binary === false) { - throw new LogicException('Cannot convert hex to binary: ' . $this->hexId); - } - return $binary; - } - - public function __toString(): string - { - return $this->getBytes(); - } - -} diff --git a/tests/Fixtures/Blog/BotPromptVersion.php b/tests/Fixtures/Blog/BotPromptVersion.php index a6f7c36..8dd277c 100644 --- a/tests/Fixtures/Blog/BotPromptVersion.php +++ b/tests/Fixtures/Blog/BotPromptVersion.php @@ -7,7 +7,7 @@ use Doctrine\ORM\Mapping\OneToOne; #[Entity] -class BotPromptVersion extends TestEntityWithBinaryId +class BotPromptVersion extends TestEntityWithCustomPrimaryKey { #[Column] diff --git a/tests/Fixtures/Blog/Category.php b/tests/Fixtures/Blog/Category.php index d2efa81..0df7ec7 100644 --- a/tests/Fixtures/Blog/Category.php +++ b/tests/Fixtures/Blog/Category.php @@ -11,7 +11,7 @@ use Doctrine\ORM\Mapping\OneToMany; #[Entity] -class Category extends TestEntityWithBinaryId +class Category extends TestEntityWithCustomPrimaryKey { #[Column] diff --git a/tests/Fixtures/Blog/Comment.php b/tests/Fixtures/Blog/Comment.php index b84ad6f..246ceab 100644 --- a/tests/Fixtures/Blog/Comment.php +++ b/tests/Fixtures/Blog/Comment.php @@ -7,7 +7,7 @@ use Doctrine\ORM\Mapping\ManyToOne; #[Entity] -class Comment extends TestEntityWithBinaryId +class Comment extends TestEntityWithCustomPrimaryKey { #[ManyToOne(targetEntity: Article::class, inversedBy: 'comments')] diff --git a/tests/Fixtures/Blog/Contributor.php b/tests/Fixtures/Blog/Contributor.php index c0d921a..da1d227 100644 --- a/tests/Fixtures/Blog/Contributor.php +++ b/tests/Fixtures/Blog/Contributor.php @@ -13,7 +13,7 @@ #[Entity] #[InheritanceType('SINGLE_TABLE')] -abstract class Contributor extends TestEntityWithBinaryId +abstract class Contributor extends TestEntityWithCustomPrimaryKey { #[Column] diff --git a/tests/Fixtures/Blog/PrimaryKey.php b/tests/Fixtures/Blog/PrimaryKey.php new file mode 100644 index 0000000..5cdb50b --- /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 md5((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 a0c849f..945547a 100644 --- a/tests/Fixtures/Blog/Tag.php +++ b/tests/Fixtures/Blog/Tag.php @@ -10,7 +10,7 @@ use Doctrine\ORM\Mapping\ManyToMany; #[Entity] -class Tag extends TestEntityWithBinaryId +class Tag extends TestEntityWithCustomPrimaryKey { #[Column] diff --git a/tests/Fixtures/Blog/TestEntityWithBinaryId.php b/tests/Fixtures/Blog/TestEntityWithCustomPrimaryKey.php similarity index 59% rename from tests/Fixtures/Blog/TestEntityWithBinaryId.php rename to tests/Fixtures/Blog/TestEntityWithCustomPrimaryKey.php index 753e22d..43270a3 100644 --- a/tests/Fixtures/Blog/TestEntityWithBinaryId.php +++ b/tests/Fixtures/Blog/TestEntityWithCustomPrimaryKey.php @@ -7,19 +7,19 @@ use Doctrine\ORM\Mapping\MappedSuperclass; #[MappedSuperclass] -abstract class TestEntityWithBinaryId +abstract class TestEntityWithCustomPrimaryKey { #[Id] - #[Column(type: BinaryIdType::NAME, nullable: false)] - private BinaryId $id; + #[Column(type: PrimaryKey::DOCTRINE_TYPE_NAME, nullable: false)] + private PrimaryKey $id; protected function __construct() { - $this->id = BinaryId::new(); + $this->id = PrimaryKey::new(); } - public function getId(): BinaryId + public function getId(): PrimaryKey { return $this->id; } diff --git a/tests/Fixtures/Blog/BinaryIdType.php b/tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php similarity index 67% rename from tests/Fixtures/Blog/BinaryIdType.php rename to tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php index 0eebea4..7ff235a 100644 --- a/tests/Fixtures/Blog/BinaryIdType.php +++ b/tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php @@ -1,34 +1,35 @@ getBytes(); + } elseif ($value instanceof PrimaryKey) { + return pack('N', $value->getData()); } else { throw new LogicException('Unexpected value: ' . $value); @@ -53,14 +54,14 @@ public function getSQLDeclaration( ): string { return $platform->getBinaryTypeDeclarationSQL([ - 'length' => BinaryId::LENGTH, + 'length' => PrimaryKey::LENGTH_BYTES, 'fixed' => true, ]); } public function getName(): string { - return self::NAME; + return PrimaryKey::DOCTRINE_TYPE_NAME; } public function doGetBindingType(): ParameterType|int // @phpstan-ignore return.unusedType (old dbal compat) diff --git a/tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php b/tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php new file mode 100644 index 0000000..d515fe2 --- /dev/null +++ b/tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php @@ -0,0 +1,69 @@ +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..566ca59 --- /dev/null +++ b/tests/Fixtures/Blog/Type/PrimaryKeyStringType.php @@ -0,0 +1,67 @@ +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/Lib/TestCase.php b/tests/Lib/TestCase.php index 0cf83dd..2cf82d6 100644 --- a/tests/Lib/TestCase.php +++ b/tests/Lib/TestCase.php @@ -4,8 +4,11 @@ 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; @@ -19,11 +22,14 @@ use ShipMonk\DoctrineEntityPreloader\EntityPreloader; use ShipMonk\DoctrineEntityPreloader\Exception\LogicException; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article; -use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\BinaryIdType; 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\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; @@ -41,6 +47,16 @@ 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()]; + } + protected function setUp(): void { parent::setUp(); @@ -94,6 +110,7 @@ protected function assertAggregatedQueries( } protected function createDummyBlogData( + DbalType $dbalType, int $categoryCount = 1, int $categoryParentsCount = 0, int $articleInEachCategoryCount = 1, @@ -102,6 +119,7 @@ protected function createDummyBlogData( int $promptChangeCount = 0, ): void { + $this->initializeEntityManager($dbalType, $this->getQueryLogger()); $entityManager = $this->getEntityManager(); for ($h = 0; $h < $categoryCount; $h++) { @@ -204,7 +222,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; } /** @@ -221,6 +242,7 @@ private function createQueryLogger(): QueryLogger } private function createEntityManager( + DbalType $primaryKey, LoggerInterface $logger, bool $inMemory = true, ): EntityManagerInterface @@ -241,8 +263,11 @@ private function createEntityManager( $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite'] + $driverOptions, $config); $entityManager = new EntityManager($connection, $config); - if (!DbalType::hasType(BinaryIdType::NAME)) { - DbalType::addType(BinaryIdType::NAME, BinaryIdType::class); + $typeRegistry = DbalType::getTypeRegistry(); + if ($typeRegistry->has(PrimaryKey::DOCTRINE_TYPE_NAME)) { + $typeRegistry->override(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey); + } else { + $typeRegistry->register(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey); } $schemaTool = new SchemaTool($entityManager); @@ -269,4 +294,29 @@ protected function skipIfDoctrineOrmHasBrokenUnhandledMatchCase(): void } } + protected function initializeEntityManager( + DbalType $primaryKey, + QueryLogger $queryLogger, + ): void + { + if ($this->entityManager === null) { + $this->entityManager = $this->createEntityManager($primaryKey, $queryLogger); + } + } + + protected function deduceArrayParameterType(Type $dbalType): ArrayParameterType + { + 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.'); + } + } + } From 7665f49a11d34477ae1f7e0df8e495b61bb6c394 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 15 Aug 2025 14:06:15 +0200 Subject: [PATCH 11/17] Revert "remove convertFieldValuesToDatabaseValues()" This reverts commit a132276f889c20bfdbb005b4f6adc828f17459c8. --- src/EntityPreloader.php | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/EntityPreloader.php b/src/EntityPreloader.php index 9da7b03..c9d59ed 100644 --- a/src/EntityPreloader.php +++ b/src/EntityPreloader.php @@ -331,7 +331,7 @@ private function preloadManyToManyInner( ->andWhere('source IN (:sourceEntityIds)') ->setParameter( 'sourceEntityIds', - $uninitializedSourceEntityIdsChunk, + $this->convertFieldValuesToDatabaseValues($sourceIdentifierType, $uninitializedSourceEntityIdsChunk), $this->deduceArrayParameterType($sourceIdentifierType), ) ->getQuery() @@ -444,7 +444,7 @@ private function loadEntitiesBy( ->andWhere("{$rootLevelAlias}.{$fieldName} IN (:fieldValues)") ->setParameter( 'fieldValues', - $fieldValues, + $this->convertFieldValuesToDatabaseValues($referencedType, $fieldValues), $this->deduceArrayParameterType($referencedType), ); @@ -468,6 +468,26 @@ private function deduceArrayParameterType(Type $dbalType): ArrayParameterType|in }; } + /** + * @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 * From af13c747e375b442e2469f0d5c1173b6bf37d4dc Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 15 Aug 2025 16:05:46 +0200 Subject: [PATCH 12/17] Use DBAL static methods instead of type registry --- tests/Lib/TestCase.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/Lib/TestCase.php b/tests/Lib/TestCase.php index 2cf82d6..52d17c8 100644 --- a/tests/Lib/TestCase.php +++ b/tests/Lib/TestCase.php @@ -263,11 +263,10 @@ private function createEntityManager( $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite'] + $driverOptions, $config); $entityManager = new EntityManager($connection, $config); - $typeRegistry = DbalType::getTypeRegistry(); - if ($typeRegistry->has(PrimaryKey::DOCTRINE_TYPE_NAME)) { - $typeRegistry->override(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey); + if (DbalType::hasType(PrimaryKey::DOCTRINE_TYPE_NAME)) { + DbalType::overrideType(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey); } else { - $typeRegistry->register(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey); + DbalType::addType(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey); } $schemaTool = new SchemaTool($entityManager); From de07ff86660cfc82fd71dc02154cb34928578a06 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Fri, 15 Aug 2025 16:12:48 +0200 Subject: [PATCH 13/17] Simplify PrimaryKeyTypes --- tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php | 8 +++++--- tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php | 8 +++++--- tests/Fixtures/Blog/Type/PrimaryKeyStringType.php | 8 +++++--- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php b/tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php index 7ff235a..b43e9a3 100644 --- a/tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php +++ b/tests/Fixtures/Blog/Type/PrimaryKeyBinaryType.php @@ -8,6 +8,8 @@ use LogicException; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\PrimaryKey; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Compat\CompatibilityType; +use function get_debug_type; +use function is_string; use function pack; use function unpack; @@ -25,11 +27,11 @@ public function convertToPHPValue( return null; } - if ($value instanceof PrimaryKey) { - return $value; + if (is_string($value)) { + return new PrimaryKey(unpack('N', $value)[1]); // @phpstan-ignore offsetAccess.nonOffsetAccessible } - return new PrimaryKey(unpack('N', $value)[1]); // @phpstan-ignore offsetAccess.nonOffsetAccessible + throw new LogicException('Unexpected value: ' . get_debug_type($value)); } public function convertToDatabaseValue( diff --git a/tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php b/tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php index d515fe2..e89eea9 100644 --- a/tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php +++ b/tests/Fixtures/Blog/Type/PrimaryKeyIntegerType.php @@ -8,6 +8,8 @@ use LogicException; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\PrimaryKey; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Compat\CompatibilityType; +use function get_debug_type; +use function is_int; final class PrimaryKeyIntegerType extends Type { @@ -23,11 +25,11 @@ public function convertToPHPValue( return null; } - if ($value instanceof PrimaryKey) { - return $value; + if (is_int($value)) { + return new PrimaryKey($value); } - return new PrimaryKey($value); + throw new LogicException('Unexpected value: ' . get_debug_type($value)); } public function convertToDatabaseValue( diff --git a/tests/Fixtures/Blog/Type/PrimaryKeyStringType.php b/tests/Fixtures/Blog/Type/PrimaryKeyStringType.php index 566ca59..11b75db 100644 --- a/tests/Fixtures/Blog/Type/PrimaryKeyStringType.php +++ b/tests/Fixtures/Blog/Type/PrimaryKeyStringType.php @@ -8,6 +8,8 @@ use LogicException; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\PrimaryKey; use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Compat\CompatibilityType; +use function get_debug_type; +use function is_string; final class PrimaryKeyStringType extends Type { @@ -23,11 +25,11 @@ public function convertToPHPValue( return null; } - if ($value instanceof PrimaryKey) { - return $value; + if (is_string($value)) { + return new PrimaryKey((int) $value); } - return new PrimaryKey((int) $value); + throw new LogicException('Unexpected value: ' . get_debug_type($value)); } public function convertToDatabaseValue( From 7dc8e392383450e224cb81bdf638f5e4aacc5968 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 18 Aug 2025 12:54:32 +0200 Subject: [PATCH 14/17] Fix tests for old doctrine --- tests/Lib/TestCase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Lib/TestCase.php b/tests/Lib/TestCase.php index 52d17c8..14374b4 100644 --- a/tests/Lib/TestCase.php +++ b/tests/Lib/TestCase.php @@ -303,7 +303,7 @@ protected function initializeEntityManager( } } - protected function deduceArrayParameterType(Type $dbalType): ArrayParameterType + protected function deduceArrayParameterType(Type $dbalType): ArrayParameterType|int { if ($dbalType->getBindingType() === ParameterType::INTEGER) { return ArrayParameterType::INTEGER; From 2d216f4054465ee3edd39734d9b0b3b3dacdd85a Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 18 Aug 2025 13:16:11 +0200 Subject: [PATCH 15/17] Skip tests broken by ORM bug and add one working testcase for each --- tests/EntityPreloadBlogManyHasOneDeepTest.php | 1 + tests/EntityPreloadBlogManyHasOneTest.php | 1 + ...ntityPreloadBlogOneHasManyAbstractTest.php | 1 + tests/EntityPreloadBlogOneHasManyDeepTest.php | 1 + tests/EntityPreloadBlogOneHasManyTest.php | 1 + tests/Fixtures/Blog/PrimaryKey.php | 4 +- .../Blog/Type/PrimaryKeyBase64StringType.php | 75 +++++++++++++++++++ tests/Lib/TestCase.php | 9 +++ 8 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 tests/Fixtures/Blog/Type/PrimaryKeyBase64StringType.php diff --git a/tests/EntityPreloadBlogManyHasOneDeepTest.php b/tests/EntityPreloadBlogManyHasOneDeepTest.php index 5e54275..9762b16 100644 --- a/tests/EntityPreloadBlogManyHasOneDeepTest.php +++ b/tests/EntityPreloadBlogManyHasOneDeepTest.php @@ -98,6 +98,7 @@ public function testManyHasOneDeepWithFetchJoin(DbalType $primaryKey): void public function testManyHasOneDeepWithEagerFetchMode(DbalType $primaryKey): void { $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); + $this->skipIfDoctrineOrmHasBrokenEagerFetch($primaryKey); $this->createDummyBlogData($primaryKey, categoryCount: 5, categoryParentsCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() diff --git a/tests/EntityPreloadBlogManyHasOneTest.php b/tests/EntityPreloadBlogManyHasOneTest.php index 178611b..1e0082d 100644 --- a/tests/EntityPreloadBlogManyHasOneTest.php +++ b/tests/EntityPreloadBlogManyHasOneTest.php @@ -84,6 +84,7 @@ public function testManyHasOneWithFetchJoin(DbalType $primaryKey): void public function testManyHasOneWithEagerFetchMode(DbalType $primaryKey): void { $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); + $this->skipIfDoctrineOrmHasBrokenEagerFetch($primaryKey); $this->createDummyBlogData($primaryKey, categoryCount: 5, articleInEachCategoryCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() diff --git a/tests/EntityPreloadBlogOneHasManyAbstractTest.php b/tests/EntityPreloadBlogOneHasManyAbstractTest.php index bb0199e..f1486d7 100644 --- a/tests/EntityPreloadBlogOneHasManyAbstractTest.php +++ b/tests/EntityPreloadBlogOneHasManyAbstractTest.php @@ -52,6 +52,7 @@ public function testOneHasManyAbstractWithFetchJoin(DbalType $primaryKey): void public function testOneHasManyAbstractWithEagerFetchMode(DbalType $primaryKey): void { $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); + $this->skipIfDoctrineOrmHasBrokenEagerFetch($primaryKey); $this->createDummyBlogData($primaryKey, categoryCount: 1, articleInEachCategoryCount: 5, commentForEachArticleCount: 5); $articles = $this->getEntityManager()->createQueryBuilder() diff --git a/tests/EntityPreloadBlogOneHasManyDeepTest.php b/tests/EntityPreloadBlogOneHasManyDeepTest.php index 109f26c..72dcac5 100644 --- a/tests/EntityPreloadBlogOneHasManyDeepTest.php +++ b/tests/EntityPreloadBlogOneHasManyDeepTest.php @@ -104,6 +104,7 @@ public function testOneHasManyDeepWithFetchJoin(DbalType $primaryKey): void public function testOneHasManyDeepWithEagerFetchMode(DbalType $primaryKey): void { $this->skipIfDoctrineOrmHasBrokenUnhandledMatchCase(); + $this->skipIfDoctrineOrmHasBrokenEagerFetch($primaryKey); $this->createCategoryTree($primaryKey, depth: 5, branchingFactor: 5); $rootCategories = $this->getEntityManager()->createQueryBuilder() diff --git a/tests/EntityPreloadBlogOneHasManyTest.php b/tests/EntityPreloadBlogOneHasManyTest.php index a1a8079..ac02ffd 100644 --- a/tests/EntityPreloadBlogOneHasManyTest.php +++ b/tests/EntityPreloadBlogOneHasManyTest.php @@ -105,6 +105,7 @@ public function testOneHasManyWithFetchJoin(DbalType $primaryKey): void public function testOneHasManyWithEagerFetchMode(DbalType $primaryKey): void { $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() diff --git a/tests/Fixtures/Blog/PrimaryKey.php b/tests/Fixtures/Blog/PrimaryKey.php index 5cdb50b..210d934 100644 --- a/tests/Fixtures/Blog/PrimaryKey.php +++ b/tests/Fixtures/Blog/PrimaryKey.php @@ -2,7 +2,7 @@ namespace ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog; -use function md5; +use function base64_encode; use function random_int; class PrimaryKey @@ -33,7 +33,7 @@ public function getData(): int public function __toString(): string { - return md5((string) $this->data); // intentionally not matching any internal PK representation + return base64_encode((string) $this->data); // intentionally not matching any internal PK representation } } 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/Lib/TestCase.php b/tests/Lib/TestCase.php index 14374b4..f157c0b 100644 --- a/tests/Lib/TestCase.php +++ b/tests/Lib/TestCase.php @@ -27,6 +27,7 @@ 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; @@ -55,6 +56,7 @@ 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 @@ -293,6 +295,13 @@ protected function skipIfDoctrineOrmHasBrokenUnhandledMatchCase(): void } } + 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 initializeEntityManager( DbalType $primaryKey, QueryLogger $queryLogger, From d24ac05a10b1375fc37c0d14945030542177a7ab Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 18 Aug 2025 13:17:42 +0200 Subject: [PATCH 16/17] Move skipX methods closer together --- tests/Lib/TestCase.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/Lib/TestCase.php b/tests/Lib/TestCase.php index f157c0b..594d8da 100644 --- a/tests/Lib/TestCase.php +++ b/tests/Lib/TestCase.php @@ -208,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(); @@ -302,6 +293,15 @@ protected function skipIfDoctrineOrmHasBrokenEagerFetch(DbalType $primaryKey): v } } + 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, From 62fb93c3123321776c567d014494652c088134ec Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 18 Aug 2025 13:27:26 +0200 Subject: [PATCH 17/17] DbalType::addType: Fix broken edgecase combination of dbal+orm versions --- tests/Lib/TestCase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Lib/TestCase.php b/tests/Lib/TestCase.php index 594d8da..df0c3c1 100644 --- a/tests/Lib/TestCase.php +++ b/tests/Lib/TestCase.php @@ -257,9 +257,9 @@ private function createEntityManager( $entityManager = new EntityManager($connection, $config); if (DbalType::hasType(PrimaryKey::DOCTRINE_TYPE_NAME)) { - DbalType::overrideType(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey); + DbalType::overrideType(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey::class); } else { - DbalType::addType(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey); + DbalType::addType(PrimaryKey::DOCTRINE_TYPE_NAME, $primaryKey::class); } $schemaTool = new SchemaTool($entityManager);