From 42af7cabb7496d77104132f9d164f9c74da6a629 Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Wed, 11 Oct 2023 16:04:47 +0200 Subject: [PATCH 1/4] Cover calling AbstractQuery::setParameter() with an array parameter (#10996) --- .../Tests/ORM/Functional/NativeQueryTest.php | 41 +++++++++++++++++++ tests/Doctrine/Tests/ORM/Query/QueryTest.php | 23 +++++++++++ 2 files changed, 64 insertions(+) diff --git a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php index 521393e9692..e998a15c837 100644 --- a/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/NativeQueryTest.php @@ -5,6 +5,8 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\DBAL\ArrayParameterType; +use Doctrine\DBAL\Connection; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type as DBALType; use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; @@ -31,6 +33,8 @@ use Doctrine\Tests\OrmFunctionalTestCase; use InvalidArgumentException; +use function class_exists; + class NativeQueryTest extends OrmFunctionalTestCase { use SQLResultCasing; @@ -75,6 +79,43 @@ public function testBasicNativeQuery(): void self::assertEquals('Roman', $users[0]->name); } + public function testNativeQueryWithArrayParameter(): void + { + $user = new CmsUser(); + $user->name = 'William Shatner'; + $user->username = 'wshatner'; + $user->status = 'dev'; + $this->_em->persist($user); + $user = new CmsUser(); + $user->name = 'Leonard Nimoy'; + $user->username = 'lnimoy'; + $user->status = 'dev'; + $this->_em->persist($user); + $user = new CmsUser(); + $user->name = 'DeForest Kelly'; + $user->username = 'dkelly'; + $user->status = 'dev'; + $this->_em->persist($user); + $this->_em->flush(); + + $this->_em->clear(); + + $rsm = new ResultSetMapping(); + $rsm->addEntityResult(CmsUser::class, 'u'); + $rsm->addFieldResult('u', $this->getSQLResultCasing($this->platform, 'id'), 'id'); + $rsm->addFieldResult('u', $this->getSQLResultCasing($this->platform, 'name'), 'name'); + + $query = $this->_em->createNativeQuery('SELECT id, name FROM cms_users WHERE username IN (?) ORDER BY username', $rsm); + $query->setParameter(1, ['wshatner', 'lnimoy'], class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY); + + $users = $query->getResult(); + + self::assertCount(2, $users); + self::assertInstanceOf(CmsUser::class, $users[0]); + self::assertEquals('Leonard Nimoy', $users[0]->name); + self::assertEquals('William Shatner', $users[1]->name); + } + public function testBasicNativeQueryWithMetaResult(): void { $user = new CmsUser(); diff --git a/tests/Doctrine/Tests/ORM/Query/QueryTest.php b/tests/Doctrine/Tests/ORM/Query/QueryTest.php index 1b69766f772..7a1ffa0d702 100644 --- a/tests/Doctrine/Tests/ORM/Query/QueryTest.php +++ b/tests/Doctrine/Tests/ORM/Query/QueryTest.php @@ -10,7 +10,9 @@ use Doctrine\Common\Cache\Psr6\CacheAdapter; use Doctrine\Common\Cache\Psr6\DoctrineProvider; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Cache\QueryCacheProfile; +use Doctrine\DBAL\Connection; use Doctrine\DBAL\ParameterType; use Doctrine\DBAL\Types\Types; use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; @@ -36,6 +38,7 @@ use function array_map; use function assert; +use function class_exists; use function method_exists; use const PHP_VERSION_ID; @@ -251,6 +254,26 @@ public function testCollectionParameters(): void self::assertEquals($cities, $parameter->getValue()); } + /** @group DDC-1697 */ + public function testExplicitCollectionParameters(): void + { + $cities = [ + 0 => 'Paris', + 3 => 'Cannes', + 9 => 'St Julien', + ]; + + $query = $this->entityManager + ->createQuery('SELECT a FROM Doctrine\Tests\Models\CMS\CmsAddress a WHERE a.city IN (:cities)') + ->setParameter('cities', $cities, class_exists(ArrayParameterType::class) ? ArrayParameterType::STRING : Connection::PARAM_STR_ARRAY); + + $parameters = $query->getParameters(); + $parameter = $parameters->first(); + + self::assertEquals('cities', $parameter->getName()); + self::assertEquals($cities, $parameter->getValue()); + } + /** @psalm-return Generator */ public static function provideProcessParameterValueIterable(): Generator { From d84f60748791a4278feae810a909d0915ea35dcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Fri, 13 Oct 2023 19:50:25 +0200 Subject: [PATCH 2/4] Address split of doctrine/common doctrine/common has been split in several packages. A lot of what was true about doctrine/common is true about doctrine/persistence today, so let us simply reuse the existing paragraphs and mention persistence instead of common. --- docs/en/reference/architecture.rst | 32 +++++++++++++++++------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/en/reference/architecture.rst b/docs/en/reference/architecture.rst index c9d16a69ff9..00d1c419734 100644 --- a/docs/en/reference/architecture.rst +++ b/docs/en/reference/architecture.rst @@ -24,28 +24,34 @@ performance it is also recommended that you use APC with PHP. Doctrine ORM Packages ------------------- -Doctrine ORM is divided into three main packages. +Doctrine ORM is divided into four main packages. -- Common -- DBAL (includes Common) -- ORM (includes DBAL+Common) +- `Collections `_ +- `Event Manager `_ +- `Persistence `_ +- `DBAL `_ +- ORM (depends on DBAL+Persistence+Collections) This manual mainly covers the ORM package, sometimes touching parts -of the underlying DBAL and Common packages. The Doctrine code base +of the underlying DBAL and Persistence packages. The Doctrine code base is split in to these packages for a few reasons and they are to... - ...make things more maintainable and decoupled -- ...allow you to use the code in Doctrine Common without the ORM - or DBAL +- ...allow you to use the code in Doctrine Persistence and Collections + without the ORM or DBAL - ...allow you to use the DBAL without the ORM -The Common Package -~~~~~~~~~~~~~~~~~~ +Collection, Event Manager and Persistence +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The Common package contains highly reusable components that have no -dependencies beyond the package itself (and PHP, of course). The -root namespace of the Common package is ``Doctrine\Common``. +The Collection, Event Manager and Persistence packages contain highly +reusable components that have no dependencies beyond the packages +themselves (and PHP, of course). The root namespace of the Persistence +package is ``Doctrine\Persistence``. The root namespace of the +Collection package is ``Doctrine\Common\Collections``, for historical +reasons. The root namespace of the Event Manager package is just +``Doctrine\Common``, also for historical reasons. The DBAL Package ~~~~~~~~~~~~~~~~ @@ -199,5 +205,3 @@ typical implementation of the to keep track of all the things that need to be done the next time ``flush`` is invoked. You usually do not directly interact with a ``UnitOfWork`` but with the ``EntityManager`` instead. - - From 866283d1a7a316390890276df92b12645251ba2e Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Tue, 17 Oct 2023 18:19:38 +0200 Subject: [PATCH 3/4] Fix the support for enum types in the ResultSetMappingBuilder --- .../ORM/Query/ResultSetMappingBuilder.php | 5 ++ .../Ticket/GH11017/GH11017Entity.php | 29 +++++++++++ .../Functional/Ticket/GH11017/GH11017Enum.php | 11 +++++ .../Functional/Ticket/GH11017/GH11017Test.php | 49 +++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/GH11017/GH11017Entity.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/GH11017/GH11017Enum.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/GH11017/GH11017Test.php diff --git a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php index a65b52c87f0..8002df787b6 100644 --- a/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php +++ b/lib/Doctrine/ORM/Query/ResultSetMappingBuilder.php @@ -154,6 +154,11 @@ protected function addAllClassFields($class, $alias, $columnAliasMap = []) } $this->addFieldResult($alias, $columnAlias, $propertyName); + + $enumType = $classMetadata->getFieldMapping($propertyName)['enumType'] ?? null; + if (! empty($enumType)) { + $this->addEnumResult($columnAlias, $enumType); + } } foreach ($classMetadata->associationMappings as $associationMapping) { diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH11017/GH11017Entity.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH11017/GH11017Entity.php new file mode 100644 index 00000000000..25d0a90d733 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH11017/GH11017Entity.php @@ -0,0 +1,29 @@ +setUpEntitySchema([ + GH11017Entity::class, + ]); + } + + public function testPostPersistListenerUpdatingObjectFieldWhileOtherInsertPending(): void + { + $entity1 = new GH11017Entity(); + $entity1->field = GH11017Enum::FIRST; + $this->_em->persist($entity1); + + $this->_em->flush(); + $this->_em->clear(); + + $rsm = new ResultSetMappingBuilder($this->_em, ResultSetMappingBuilder::COLUMN_RENAMING_INCREMENT); + $rsm->addRootEntityFromClassMetadata(GH11017Entity::class, 'e'); + + $tableName = $this->_em->getClassMetadata(GH11017Entity::class)->getTableName(); + $sql = sprintf('SELECT %s FROM %s e WHERE id = :id', $rsm->generateSelectClause(), $tableName); + + $query = $this->_em->createNativeQuery($sql, $rsm) + ->setParameter('id', $entity1->id); + + $entity1Reloaded = $query->getSingleResult(AbstractQuery::HYDRATE_ARRAY); + self::assertNotNull($entity1Reloaded); + self::assertSame($entity1->field, $entity1Reloaded['field']); + } +} From 293299a3149f1f810dd133e4c0a99fc9c29391bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Mon, 16 Oct 2023 13:27:00 +0200 Subject: [PATCH 4/4] Make phpdoc accurate When transforming these phpdoc types into native types, things break down. They are correct according to the EBNF, but in practice, there are so-called phase 2 optimizations that allow using ConditionalPrimary, ConditionalFactor and ConditionalTerm instances in places where ConditionalExpression is used. --- .../ORM/Query/AST/ConditionalFactor.php | 2 +- .../ORM/Query/AST/ConditionalPrimary.php | 4 +-- .../ORM/Query/AST/ConditionalTerm.php | 2 +- lib/Doctrine/ORM/Query/AST/HavingClause.php | 4 +-- lib/Doctrine/ORM/Query/AST/Join.php | 2 +- .../AST/Phase2OptimizableConditional.php | 17 ++++++++++++ lib/Doctrine/ORM/Query/AST/WhenClause.php | 6 ++--- lib/Doctrine/ORM/Query/AST/WhereClause.php | 4 +-- lib/Doctrine/ORM/Query/SqlWalker.php | 12 ++++----- .../ORM/Tools/Pagination/WhereInWalker.php | 6 +---- phpstan-baseline.neon | 15 ++++------- psalm-baseline.xml | 27 ------------------- 12 files changed, 41 insertions(+), 60 deletions(-) create mode 100644 lib/Doctrine/ORM/Query/AST/Phase2OptimizableConditional.php diff --git a/lib/Doctrine/ORM/Query/AST/ConditionalFactor.php b/lib/Doctrine/ORM/Query/AST/ConditionalFactor.php index 654b0259263..a39a02c819a 100644 --- a/lib/Doctrine/ORM/Query/AST/ConditionalFactor.php +++ b/lib/Doctrine/ORM/Query/AST/ConditionalFactor.php @@ -9,7 +9,7 @@ * * @link www.doctrine-project.org */ -class ConditionalFactor extends Node +class ConditionalFactor extends Node implements Phase2OptimizableConditional { /** @var bool */ public $not = false; diff --git a/lib/Doctrine/ORM/Query/AST/ConditionalPrimary.php b/lib/Doctrine/ORM/Query/AST/ConditionalPrimary.php index 381e81488ac..c0c7e917b20 100644 --- a/lib/Doctrine/ORM/Query/AST/ConditionalPrimary.php +++ b/lib/Doctrine/ORM/Query/AST/ConditionalPrimary.php @@ -9,12 +9,12 @@ * * @link www.doctrine-project.org */ -class ConditionalPrimary extends Node +class ConditionalPrimary extends Node implements Phase2OptimizableConditional { /** @var Node|null */ public $simpleConditionalExpression; - /** @var ConditionalExpression|null */ + /** @var ConditionalExpression|Phase2OptimizableConditional|null */ public $conditionalExpression; /** @return bool */ diff --git a/lib/Doctrine/ORM/Query/AST/ConditionalTerm.php b/lib/Doctrine/ORM/Query/AST/ConditionalTerm.php index 9444422be24..37cd95e4e30 100644 --- a/lib/Doctrine/ORM/Query/AST/ConditionalTerm.php +++ b/lib/Doctrine/ORM/Query/AST/ConditionalTerm.php @@ -9,7 +9,7 @@ * * @link www.doctrine-project.org */ -class ConditionalTerm extends Node +class ConditionalTerm extends Node implements Phase2OptimizableConditional { /** @var mixed[] */ public $conditionalFactors = []; diff --git a/lib/Doctrine/ORM/Query/AST/HavingClause.php b/lib/Doctrine/ORM/Query/AST/HavingClause.php index fbbc3930045..f2891a4c745 100644 --- a/lib/Doctrine/ORM/Query/AST/HavingClause.php +++ b/lib/Doctrine/ORM/Query/AST/HavingClause.php @@ -6,10 +6,10 @@ class HavingClause extends Node { - /** @var ConditionalExpression */ + /** @var ConditionalExpression|Phase2OptimizableConditional */ public $conditionalExpression; - /** @param ConditionalExpression $conditionalExpression */ + /** @param ConditionalExpression|Phase2OptimizableConditional $conditionalExpression */ public function __construct($conditionalExpression) { $this->conditionalExpression = $conditionalExpression; diff --git a/lib/Doctrine/ORM/Query/AST/Join.php b/lib/Doctrine/ORM/Query/AST/Join.php index 7b0759228ad..9c595a457ae 100644 --- a/lib/Doctrine/ORM/Query/AST/Join.php +++ b/lib/Doctrine/ORM/Query/AST/Join.php @@ -25,7 +25,7 @@ class Join extends Node /** @var Node|null */ public $joinAssociationDeclaration = null; - /** @var ConditionalExpression|null */ + /** @var ConditionalExpression|Phase2OptimizableConditional|null */ public $conditionalExpression = null; /** diff --git a/lib/Doctrine/ORM/Query/AST/Phase2OptimizableConditional.php b/lib/Doctrine/ORM/Query/AST/Phase2OptimizableConditional.php new file mode 100644 index 00000000000..276f8f8d945 --- /dev/null +++ b/lib/Doctrine/ORM/Query/AST/Phase2OptimizableConditional.php @@ -0,0 +1,17 @@ +conditionalExpression = $conditionalExpression; diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index a677ca26710..2bbd800c3b5 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -1010,9 +1010,9 @@ private function generateRangeVariableDeclarationSQL( /** * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL. * - * @param AST\JoinAssociationDeclaration $joinAssociationDeclaration - * @param int $joinType - * @param AST\ConditionalExpression $condExpr + * @param AST\JoinAssociationDeclaration $joinAssociationDeclaration + * @param int $joinType + * @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr * @psalm-param AST\Join::JOIN_TYPE_* $joinType * * @return string @@ -2048,7 +2048,7 @@ public function walkWhereClause($whereClause) /** * Walk down a ConditionalExpression AST node, thereby generating the appropriate SQL. * - * @param AST\ConditionalExpression $condExpr + * @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr * * @return string * @@ -2068,7 +2068,7 @@ public function walkConditionalExpression($condExpr) /** * Walks down a ConditionalTerm AST node, thereby generating the appropriate SQL. * - * @param AST\ConditionalTerm $condTerm + * @param AST\ConditionalTerm|AST\ConditionalFactor|AST\ConditionalPrimary $condTerm * * @return string * @@ -2088,7 +2088,7 @@ public function walkConditionalTerm($condTerm) /** * Walks down a ConditionalFactor AST node, thereby generating the appropriate SQL. * - * @param AST\ConditionalFactor $factor + * @param AST\ConditionalFactor|AST\ConditionalPrimary $factor * * @return string The SQL. * diff --git a/lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php b/lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php index 6613e1e7474..0e88dad6ef0 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php +++ b/lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php @@ -6,7 +6,6 @@ use Doctrine\ORM\Query\AST\ArithmeticExpression; use Doctrine\ORM\Query\AST\ConditionalExpression; -use Doctrine\ORM\Query\AST\ConditionalFactor; use Doctrine\ORM\Query\AST\ConditionalPrimary; use Doctrine\ORM\Query\AST\ConditionalTerm; use Doctrine\ORM\Query\AST\InListExpression; @@ -96,10 +95,7 @@ public function walkSelectStatement(SelectStatement $AST) ), ] ); - } elseif ( - $AST->whereClause->conditionalExpression instanceof ConditionalExpression - || $AST->whereClause->conditionalExpression instanceof ConditionalFactor - ) { + } else { $tmpPrimary = new ConditionalPrimary(); $tmpPrimary->conditionalExpression = $AST->whereClause->conditionalExpression; $AST->whereClause->conditionalExpression = new ConditionalTerm( diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 87d9dc9837c..f85a4fc74cd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -440,6 +440,11 @@ parameters: count: 1 path: lib/Doctrine/ORM/Query/SqlWalker.php + - + message: "#^Parameter \\#1 \\$condTerm of method Doctrine\\\\ORM\\\\Query\\\\SqlWalker\\:\\:walkConditionalTerm\\(\\) expects Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalFactor\\|Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalPrimary\\|Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalTerm, Doctrine\\\\ORM\\\\Query\\\\AST\\\\Phase2OptimizableConditional given\\.$#" + count: 1 + path: lib/Doctrine/ORM/Query/SqlWalker.php + - message: "#^Result of && is always false\\.$#" count: 1 @@ -595,16 +600,6 @@ parameters: count: 1 path: lib/Doctrine/ORM/Tools/Export/Driver/YamlExporter.php - - - message: "#^Instanceof between \\*NEVER\\* and Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalFactor will always evaluate to false\\.$#" - count: 1 - path: lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php - - - - message: "#^Instanceof between Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalExpression and Doctrine\\\\ORM\\\\Query\\\\AST\\\\ConditionalPrimary will always evaluate to false\\.$#" - count: 1 - path: lib/Doctrine/ORM/Tools/Pagination/WhereInWalker.php - - message: "#^Else branch is unreachable because ternary operator condition is always true\\.$#" count: 1 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 1414b70c24d..17103a694c0 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -2022,18 +2022,13 @@ $AST - $conditionalExpression $expr $pathExp - ConditionalExpression()]]> - ConditionalExpression()]]> lexer->getLiteral($token)]]> lexer->getLiteral($token)]]> lexer->getLiteral($token)]]> - ConditionalExpression()]]> - ConditionalExpression()]]> SimpleArithmeticExpression()]]> @@ -2145,11 +2140,6 @@ $expr - - $condExpr - $condTerm - $factor - string @@ -2158,7 +2148,6 @@ pathExpression]]> - conditionalExpression]]> whereClause]]> @@ -2194,7 +2183,6 @@ $whereClause !== null - not ? 'NOT ' : '') . $this->walkConditionalPrimary($factor->conditionalPrimary)]]> @@ -2633,21 +2621,6 @@ $orderByClause - - - whereClause->conditionalExpression instanceof ConditionalExpression - || $AST->whereClause->conditionalExpression instanceof ConditionalFactor]]> - whereClause->conditionalExpression instanceof ConditionalFactor]]> - whereClause->conditionalExpression instanceof ConditionalPrimary]]> - - - whereClause->conditionalExpression]]> - - - whereClause->conditionalExpression instanceof ConditionalExpression - || $AST->whereClause->conditionalExpression instanceof ConditionalFactor]]> - - $classes