Skip to content

Commit 3dca27c

Browse files
committed
Fix unhandled ParameterType case for binary PKs
Add proper handling for binary primary key parameter types that were previously causing runtime exceptions. The existing parameter type switch statement was missing a case for binary types, leading to unhandled scenarios when working with binary primary keys. This ensures consistent parameter type handling across all supported primary key data types in the ORM.
1 parent e19704e commit 3dca27c

File tree

5 files changed

+273
-0
lines changed

5 files changed

+273
-0
lines changed

src/Persisters/Entity/BasicEntityPersister.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1964,6 +1964,7 @@ private function getArrayBindingType(ParameterType|int|string $type): ArrayParam
19641964
ParameterType::STRING => ArrayParameterType::STRING,
19651965
ParameterType::INTEGER => ArrayParameterType::INTEGER,
19661966
ParameterType::ASCII => ArrayParameterType::ASCII,
1967+
ParameterType::BINARY => ArrayParameterType::BINARY,
19671968
};
19681969
}
19691970

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\BinaryPrimaryKey;
6+
7+
use LogicException;
8+
9+
use function bin2hex;
10+
use function hex2bin;
11+
use function random_bytes;
12+
13+
class BinaryId
14+
{
15+
public const LENGTH = 6;
16+
17+
private string $hexId;
18+
19+
private function __construct(string $data)
20+
{
21+
$this->hexId = $data;
22+
}
23+
24+
public static function new(): self
25+
{
26+
return new self(bin2hex(random_bytes(self::LENGTH)));
27+
}
28+
29+
public static function fromBytes(string $value): self
30+
{
31+
return new self(bin2hex($value));
32+
}
33+
34+
public function getBytes(): string
35+
{
36+
$binary = hex2bin($this->hexId);
37+
if ($binary === false) {
38+
throw new LogicException('Cannot convert hex to binary: ' . $this->hexId);
39+
}
40+
41+
return $binary;
42+
}
43+
44+
public function __toString(): string
45+
{
46+
return $this->getBytes();
47+
}
48+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\BinaryPrimaryKey;
6+
7+
use Doctrine\DBAL\ParameterType;
8+
use Doctrine\DBAL\Platforms\AbstractPlatform;
9+
use Doctrine\DBAL\Types\Type;
10+
use Doctrine\Tests\Mocks\CompatibilityType;
11+
use LogicException;
12+
13+
final class BinaryIdType extends Type
14+
{
15+
use CompatibilityType;
16+
17+
public const NAME = 'binary_id';
18+
19+
public function convertToPHPValue(
20+
mixed $value,
21+
AbstractPlatform $platform,
22+
): BinaryId|null {
23+
if ($value === null) {
24+
return null;
25+
}
26+
27+
if ($value instanceof BinaryId) {
28+
return $value;
29+
}
30+
31+
return BinaryId::fromBytes($value);
32+
}
33+
34+
public function convertToDatabaseValue(
35+
mixed $value,
36+
AbstractPlatform $platform,
37+
): string|null {
38+
if ($value === null) {
39+
return null;
40+
} elseif ($value instanceof BinaryId) {
41+
return $value->getBytes();
42+
} else {
43+
throw new LogicException('Unexpected value: ' . $value);
44+
}
45+
}
46+
47+
public function getSQLDeclaration(
48+
array $column,
49+
AbstractPlatform $platform,
50+
): string {
51+
return $platform->getBinaryTypeDeclarationSQL([
52+
'length' => BinaryId::LENGTH,
53+
'fixed' => true,
54+
]);
55+
}
56+
57+
private function doGetBindingType(): ParameterType|int
58+
{
59+
return ParameterType::BINARY;
60+
}
61+
62+
public function getName(): string
63+
{
64+
return self::NAME;
65+
}
66+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\Models\BinaryPrimaryKey;
6+
7+
use Doctrine\Common\Collections\ArrayCollection;
8+
use Doctrine\Common\Collections\Collection;
9+
use Doctrine\Common\Collections\ReadableCollection;
10+
use Doctrine\ORM\Mapping\Column;
11+
use Doctrine\ORM\Mapping\Entity;
12+
use Doctrine\ORM\Mapping\Id;
13+
use Doctrine\ORM\Mapping\ManyToOne;
14+
use Doctrine\ORM\Mapping\OneToMany;
15+
16+
#[Entity]
17+
class Category
18+
{
19+
#[Id]
20+
#[Column(type: BinaryIdType::NAME, nullable: false)]
21+
private BinaryId $id;
22+
23+
#[Column]
24+
private string $name;
25+
26+
#[ManyToOne(targetEntity: self::class, inversedBy: 'children')]
27+
private self|null $parent;
28+
29+
/** @var Collection<int, Category> */
30+
#[OneToMany(targetEntity: self::class, mappedBy: 'parent')]
31+
private Collection $children;
32+
33+
public function __construct(
34+
string $name,
35+
self|null $parent = null,
36+
) {
37+
$this->id = BinaryId::new();
38+
$this->name = $name;
39+
$this->parent = $parent;
40+
$this->children = new ArrayCollection();
41+
42+
$parent?->addChild($this);
43+
}
44+
45+
public function getId(): BinaryId
46+
{
47+
return $this->id;
48+
}
49+
50+
public function getName(): string
51+
{
52+
return $this->name;
53+
}
54+
55+
public function getParent(): self|null
56+
{
57+
return $this->parent;
58+
}
59+
60+
/** @return ReadableCollection<int, Category> */
61+
public function getChildren(): ReadableCollection
62+
{
63+
return $this->children;
64+
}
65+
66+
/** @internal */
67+
public function addChild(self $category): void
68+
{
69+
if (! $this->children->contains($category)) {
70+
$this->children->add($category);
71+
}
72+
}
73+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Persisters;
6+
7+
use Doctrine\DBAL\DriverManager;
8+
use Doctrine\DBAL\Types\Type as DbalType;
9+
use Doctrine\ORM\EntityManager;
10+
use Doctrine\ORM\Mapping\ClassMetadata;
11+
use Doctrine\ORM\ORMSetup;
12+
use Doctrine\ORM\Tools\SchemaTool;
13+
use Doctrine\ORM\Tools\SchemaValidator;
14+
use Doctrine\Tests\Mocks\EntityManagerMock;
15+
use Doctrine\Tests\Models\BinaryPrimaryKey\BinaryIdType;
16+
use Doctrine\Tests\Models\BinaryPrimaryKey\Category;
17+
use Doctrine\Tests\OrmTestCase;
18+
19+
final class BinaryIdPersisterTest extends OrmTestCase
20+
{
21+
private EntityManager|null $entityManager = null;
22+
23+
public function testOneHasManyWithEagerFetchMode(): void
24+
{
25+
$entityManager = $this->createEntityManager();
26+
27+
$this->createDummyBlogData($entityManager, 3);
28+
29+
$categories = $entityManager->createQueryBuilder()
30+
->select('category')
31+
->from(Category::class, 'category')
32+
->getQuery()
33+
->setFetchMode(Category::class, 'children', ClassMetadata::FETCH_EAGER)
34+
->getResult();
35+
36+
self::assertCount(3, $categories);
37+
}
38+
39+
private function createDummyBlogData(
40+
EntityManager $entityManager,
41+
int $categoryCount = 1,
42+
int $categoryParentsCount = 0,
43+
): void {
44+
for ($h = 0; $h < $categoryCount; $h++) {
45+
$categoryParent = null;
46+
47+
for ($i = 0; $i < $categoryParentsCount; $i++) {
48+
$categoryParent = new Category('CategoryParent#' . $i, $categoryParent);
49+
$entityManager->persist($categoryParent);
50+
}
51+
52+
$category = new Category('Category#' . $h, $categoryParent);
53+
$entityManager->persist($category);
54+
}
55+
56+
$entityManager->flush();
57+
$entityManager->clear();
58+
}
59+
60+
private function createEntityManager(): EntityManager
61+
{
62+
if ($this->entityManager !== null) {
63+
return $this->entityManager;
64+
}
65+
66+
$config = ORMSetup::createAttributeMetadataConfiguration([__DIR__ . '/../../Models/BinaryPrimaryKey'], isDevMode: true);
67+
68+
if (! DbalType::hasType(BinaryIdType::NAME)) {
69+
DbalType::addType(BinaryIdType::NAME, BinaryIdType::class);
70+
}
71+
72+
$connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config);
73+
$entityManager = new EntityManagerMock($connection, $config);
74+
75+
$schemaTool = new SchemaTool($entityManager);
76+
$schemaTool->createSchema($entityManager->getMetadataFactory()->getAllMetadata());
77+
78+
$schemaValidator = new SchemaValidator($entityManager);
79+
$schemaValidator->validateMapping();
80+
81+
$this->entityManager = $entityManager;
82+
83+
return $entityManager;
84+
}
85+
}

0 commit comments

Comments
 (0)