From b24586b1b53d141169a3be381eb64515b5adff65 Mon Sep 17 00:00:00 2001 From: Elliot Bruneel Date: Thu, 30 Oct 2025 11:35:53 +0100 Subject: [PATCH 1/5] fix: handling of empty array in SQL condition generation --- .../Entity/BasicEntityPersister.php | 6 +++ .../ORM/Functional/Ticket/GH12254Test.php | 49 +++++++++++++++++++ .../BasicEntityPersisterTypeValueSqlTest.php | 9 ++++ 3 files changed, 64 insertions(+) create mode 100644 tests/Tests/ORM/Functional/Ticket/GH12254Test.php diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index ad29122300..5b93f095d7 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -1753,6 +1753,11 @@ public function getSelectConditionStatementSQL($field, $value, $assoc = null, $c $value = [$value]; } + if (empty($value)) { + $selectedColumns[] = $column . ' IN (NULL)'; + continue; + } + $nullKeys = array_keys($value, null, true); $nonNullValues = array_diff_key($value, array_flip($nullKeys)); @@ -1760,6 +1765,7 @@ public function getSelectConditionStatementSQL($field, $value, $assoc = null, $c $in = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholders); + // @phpstan-ignore if.alwaysTrue (false positive) if ($nullKeys) { if ($nonNullValues) { $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column); diff --git a/tests/Tests/ORM/Functional/Ticket/GH12254Test.php b/tests/Tests/ORM/Functional/Ticket/GH12254Test.php new file mode 100644 index 0000000000..5fb00415fe --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH12254Test.php @@ -0,0 +1,49 @@ +setUpEntitySchema([ + GH12254EntityA::class, + ]); + + $this->_em->persist(new GH12254EntityA()); + $this->_em->flush(); + $this->_em->clear(); + } + + public function testFindByEmptyArrayShouldReturnEmptyArray(): void + { + // pretend we are starting afresh + $this->_em = $this->getEntityManager(); + $result = $this->_em->getRepository(GH12254EntityA::class)->findBy(['id' => []]); + $this->assertEmpty($result); + } +} + +/** + * @Entity() + */ +class GH12254EntityA +{ + /** + * @Column(type="integer") + * @Id() + * @GeneratedValue(strategy="AUTO") + * @var int + */ + public $id; +} diff --git a/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php b/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php index 60ed7391f3..b60f6e2229 100644 --- a/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php +++ b/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php @@ -153,6 +153,15 @@ public function testSelectConditionStatementWithMultipleValuesContainingNull(): ); } + /** @group GH12254 */ + public function testSelectConditionStatementWithValuesIsEmptyArray(): void + { + self::assertEquals( + 't0.id IN (NULL)', + $this->persister->getSelectConditionStatementSQL('id', []) + ); + } + public function testCountCondition(): void { $persister = new BasicEntityPersister($this->entityManager, $this->entityManager->getClassMetadata(NonAlphaColumnsEntity::class)); From 23f22860f1a5415d6a5465a3070464083abfedee Mon Sep 17 00:00:00 2001 From: Elliot Bruneel Date: Thu, 30 Oct 2025 17:31:32 +0100 Subject: [PATCH 2/5] chore: update phpstan version and regenerate baseline --- composer.json | 2 +- phpstan-baseline.neon | 32 ++++--------------- .../Entity/BasicEntityPersister.php | 1 - 3 files changed, 8 insertions(+), 27 deletions(-) diff --git a/composer.json b/composer.json index ebcd00980d..ccf22b1f1a 100644 --- a/composer.json +++ b/composer.json @@ -54,7 +54,7 @@ "doctrine/coding-standard": "^9.0.2 || ^14.0", "phpbench/phpbench": "^0.16.10 || ^1.0", "phpstan/extension-installer": "~1.1.0 || ^1.4", - "phpstan/phpstan": "~1.4.10 || 2.1.22", + "phpstan/phpstan": "~1.4.10 || 2.1.23", "phpstan/phpstan-deprecation-rules": "^1 || ^2", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0b966b47b7..897178a71c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1056,6 +1056,12 @@ parameters: count: 1 path: src/Internal/HydrationCompleteHandler.php + - + message: '#^Offset int\|null might not exist on array\\.$#' + identifier: offsetAccess.notFound + count: 1 + path: src/Internal/StronglyConnectedComponents.php + - message: '#^Property Doctrine\\ORM\\Internal\\StronglyConnectedComponents\:\:\$representingNodes \(array\\) does not accept array\\.$#' identifier: assign.propertyType @@ -3594,7 +3600,7 @@ parameters: - message: '#^Property Doctrine\\ORM\\Query\\Filter\\SQLFilter\:\:\$parameters \(array\\) does not accept non\-empty\-array\\.$#' identifier: assign.propertyType - count: 1 + count: 2 path: src/Query/Filter/SQLFilter.php - @@ -4173,12 +4179,6 @@ parameters: count: 1 path: src/Tools/Console/Command/ConvertMappingCommand.php - - - message: '#^Parameter \#2 \$destPath of method Doctrine\\ORM\\Tools\\Console\\Command\\ConvertMappingCommand\:\:getExporter\(\) expects string, string\|false given\.$#' - identifier: argument.type - count: 1 - path: src/Tools/Console/Command/ConvertMappingCommand.php - - message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\:\:\$name\.$#' identifier: property.notFound @@ -4203,12 +4203,6 @@ parameters: count: 1 path: src/Tools/Console/Command/GenerateEntitiesCommand.php - - - message: '#^Parameter \#2 \$outputDirectory of method Doctrine\\ORM\\Tools\\EntityGenerator\:\:generate\(\) expects string, string\|false given\.$#' - identifier: argument.type - count: 1 - path: src/Tools/Console/Command/GenerateEntitiesCommand.php - - message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\:\:\$name\.$#' identifier: property.notFound @@ -4227,12 +4221,6 @@ parameters: count: 1 path: src/Tools/Console/Command/GenerateProxiesCommand.php - - - message: '#^Parameter \#2 \$proxyDir of method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:generateProxyClasses\(\) expects string\|null, string\|false given\.$#' - identifier: argument.type - count: 1 - path: src/Tools/Console/Command/GenerateProxiesCommand.php - - message: '#^Access to an undefined property Doctrine\\Persistence\\Mapping\\ClassMetadata\:\:\$customRepositoryClassName\.$#' identifier: property.notFound @@ -4251,12 +4239,6 @@ parameters: count: 1 path: src/Tools/Console/Command/GenerateRepositoriesCommand.php - - - message: '#^Parameter \#2 \$outputDirectory of method Doctrine\\ORM\\Tools\\EntityRepositoryGenerator\:\:writeEntityRepositoryClass\(\) expects string, string\|false given\.$#' - identifier: argument.type - count: 1 - path: src/Tools/Console/Command/GenerateRepositoriesCommand.php - - message: '#^Method Doctrine\\ORM\\Tools\\Console\\Command\\MappingDescribeCommand\:\:formatMappings\(\) has parameter \$propertyMappings with no value type specified in iterable type array\.$#' identifier: missingType.iterableValue diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index 5b93f095d7..7ee83bc0f3 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -1765,7 +1765,6 @@ public function getSelectConditionStatementSQL($field, $value, $assoc = null, $c $in = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholders); - // @phpstan-ignore if.alwaysTrue (false positive) if ($nullKeys) { if ($nonNullValues) { $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column); From 32d1e97ce7b0be4c9b24f9df39b7bd8da3d03258 Mon Sep 17 00:00:00 2001 From: Elliot Bruneel Date: Wed, 5 Nov 2025 09:51:33 +0100 Subject: [PATCH 3/5] chore: improve empty array check in SQL condition generation --- src/Persisters/Entity/BasicEntityPersister.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index 7ee83bc0f3..7fe94f9bf3 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -1753,7 +1753,7 @@ public function getSelectConditionStatementSQL($field, $value, $assoc = null, $c $value = [$value]; } - if (empty($value)) { + if ($value === []) { $selectedColumns[] = $column . ' IN (NULL)'; continue; } From 4989ca6f1566b6929cc81b4b746b64ec889e0174 Mon Sep 17 00:00:00 2001 From: Elliot Bruneel Date: Wed, 5 Nov 2025 10:03:09 +0100 Subject: [PATCH 4/5] test: add test for finding by nullable field with empty array --- tests/Tests/ORM/Functional/Ticket/GH12254Test.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Tests/ORM/Functional/Ticket/GH12254Test.php b/tests/Tests/ORM/Functional/Ticket/GH12254Test.php index 5fb00415fe..6be7fefc4d 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH12254Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH12254Test.php @@ -32,6 +32,13 @@ public function testFindByEmptyArrayShouldReturnEmptyArray(): void $result = $this->_em->getRepository(GH12254EntityA::class)->findBy(['id' => []]); $this->assertEmpty($result); } + + public function testFindByInNullableField(): void + { + $this->_em = $this->getEntityManager(); + $result = $this->_em->getRepository(GH12254EntityA::class)->findBy(['name' => []]); + $this->assertEmpty($result); + } } /** @@ -46,4 +53,10 @@ class GH12254EntityA * @var int */ public $id; + + /** + * @Column(type="string", nullable=true) + * @var string|null + */ + public $name = null; } From 9ef0f5301b2176d093a6289961414db26c993282 Mon Sep 17 00:00:00 2001 From: Elliot Bruneel Date: Mon, 10 Nov 2025 10:44:48 +0100 Subject: [PATCH 5/5] fix: update SQL condition for empty array to 1=0 instead of IN (NULL) --- src/Persisters/Entity/BasicEntityPersister.php | 2 +- .../BasicEntityPersisterTypeValueSqlTest.php | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index 7fe94f9bf3..75b89e2c39 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -1754,7 +1754,7 @@ public function getSelectConditionStatementSQL($field, $value, $assoc = null, $c } if ($value === []) { - $selectedColumns[] = $column . ' IN (NULL)'; + $selectedColumns[] = '1=0'; continue; } diff --git a/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php b/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php index b60f6e2229..d9e8fe96fe 100644 --- a/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php +++ b/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php @@ -129,7 +129,10 @@ public function testSelectConditionStatementNeqNull(): void self::assertEquals('test IS NOT NULL', $statement); } - /** @group DDC-3056 */ + /** + * @group DDC-3056 + * @group GH12254 + */ public function testSelectConditionStatementWithMultipleValuesContainingNull(): void { self::assertEquals( @@ -151,13 +154,9 @@ public function testSelectConditionStatementWithMultipleValuesContainingNull(): '(t0.id IN (?, ?) OR t0.id IS NULL)', $this->persister->getSelectConditionStatementSQL('id', [123, null, 234]) ); - } - /** @group GH12254 */ - public function testSelectConditionStatementWithValuesIsEmptyArray(): void - { self::assertEquals( - 't0.id IN (NULL)', + '1=0', $this->persister->getSelectConditionStatementSQL('id', []) ); }