diff --git a/docs/en/reference/attributes-reference.rst b/docs/en/reference/attributes-reference.rst index 3452f78acb5..8190c28c8ef 100644 --- a/docs/en/reference/attributes-reference.rst +++ b/docs/en/reference/attributes-reference.rst @@ -576,7 +576,7 @@ Example with partial indexes: #[Index(name: "search_idx", columns: ["category"], options: [ - "where": "((category IS NOT NULL))" + "where" => "((category IS NOT NULL))" ] )] class ECommerceProduct diff --git a/doctrine-mapping.xsd b/doctrine-mapping.xsd index 1965c8f4561..a9b1a367e67 100644 --- a/doctrine-mapping.xsd +++ b/doctrine-mapping.xsd @@ -302,7 +302,7 @@ - + @@ -415,7 +415,7 @@ - + @@ -447,6 +447,13 @@ + + + + + + + @@ -631,7 +638,7 @@ - + diff --git a/lib/Doctrine/ORM/Internal/HydrationCompleteHandler.php b/lib/Doctrine/ORM/Internal/HydrationCompleteHandler.php index 2ca06847236..393255c0c50 100644 --- a/lib/Doctrine/ORM/Internal/HydrationCompleteHandler.php +++ b/lib/Doctrine/ORM/Internal/HydrationCompleteHandler.php @@ -51,7 +51,7 @@ public function deferPostLoadInvoking(ClassMetadata $class, $entity): void } /** - * This method should me called after any hydration cycle completed. + * This method should be called after any hydration cycle completed. * * Method fires all deferred invocations of postLoad events */ diff --git a/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php index 1813319e75c..2dccf6707e8 100644 --- a/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/Collection/ManyToManyPersister.php @@ -6,6 +6,7 @@ use BadMethodCallException; use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\Expr\Comparison; use Doctrine\DBAL\Exception as DBALException; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\PersistentCollection; @@ -248,10 +249,15 @@ public function loadCriteria(PersistentCollection $collection, Criteria $criteri foreach ($parameters as $parameter) { [$name, $value, $operator] = $parameter; - $field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform); - $whereClauses[] = sprintf('te.%s %s ?', $field, $operator); - $params[] = $value; - $paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0]; + $field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform); + + if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) { + $whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT'); + } else { + $whereClauses[] = sprintf('te.%s %s ?', $field, $operator); + $params[] = $value; + $paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0]; + } } $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); diff --git a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php index 1cc8e57db66..c3b70e41f68 100644 --- a/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php @@ -892,15 +892,17 @@ public function expandCriteriaParameters(Criteria $criteria) $valueVisitor->dispatch($expression); - [$params, $types] = $valueVisitor->getParamsAndTypes(); - - foreach ($params as $param) { - $sqlParams = array_merge($sqlParams, $this->getValues($param)); - } + [, $types] = $valueVisitor->getParamsAndTypes(); foreach ($types as $type) { - [$field, $value] = $type; - $sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class)); + [$field, $value, $operator] = $type; + + if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) { + continue; + } + + $sqlParams = array_merge($sqlParams, $this->getValues($value)); + $sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class)); } return [$sqlParams, $sqlTypes]; diff --git a/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php b/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php index a61d0a25f86..999746faaa9 100644 --- a/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php +++ b/lib/Doctrine/ORM/Persisters/SqlValueVisitor.php @@ -27,18 +27,10 @@ class SqlValueVisitor extends ExpressionVisitor */ public function walkComparison(Comparison $comparison) { - $value = $this->getValueFromComparison($comparison); - $field = $comparison->getField(); - $operator = $comparison->getOperator(); - - if (($operator === Comparison::EQ || $operator === Comparison::IS) && $value === null) { - return null; - } elseif ($operator === Comparison::NEQ && $value === null) { - return null; - } + $value = $this->getValueFromComparison($comparison); $this->values[] = $value; - $this->types[] = [$field, $value, $operator]; + $this->types[] = [$comparison->getField(), $value, $comparison->getOperator()]; return null; } diff --git a/tests/Doctrine/Tests/Models/GH7717/GH7717Child.php b/tests/Doctrine/Tests/Models/GH7717/GH7717Child.php new file mode 100644 index 00000000000..0ea30c8437f --- /dev/null +++ b/tests/Doctrine/Tests/Models/GH7717/GH7717Child.php @@ -0,0 +1,26 @@ + + */ + public Selectable $children; +} diff --git a/tests/Doctrine/Tests/Models/Project/Project.php b/tests/Doctrine/Tests/Models/Project/Project.php new file mode 100644 index 00000000000..fc64b43561a --- /dev/null +++ b/tests/Doctrine/Tests/Models/Project/Project.php @@ -0,0 +1,24 @@ +id = $id; + $this->name = $name; + } +} diff --git a/tests/Doctrine/Tests/Models/Project/ProjectId.php b/tests/Doctrine/Tests/Models/Project/ProjectId.php new file mode 100644 index 00000000000..cedc0e5bd9d --- /dev/null +++ b/tests/Doctrine/Tests/Models/Project/ProjectId.php @@ -0,0 +1,18 @@ +id = $id; + } +} diff --git a/tests/Doctrine/Tests/Models/Project/ProjectInvalidMapping.php b/tests/Doctrine/Tests/Models/Project/ProjectInvalidMapping.php new file mode 100644 index 00000000000..ceecd48f754 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Project/ProjectInvalidMapping.php @@ -0,0 +1,24 @@ +id = $id; + $this->name = $name; + } +} diff --git a/tests/Doctrine/Tests/Models/Project/ProjectName.php b/tests/Doctrine/Tests/Models/Project/ProjectName.php new file mode 100644 index 00000000000..ea2177996d6 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Project/ProjectName.php @@ -0,0 +1,18 @@ +name = $name; + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7717Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7717Test.php new file mode 100755 index 00000000000..96c519e8624 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7717Test.php @@ -0,0 +1,45 @@ +createSchemaForModels( + GH7717Parent::class, + GH7717Child::class + ); + } + + public function testManyToManyPersisterIsNullComparison(): void + { + $childWithNullProperty = new GH7717Child(); + $childWithoutNullProperty = new GH7717Child(); + $childWithoutNullProperty->nullableProperty = 'nope'; + + $parent = new GH7717Parent(); + $parent->children = new ArrayCollection([$childWithNullProperty, $childWithoutNullProperty]); + + $this->_em->persist($parent); + $this->_em->flush(); + $this->_em->clear(); + + $parent = $this->_em->find(GH7717Parent::class, 1); + + $this->assertCount(1, $parent->children->matching(new Criteria(Criteria::expr()->isNull('nullableProperty')))); + } +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php index df09f9613d2..090a9d17693 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php @@ -22,6 +22,10 @@ use Doctrine\Tests\Models\Generic\BooleanModel; use Doctrine\Tests\Models\GH7141\GH7141Article; use Doctrine\Tests\Models\GH7316\GH7316Article; +use Doctrine\Tests\Models\Project\Project; +use Doctrine\Tests\Models\Project\ProjectId; +use Doctrine\Tests\Models\Project\ProjectInvalidMapping; +use Doctrine\Tests\Models\Project\ProjectName; use Doctrine\Tests\Models\ValueObjects\Name; use Doctrine\Tests\Models\ValueObjects\Person; @@ -239,6 +243,10 @@ public static function dataInvalidSchema(): array UserMissingAttributes::class, ['The attribute \'name\' is required but missing' => 1], ], + [ + ProjectInvalidMapping::class, + ['attribute \'type\': [facet \'pattern\'] The value' => 2], + ], ]; } @@ -279,6 +287,23 @@ public function testInvalidEntityOrMappedSuperClassShouldMentionParentClasses(): $this->createClassMetadata(DDC889Class::class); } + + public function testClassNameInFieldOrId(): void + { + $class = new ClassMetadata(Project::class); + $class->initializeReflection(new RuntimeReflectionService()); + + $driver = $this->loadDriver(); + $driver->loadMetadataForClass(Project::class, $class); + + /** @var array{type: string} $id */ + $id = $class->getFieldMapping('id'); + /** @var array{type: string} $name */ + $name = $class->getFieldMapping('name'); + + self::assertEquals(ProjectId::class, $id['type']); + self::assertEquals(ProjectName::class, $name['type']); + } } class CTI diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Project.Project.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Project.Project.dcm.xml new file mode 100644 index 00000000000..05ad513b7b6 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Project.Project.dcm.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Project.ProjectInvalidMapping.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Project.ProjectInvalidMapping.dcm.xml new file mode 100644 index 00000000000..d70800a3552 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Project.ProjectInvalidMapping.dcm.xml @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/tests/Doctrine/Tests/TestUtil.php b/tests/Doctrine/Tests/TestUtil.php index 28b3185c5fa..4eb0afe22c6 100644 --- a/tests/Doctrine/Tests/TestUtil.php +++ b/tests/Doctrine/Tests/TestUtil.php @@ -6,6 +6,7 @@ use Doctrine\Common\EventSubscriber; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Driver\AbstractSQLiteDriver\Middleware\EnableForeignKeys; use Doctrine\DBAL\DriverManager; use Doctrine\DBAL\Exception\DriverException; use Doctrine\DBAL\Platforms\SqlitePlatform; @@ -16,10 +17,12 @@ use UnexpectedValueException; use function assert; +use function class_exists; use function explode; use function fwrite; use function get_debug_type; use function getenv; +use function in_array; use function method_exists; use function sprintf; use function str_starts_with; @@ -63,7 +66,13 @@ public static function getConnection(): DbalExtensions\Connection self::$initialized = true; } - $connection = DriverManager::getConnection(self::getTestConnectionParameters()); + $connectionParameters = self::getTestConnectionParameters(); + $configuration = new Configuration(); + if (in_array($connectionParameters['driver'], ['pdo_sqlite', 'sqlite3'], true) && class_exists(EnableForeignKeys::class)) { + $configuration->setMiddlewares([new EnableForeignKeys()]); + } + + $connection = DriverManager::getConnection($connectionParameters, $configuration); assert($connection instanceof DbalExtensions\Connection); self::addDbEventSubscribers($connection); @@ -212,6 +221,7 @@ private static function mapConnectionParameters(array $configuration, string $pr 'port', 'server', 'memory', + 'path', 'ssl_key', 'ssl_cert', 'ssl_ca',