From 3dca27ce0d38177f6cdcc6f94b7c80b61af41d91 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Mon, 30 Jun 2025 16:28:38 +0200 Subject: [PATCH 1/2] 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. --- .../Entity/BasicEntityPersister.php | 1 + .../Models/BinaryPrimaryKey/BinaryId.php | 48 +++++++++++ .../Models/BinaryPrimaryKey/BinaryIdType.php | 66 ++++++++++++++ .../Models/BinaryPrimaryKey/Category.php | 73 ++++++++++++++++ .../ORM/Persisters/BinaryIdPersisterTest.php | 85 +++++++++++++++++++ 5 files changed, 273 insertions(+) create mode 100644 tests/Tests/Models/BinaryPrimaryKey/BinaryId.php create mode 100644 tests/Tests/Models/BinaryPrimaryKey/BinaryIdType.php create mode 100644 tests/Tests/Models/BinaryPrimaryKey/Category.php create mode 100644 tests/Tests/ORM/Persisters/BinaryIdPersisterTest.php diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index 4c92d9840b2..81abd7e5287 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -1964,6 +1964,7 @@ private function getArrayBindingType(ParameterType|int|string $type): ArrayParam ParameterType::STRING => ArrayParameterType::STRING, ParameterType::INTEGER => ArrayParameterType::INTEGER, ParameterType::ASCII => ArrayParameterType::ASCII, + ParameterType::BINARY => ArrayParameterType::BINARY, }; } diff --git a/tests/Tests/Models/BinaryPrimaryKey/BinaryId.php b/tests/Tests/Models/BinaryPrimaryKey/BinaryId.php new file mode 100644 index 00000000000..87c68150021 --- /dev/null +++ b/tests/Tests/Models/BinaryPrimaryKey/BinaryId.php @@ -0,0 +1,48 @@ +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/Tests/Models/BinaryPrimaryKey/BinaryIdType.php b/tests/Tests/Models/BinaryPrimaryKey/BinaryIdType.php new file mode 100644 index 00000000000..2b66f9809ff --- /dev/null +++ b/tests/Tests/Models/BinaryPrimaryKey/BinaryIdType.php @@ -0,0 +1,66 @@ +getBytes(); + } else { + throw new LogicException('Unexpected value: ' . $value); + } + } + + public function getSQLDeclaration( + array $column, + AbstractPlatform $platform, + ): string { + return $platform->getBinaryTypeDeclarationSQL([ + 'length' => BinaryId::LENGTH, + 'fixed' => true, + ]); + } + + private function doGetBindingType(): ParameterType|int + { + return ParameterType::BINARY; + } + + public function getName(): string + { + return self::NAME; + } +} diff --git a/tests/Tests/Models/BinaryPrimaryKey/Category.php b/tests/Tests/Models/BinaryPrimaryKey/Category.php new file mode 100644 index 00000000000..d9924aeda16 --- /dev/null +++ b/tests/Tests/Models/BinaryPrimaryKey/Category.php @@ -0,0 +1,73 @@ + */ + #[OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; + + public function __construct( + string $name, + self|null $parent = null, + ) { + $this->id = BinaryId::new(); + $this->name = $name; + $this->parent = $parent; + $this->children = new ArrayCollection(); + + $parent?->addChild($this); + } + + public function getId(): BinaryId + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getParent(): self|null + { + return $this->parent; + } + + /** @return ReadableCollection */ + public function getChildren(): ReadableCollection + { + return $this->children; + } + + /** @internal */ + public function addChild(self $category): void + { + if (! $this->children->contains($category)) { + $this->children->add($category); + } + } +} diff --git a/tests/Tests/ORM/Persisters/BinaryIdPersisterTest.php b/tests/Tests/ORM/Persisters/BinaryIdPersisterTest.php new file mode 100644 index 00000000000..93151b5057c --- /dev/null +++ b/tests/Tests/ORM/Persisters/BinaryIdPersisterTest.php @@ -0,0 +1,85 @@ +createEntityManager(); + + $this->createDummyBlogData($entityManager, 3); + + $categories = $entityManager->createQueryBuilder() + ->select('category') + ->from(Category::class, 'category') + ->getQuery() + ->setFetchMode(Category::class, 'children', ClassMetadata::FETCH_EAGER) + ->getResult(); + + self::assertCount(3, $categories); + } + + private function createDummyBlogData( + EntityManager $entityManager, + int $categoryCount = 1, + int $categoryParentsCount = 0, + ): void { + for ($h = 0; $h < $categoryCount; $h++) { + $categoryParent = null; + + for ($i = 0; $i < $categoryParentsCount; $i++) { + $categoryParent = new Category('CategoryParent#' . $i, $categoryParent); + $entityManager->persist($categoryParent); + } + + $category = new Category('Category#' . $h, $categoryParent); + $entityManager->persist($category); + } + + $entityManager->flush(); + $entityManager->clear(); + } + + private function createEntityManager(): EntityManager + { + if ($this->entityManager !== null) { + return $this->entityManager; + } + + $config = ORMSetup::createAttributeMetadataConfiguration([__DIR__ . '/../../Models/BinaryPrimaryKey'], isDevMode: true); + + if (! DbalType::hasType(BinaryIdType::NAME)) { + DbalType::addType(BinaryIdType::NAME, BinaryIdType::class); + } + + $connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $config); + $entityManager = new EntityManagerMock($connection, $config); + + $schemaTool = new SchemaTool($entityManager); + $schemaTool->createSchema($entityManager->getMetadataFactory()->getAllMetadata()); + + $schemaValidator = new SchemaValidator($entityManager); + $schemaValidator->validateMapping(); + + $this->entityManager = $entityManager; + + return $entityManager; + } +} From 6ab858a5c54ac231398d076964a13db7ac8991d5 Mon Sep 17 00:00:00 2001 From: Jan Nedbal Date: Thu, 10 Jul 2025 16:54:02 +0200 Subject: [PATCH 2/2] Apply suggestion from @greg0ire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Grégoire Paris --- tests/Tests/ORM/Persisters/BinaryIdPersisterTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tests/ORM/Persisters/BinaryIdPersisterTest.php b/tests/Tests/ORM/Persisters/BinaryIdPersisterTest.php index 93151b5057c..fdeaeddb12a 100644 --- a/tests/Tests/ORM/Persisters/BinaryIdPersisterTest.php +++ b/tests/Tests/ORM/Persisters/BinaryIdPersisterTest.php @@ -20,7 +20,7 @@ final class BinaryIdPersisterTest extends OrmTestCase { private EntityManager|null $entityManager = null; - public function testOneHasManyWithEagerFetchMode(): void + public function testOneToManyWithEagerFetchMode(): void { $entityManager = $this->createEntityManager();