Skip to content

Commit c1cad52

Browse files
committed
Support UUIDs in PK stored in binary form
1 parent 03ca7e1 commit c1cad52

18 files changed

+257
-100
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
],
77
"require": {
88
"php": "^8.1",
9+
"doctrine/dbal": "^3.9 || ^4.0",
910
"doctrine/orm": "^2.19.7 || ^3.2"
1011
},
1112
"require-dev": {
1213
"doctrine/collections": "^2.2",
13-
"doctrine/dbal": "^3.9 || ^4.0",
1414
"doctrine/persistence": "^3.3",
1515
"editorconfig-checker/editorconfig-checker": "^10.6.0",
1616
"ergebnis/composer-normalize": "^2.42.0",

phpstan.neon.dist

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ parameters:
3434
identifier: 'booleanOr.alwaysFalse'
3535
reportUnmatched: false
3636
path: 'src/EntityPreloader.php'
37-
-
38-
message: '#has an uninitialized property \$id#'
39-
identifier: 'property.uninitialized'
40-
path: 'tests/Fixtures/Blog'
4137
-
4238
identifier: 'property.onlyWritten'
4339
path: 'tests/Fixtures/Synthetic'

src/EntityPreloader.php

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
namespace ShipMonk\DoctrineEntityPreloader;
44

55
use ArrayAccess;
6+
use Doctrine\DBAL\ArrayParameterType;
7+
use Doctrine\DBAL\ParameterType;
8+
use Doctrine\DBAL\Types\Type;
69
use Doctrine\ORM\EntityManagerInterface;
710
use Doctrine\ORM\Mapping\ClassMetadata;
811
use Doctrine\ORM\PersistentCollection;
@@ -141,7 +144,7 @@ private function loadProxies(
141144
}
142145

143146
foreach (array_chunk($uninitializedIds, $batchSize) as $idsChunk) {
144-
$this->loadEntitiesBy($classMetadata, $identifierName, $idsChunk, $maxFetchJoinSameFieldCount);
147+
$this->loadEntitiesBy($classMetadata, $identifierName, $classMetadata, $idsChunk, $maxFetchJoinSameFieldCount);
145148
}
146149

147150
return array_values($uniqueEntities);
@@ -270,6 +273,7 @@ private function preloadOneToManyInner(
270273
$targetEntitiesList = $this->loadEntitiesBy(
271274
$targetClassMetadata,
272275
$targetPropertyName,
276+
$sourceClassMetadata,
273277
$uninitializedSourceEntityIdsChunk,
274278
$maxFetchJoinSameFieldCount,
275279
$associationMapping['orderBy'] ?? [],
@@ -318,12 +322,18 @@ private function preloadManyToManyInner(
318322
$sourceIdentifierName = $sourceClassMetadata->getSingleIdentifierFieldName();
319323
$targetIdentifierName = $targetClassMetadata->getSingleIdentifierFieldName();
320324

325+
$sourceIdentifierType = $this->getIdentifierFieldType($sourceClassMetadata);
326+
321327
$manyToManyRows = $this->entityManager->createQueryBuilder()
322328
->select("source.{$sourceIdentifierName} AS sourceId", "target.{$targetIdentifierName} AS targetId")
323329
->from($sourceClassMetadata->getName(), 'source')
324330
->join("source.{$sourcePropertyName}", 'target')
325331
->andWhere('source IN (:sourceEntityIds)')
326-
->setParameter('sourceEntityIds', $uninitializedSourceEntityIdsChunk)
332+
->setParameter(
333+
'sourceEntityIds',
334+
$this->convertFieldValuesToDatabaseValues($sourceIdentifierType, $uninitializedSourceEntityIdsChunk),
335+
$this->deduceArrayParameterType($sourceIdentifierType),
336+
)
327337
->getQuery()
328338
->getResult();
329339

@@ -345,7 +355,7 @@ private function preloadManyToManyInner(
345355
$uninitializedTargetEntityIds[$targetEntityKey] = $targetEntityId;
346356
}
347357

348-
foreach ($this->loadEntitiesBy($targetClassMetadata, $targetIdentifierName, array_values($uninitializedTargetEntityIds), $maxFetchJoinSameFieldCount) as $targetEntity) {
358+
foreach ($this->loadEntitiesBy($targetClassMetadata, $targetIdentifierName, $sourceClassMetadata, array_values($uninitializedTargetEntityIds), $maxFetchJoinSameFieldCount) as $targetEntity) {
349359
$targetEntityKey = (string) $targetIdentifierReflection->getValue($targetEntity);
350360
$targetEntities[$targetEntityKey] = $targetEntity;
351361
}
@@ -404,15 +414,18 @@ private function preloadToOne(
404414
/**
405415
* @param ClassMetadata<T> $targetClassMetadata
406416
* @param list<mixed> $fieldValues
417+
* @param ClassMetadata<R> $referencedClassMetadata
407418
* @param non-negative-int $maxFetchJoinSameFieldCount
408419
* @param array<string, 'asc'|'desc'> $orderBy
409420
* @return list<T>
410421
*
411422
* @template T of E
423+
* @template R of E
412424
*/
413425
private function loadEntitiesBy(
414426
ClassMetadata $targetClassMetadata,
415427
string $fieldName,
428+
ClassMetadata $referencedClassMetadata,
416429
array $fieldValues,
417430
int $maxFetchJoinSameFieldCount,
418431
array $orderBy = [],
@@ -422,13 +435,18 @@ private function loadEntitiesBy(
422435
return [];
423436
}
424437

438+
$referencedType = $this->getIdentifierFieldType($referencedClassMetadata);
425439
$rootLevelAlias = 'e';
426440

427441
$queryBuilder = $this->entityManager->createQueryBuilder()
428442
->select($rootLevelAlias)
429443
->from($targetClassMetadata->getName(), $rootLevelAlias)
430444
->andWhere("{$rootLevelAlias}.{$fieldName} IN (:fieldValues)")
431-
->setParameter('fieldValues', $fieldValues);
445+
->setParameter(
446+
'fieldValues',
447+
$this->convertFieldValuesToDatabaseValues($referencedType, $fieldValues),
448+
$this->deduceArrayParameterType($referencedType),
449+
);
432450

433451
$this->addFetchJoinsToPreventFetchDuringHydration($rootLevelAlias, $queryBuilder, $targetClassMetadata, $maxFetchJoinSameFieldCount);
434452

@@ -439,6 +457,54 @@ private function loadEntitiesBy(
439457
return $queryBuilder->getQuery()->getResult();
440458
}
441459

460+
private function deduceArrayParameterType(Type $dbalType): ?ArrayParameterType
461+
{
462+
return match ($dbalType->getBindingType()) {
463+
ParameterType::INTEGER => ArrayParameterType::INTEGER,
464+
ParameterType::STRING => ArrayParameterType::STRING,
465+
ParameterType::ASCII => ArrayParameterType::ASCII,
466+
ParameterType::BINARY => ArrayParameterType::BINARY,
467+
default => null, // @phpstan-ignore shipmonk.defaultMatchArmWithEnum
468+
};
469+
}
470+
471+
/**
472+
* @param array<mixed> $fieldValues
473+
* @return list<mixed>
474+
*/
475+
private function convertFieldValuesToDatabaseValues(
476+
Type $dbalType,
477+
array $fieldValues,
478+
): array
479+
{
480+
$connection = $this->entityManager->getConnection();
481+
$platform = $connection->getDatabasePlatform();
482+
483+
$convertedValues = [];
484+
foreach ($fieldValues as $value) {
485+
$convertedValues[] = $dbalType->convertToDatabaseValue($value, $platform);
486+
}
487+
488+
return $convertedValues;
489+
}
490+
491+
/**
492+
* @param ClassMetadata<C> $classMetadata
493+
*
494+
* @template C of E
495+
*/
496+
private function getIdentifierFieldType(ClassMetadata $classMetadata): Type
497+
{
498+
$identifierName = $classMetadata->getSingleIdentifierFieldName();
499+
$sourceIdTypeName = $classMetadata->getTypeOfField($identifierName);
500+
501+
if ($sourceIdTypeName === null) {
502+
throw new LogicException("Identifier field '{$identifierName}' for class '{$classMetadata->getName()}' has unknown field type.");
503+
}
504+
505+
return Type::getType($sourceIdTypeName);
506+
}
507+
442508
/**
443509
* @param ClassMetadata<S> $sourceClassMetadata
444510
* @param array<string, array<string, int>> $alreadyPreloadedJoins

tests/EntityPreloadBlogManyHasManyTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace ShipMonkTests\DoctrineEntityPreloader;
44

5+
use Doctrine\DBAL\ArrayParameterType;
56
use Doctrine\ORM\Mapping\ClassMetadata;
67
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article;
78
use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase;
9+
use function array_map;
810

911
class EntityPreloadBlogManyHasManyTest extends TestCase
1012
{
@@ -29,13 +31,17 @@ public function testOneHasManyWithWithManualPreloadUsingPartial(): void
2931
$this->createDummyBlogData(articleInEachCategoryCount: 5, tagForEachArticleCount: 5);
3032

3133
$articles = $this->getEntityManager()->getRepository(Article::class)->findAll();
34+
$rawArticleIds = array_map(
35+
static fn (Article $article): string => $article->getId()->getBytes(),
36+
$articles,
37+
);
3238

3339
$this->getEntityManager()->createQueryBuilder()
3440
->select('PARTIAL article.{id}', 'tag')
3541
->from(Article::class, 'article')
3642
->leftJoin('article.tags', 'tag')
3743
->where('article IN (:articles)')
38-
->setParameter('articles', $articles)
44+
->setParameter('articles', $rawArticleIds, ArrayParameterType::BINARY)
3945
->getQuery()
4046
->getResult();
4147

tests/EntityPreloadBlogManyHasOneDeepTest.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ShipMonkTests\DoctrineEntityPreloader;
44

5+
use Doctrine\DBAL\ArrayParameterType;
56
use Doctrine\ORM\Mapping\ClassMetadata;
67
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article;
78
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category;
@@ -35,27 +36,27 @@ public function testManyHasOneDeepWithManualPreload(): void
3536

3637
$articles = $this->getEntityManager()->getRepository(Article::class)->findAll();
3738

38-
$categoryIds = array_map(static fn (Article $article) => $article->getCategory()?->getId(), $articles);
39-
$categoryIds = array_filter($categoryIds, static fn (?int $id) => $id !== null);
39+
$categoryIds = array_map(static fn (Article $article) => $article->getCategory()?->getId()->getBytes(), $articles);
40+
$categoryIds = array_filter($categoryIds, static fn (?string $id) => $id !== null);
4041

4142
if (count($categoryIds) > 0) {
4243
$categories = $this->getEntityManager()->createQueryBuilder()
4344
->select('category')
4445
->from(Category::class, 'category')
4546
->where('category.id IN (:ids)')
46-
->setParameter('ids', array_values(array_unique($categoryIds)))
47+
->setParameter('ids', array_values(array_unique($categoryIds)), ArrayParameterType::BINARY)
4748
->getQuery()
4849
->getResult();
4950

50-
$parentCategoryIds = array_map(static fn (Category $category) => $category->getParent()?->getId(), $categories);
51-
$parentCategoryIds = array_filter($parentCategoryIds, static fn (?int $id) => $id !== null);
51+
$parentCategoryIds = array_map(static fn (Category $category) => $category->getParent()?->getId()->getBytes(), $categories);
52+
$parentCategoryIds = array_filter($parentCategoryIds, static fn (?string $id) => $id !== null);
5253

5354
if (count($parentCategoryIds) > 0) {
5455
$this->getEntityManager()->createQueryBuilder()
5556
->select('category')
5657
->from(Category::class, 'category')
5758
->where('category.id IN (:ids)')
58-
->setParameter('ids', array_values(array_unique($parentCategoryIds)))
59+
->setParameter('ids', array_values(array_unique($parentCategoryIds)), ArrayParameterType::BINARY)
5960
->getQuery()
6061
->getResult();
6162
}

tests/EntityPreloadBlogManyHasOneTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ShipMonkTests\DoctrineEntityPreloader;
44

5+
use Doctrine\DBAL\ArrayParameterType;
56
use Doctrine\ORM\Mapping\ClassMetadata;
67
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article;
78
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category;
@@ -35,15 +36,15 @@ public function testManyHasOneWithManualPreload(): void
3536

3637
$articles = $this->getEntityManager()->getRepository(Article::class)->findAll();
3738

38-
$categoryIds = array_map(static fn (Article $article) => $article->getCategory()?->getId(), $articles);
39-
$categoryIds = array_filter($categoryIds, static fn (?int $id) => $id !== null);
39+
$categoryIds = array_map(static fn (Article $article): ?string => $article->getCategory()?->getId()->getBytes(), $articles);
40+
$categoryIds = array_filter($categoryIds, static fn (?string $id) => $id !== null);
4041

4142
if (count($categoryIds) > 0) {
4243
$this->getEntityManager()->createQueryBuilder()
4344
->select('category')
4445
->from(Category::class, 'category')
4546
->where('category.id IN (:ids)')
46-
->setParameter('ids', array_values(array_unique($categoryIds)))
47+
->setParameter('ids', array_values(array_unique($categoryIds)), ArrayParameterType::BINARY)
4748
->getQuery()
4849
->getResult();
4950
}

tests/EntityPreloadBlogOneHasManyDeepTest.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ShipMonkTests\DoctrineEntityPreloader;
44

5+
use Doctrine\DBAL\ArrayParameterType;
56
use Doctrine\ORM\Mapping\ClassMetadata;
67
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category;
78
use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase;
@@ -42,22 +43,26 @@ public function testOneHasManyDeepWithWithManualPreloadUsingPartial(): void
4243
->getQuery()
4344
->getResult();
4445

46+
$rawRootCategoryIds = array_map(static fn (Category $category) => $category->getId()->getBytes(), $rootCategories);
47+
4548
$this->getEntityManager()->createQueryBuilder()
4649
->select('PARTIAL category.{id}', 'subCategory')
4750
->from(Category::class, 'category')
4851
->leftJoin('category.children', 'subCategory')
4952
->where('category IN (:categories)')
50-
->setParameter('categories', $rootCategories)
53+
->setParameter('categories', $rawRootCategoryIds, ArrayParameterType::BINARY)
5154
->getQuery()
5255
->getResult();
5356

5457
$subCategories = array_merge(...array_map(static fn (Category $category) => $category->getChildren()->toArray(), $rootCategories));
58+
$rawSubCategoryIds = array_map(static fn (Category $category) => $category->getId()->getBytes(), $subCategories);
59+
5560
$this->getEntityManager()->createQueryBuilder()
5661
->select('PARTIAL subCategory.{id}', 'subSubCategory')
5762
->from(Category::class, 'subCategory')
5863
->leftJoin('subCategory.children', 'subSubCategory')
5964
->where('subCategory IN (:subCategories)')
60-
->setParameter('subCategories', $subCategories)
65+
->setParameter('subCategories', $rawSubCategoryIds, ArrayParameterType::BINARY)
6166
->getQuery()
6267
->getResult();
6368

tests/EntityPreloadBlogOneHasManyTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
namespace ShipMonkTests\DoctrineEntityPreloader;
44

5+
use Doctrine\DBAL\ArrayParameterType;
56
use Doctrine\ORM\Mapping\ClassMetadata;
67
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Article;
78
use ShipMonkTests\DoctrineEntityPreloader\Fixtures\Blog\Category;
89
use ShipMonkTests\DoctrineEntityPreloader\Lib\TestCase;
10+
use function array_map;
911

1012
class EntityPreloadBlogOneHasManyTest extends TestCase
1113
{
@@ -53,13 +55,17 @@ public function testOneHasManyWithWithManualPreloadUsingPartial(): void
5355
$this->createDummyBlogData(categoryCount: 5, articleInEachCategoryCount: 5);
5456

5557
$categories = $this->getEntityManager()->getRepository(Category::class)->findAll();
58+
$rawCategoryIds = array_map(
59+
static fn (Category $category): string => $category->getId()->getBytes(),
60+
$categories,
61+
);
5662

5763
$this->getEntityManager()->createQueryBuilder()
5864
->select('PARTIAL category.{id}', 'article')
5965
->from(Category::class, 'category')
6066
->leftJoin('category.articles', 'article')
6167
->where('category IN (:categories)')
62-
->setParameter('categories', $categories)
68+
->setParameter('categories', $rawCategoryIds, ArrayParameterType::BINARY)
6369
->getQuery()
6470
->getResult();
6571

tests/Fixtures/Blog/Article.php

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,15 @@
77
use Doctrine\Common\Collections\ReadableCollection;
88
use Doctrine\ORM\Mapping\Column;
99
use Doctrine\ORM\Mapping\Entity;
10-
use Doctrine\ORM\Mapping\GeneratedValue;
11-
use Doctrine\ORM\Mapping\Id;
1210
use Doctrine\ORM\Mapping\ManyToMany;
1311
use Doctrine\ORM\Mapping\ManyToOne;
1412
use Doctrine\ORM\Mapping\OneToMany;
1513
use Doctrine\ORM\Mapping\OrderBy;
1614

1715
#[Entity]
18-
class Article
16+
class Article extends TestEntityWithBinaryId
1917
{
2018

21-
#[Id]
22-
#[Column]
23-
#[GeneratedValue]
24-
private int $id;
25-
2619
#[Column]
2720
private string $title;
2821

@@ -51,6 +44,7 @@ public function __construct(
5144
?Category $category = null,
5245
)
5346
{
47+
parent::__construct();
5448
$this->title = $title;
5549
$this->content = $content;
5650
$this->category = $category;
@@ -60,11 +54,6 @@ public function __construct(
6054
$category?->addArticle($this);
6155
}
6256

63-
public function getId(): int
64-
{
65-
return $this->id;
66-
}
67-
6857
public function getTitle(): string
6958
{
7059
return $this->title;

0 commit comments

Comments
 (0)