From c1cad52d0a94d67764552e624b44fbec98d8d392 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 30 Jun 2025 15:42:15 +0200 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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": {