From 710dde83aa7e3661d5814abb65f6bc401af90152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Tue, 1 Aug 2023 14:56:34 +0200 Subject: [PATCH 01/10] Update branch metadata (#10862) --- .doctrine-project.json | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.doctrine-project.json b/.doctrine-project.json index c25d00e444..e1f84c7b85 100644 --- a/.doctrine-project.json +++ b/.doctrine-project.json @@ -12,21 +12,27 @@ "upcoming": true }, { - "name": "2.16", - "branchName": "2.16.x", - "slug": "2.16", + "name": "2.17", + "branchName": "2.17.x", + "slug": "2.17", "upcoming": true }, { - "name": "2.15", - "branchName": "2.15.x", - "slug": "2.15", + "name": "2.16", + "branchName": "2.16.x", + "slug": "2.16", "current": true, "aliases": [ "current", "stable" ] }, + { + "name": "2.15", + "branchName": "2.15.x", + "slug": "2.15", + "maintained": false + }, { "name": "2.14", "branchName": "2.14.x", From eeefc6bc0fad9cc0bae2687e3dfe5d08a3b64375 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Wed, 2 Aug 2023 13:42:49 +0200 Subject: [PATCH 02/10] Add an UPGRADE notice about the potential changes in commit order (#10866) --- UPGRADE.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/UPGRADE.md b/UPGRADE.md index ef703efa09..b7d099fe9b 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,17 @@ # Upgrade to 2.16 +## Potential changes to the order in which `INSERT`s are executed + +In https://github.com/doctrine/orm/pull/10547, the commit order computation was improved +to fix a series of bugs where a correct (working) commit order was previously not found. +Also, the new computation may get away with fewer queries being executed: By inserting +referred-to entities first and using their ID values for foreign key fields in subsequent +`INSERT` statements, additional `UPDATE` statements that were previously necessary can be +avoided. + +When using database-provided, auto-incrementing IDs, this may lead to IDs being assigned +to entities in a different order than it was previously the case. + ## Deprecated `\Doctrine\ORM\Internal\CommitOrderCalculator` and related classes With changes made to the commit order computation, the internal classes From f50803ccb91c5c31aede16df73fa7560a42f02ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Ol=C5=A1avsk=C3=BD?= Date: Wed, 2 Aug 2023 13:44:15 +0200 Subject: [PATCH 03/10] Fix UnitOfWork->originalEntityData is missing not-modified collections after computeChangeSet (#9301) * Fix original data incomplete after flush * Apply suggestions from code review Co-authored-by: Alexander M. Turek --------- Co-authored-by: Alexander M. Turek --- lib/Doctrine/ORM/UnitOfWork.php | 1 + .../Tests/Models/Issue9300/Issue9300Child.php | 44 ++++++++++ .../Models/Issue9300/Issue9300Parent.php | 30 +++++++ .../ORM/Functional/Ticket/Issue9300Test.php | 80 +++++++++++++++++++ .../Doctrine/Tests/OrmFunctionalTestCase.php | 4 + 5 files changed, 159 insertions(+) create mode 100644 tests/Doctrine/Tests/Models/Issue9300/Issue9300Child.php create mode 100644 tests/Doctrine/Tests/Models/Issue9300/Issue9300Parent.php create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/Issue9300Test.php diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 5bf8a6b608..f9b8cf2b89 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -692,6 +692,7 @@ public function computeChangeSet(ClassMetadata $class, $entity) if ($class->isCollectionValuedAssociation($name) && $value !== null) { if ($value instanceof PersistentCollection) { if ($value->getOwner() === $entity) { + $actualData[$name] = $value; continue; } diff --git a/tests/Doctrine/Tests/Models/Issue9300/Issue9300Child.php b/tests/Doctrine/Tests/Models/Issue9300/Issue9300Child.php new file mode 100644 index 0000000000..0919d6d33b --- /dev/null +++ b/tests/Doctrine/Tests/Models/Issue9300/Issue9300Child.php @@ -0,0 +1,44 @@ + + * @ManyToMany(targetEntity="Issue9300Parent") + */ + public $parents; + + /** + * @var string + * @Column(type="string") + */ + public $name; + + public function __construct() + { + $this->parents = new ArrayCollection(); + } +} diff --git a/tests/Doctrine/Tests/Models/Issue9300/Issue9300Parent.php b/tests/Doctrine/Tests/Models/Issue9300/Issue9300Parent.php new file mode 100644 index 0000000000..f9b50b36c9 --- /dev/null +++ b/tests/Doctrine/Tests/Models/Issue9300/Issue9300Parent.php @@ -0,0 +1,30 @@ +useModelSet('issue9300'); + + parent::setUp(); + } + + /** + * @group GH-9300 + */ + public function testPersistedCollectionIsPresentInOriginalDataAfterFlush(): void + { + $parent = new Issue9300Parent(); + $child = new Issue9300Child(); + $child->parents->add($parent); + + $parent->name = 'abc'; + $child->name = 'abc'; + + $this->_em->persist($parent); + $this->_em->persist($child); + $this->_em->flush(); + + $parent->name = 'abcd'; + $child->name = 'abcd'; + + $this->_em->flush(); + + self::assertArrayHasKey('parents', $this->_em->getUnitOfWork()->getOriginalEntityData($child)); + } + + /** + * @group GH-9300 + */ + public function testPersistingCollectionAfterFlushWorksAsExpected(): void + { + $parentOne = new Issue9300Parent(); + $parentTwo = new Issue9300Parent(); + $childOne = new Issue9300Child(); + + $parentOne->name = 'abc'; + $parentTwo->name = 'abc'; + $childOne->name = 'abc'; + $childOne->parents = new ArrayCollection([$parentOne]); + + $this->_em->persist($parentOne); + $this->_em->persist($parentTwo); + $this->_em->persist($childOne); + $this->_em->flush(); + + // Recalculate change-set -> new original data + $childOne->name = 'abcd'; + $this->_em->flush(); + + $childOne->parents = new ArrayCollection([$parentTwo]); + + $this->_em->flush(); + $this->_em->clear(); + + $childOneFresh = $this->_em->find(Issue9300Child::class, $childOne->id); + self::assertCount(1, $childOneFresh->parents); + self::assertEquals($parentTwo->id, $childOneFresh->parents[0]->id); + } +} diff --git a/tests/Doctrine/Tests/OrmFunctionalTestCase.php b/tests/Doctrine/Tests/OrmFunctionalTestCase.php index 07bf94eb65..4a64136f18 100644 --- a/tests/Doctrine/Tests/OrmFunctionalTestCase.php +++ b/tests/Doctrine/Tests/OrmFunctionalTestCase.php @@ -338,6 +338,10 @@ abstract class OrmFunctionalTestCase extends OrmTestCase Models\Issue5989\Issue5989Employee::class, Models\Issue5989\Issue5989Manager::class, ], + 'issue9300' => [ + Models\Issue9300\Issue9300Child::class, + Models\Issue9300\Issue9300Parent::class, + ], ]; /** @param class-string ...$models */ From fd0bdc69b0a25b9de99de814be60dbc163ac84a2 Mon Sep 17 00:00:00 2001 From: Dieter Beck Date: Wed, 2 Aug 2023 14:34:13 +0200 Subject: [PATCH 04/10] Add possibility to set reportFieldsWhereDeclared to true in ORMSetup (#10865) Otherwise it is impossible to avoid a deprecation warning when using ORMSetup::createAttributeMetadataConfiguration() --- docs/en/reference/advanced-configuration.rst | 4 ++-- lib/Doctrine/ORM/ORMSetup.php | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index fdd42aeb6c..6761ac1b97 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -29,7 +29,7 @@ steps of configuration. $config = new Configuration; $config->setMetadataCache($metadataCache); - $driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']); + $driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true); $config->setMetadataDriverImpl($driverImpl); $config->setQueryCache($queryCache); $config->setProxyDir('/path/to/myproject/lib/MyProject/Proxies'); @@ -134,7 +134,7 @@ The attribute driver can be injected in the ``Doctrine\ORM\Configuration``: setMetadataDriverImpl($driverImpl); The path information to the entities is required for the attribute diff --git a/lib/Doctrine/ORM/ORMSetup.php b/lib/Doctrine/ORM/ORMSetup.php index 4b66762861..7210209727 100644 --- a/lib/Doctrine/ORM/ORMSetup.php +++ b/lib/Doctrine/ORM/ORMSetup.php @@ -101,10 +101,11 @@ public static function createAttributeMetadataConfiguration( array $paths, bool $isDevMode = false, ?string $proxyDir = null, - ?CacheItemPoolInterface $cache = null + ?CacheItemPoolInterface $cache = null, + bool $reportFieldsWhereDeclared = false ): Configuration { $config = self::createConfiguration($isDevMode, $proxyDir, $cache); - $config->setMetadataDriverImpl(new AttributeDriver($paths)); + $config->setMetadataDriverImpl(new AttributeDriver($paths, $reportFieldsWhereDeclared)); return $config; } From a616914887ea160db4158d2c67752e99624f7c8a Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Fri, 4 Aug 2023 14:06:02 +0200 Subject: [PATCH 05/10] Turn identity map collisions from exception to deprecation notice (#10878) In #10785, a check was added that prevents entity instances from getting into the identity map when another object for the same ID is already being tracked. This caused regressions for users that work with application-provided IDs and expect this condition to fail with `UniqueConstraintViolationExceptions` when flushing to the database. Thus, this PR turns the exception into a deprecation notice. Users can opt-in to the new behavior. In 3.0, the exception will be used. Implements #10871. --- UPGRADE.md | 14 ++++++ lib/Doctrine/ORM/Configuration.php | 10 ++++ lib/Doctrine/ORM/UnitOfWork.php | 47 ++++++++++++++++--- .../ORM/Functional/BasicFunctionalTest.php | 2 + tests/Doctrine/Tests/ORM/UnitOfWorkTest.php | 20 ++++++++ 5 files changed, 86 insertions(+), 7 deletions(-) diff --git a/UPGRADE.md b/UPGRADE.md index b7d099fe9b..326d17de07 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,5 +1,19 @@ # Upgrade to 2.16 +## Deprecated accepting duplicate IDs in the identity map + +For any given entity class and ID value, there should be only one object instance +representing the entity. + +In https://github.com/doctrine/orm/pull/10785, a check was added that will guard this +in the identity map. The most probable cause for violations of this rule are collisions +of application-provided IDs. + +In ORM 2.16.0, the check was added by throwing an exception. In ORM 2.16.1, this will be +changed to a deprecation notice. ORM 3.0 will make it an exception again. Use +`\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap()` if you want to opt-in +to the new mode. + ## Potential changes to the order in which `INSERT`s are executed In https://github.com/doctrine/orm/pull/10547, the commit order computation was improved diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index fcde0c190f..ca5063667c 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -1117,4 +1117,14 @@ public function setLazyGhostObjectEnabled(bool $flag): void $this->_attributes['isLazyGhostObjectEnabled'] = $flag; } + + public function setRejectIdCollisionInIdentityMap(bool $flag): void + { + $this->_attributes['rejectIdCollisionInIdentityMap'] = $flag; + } + + public function isRejectIdCollisionInIdentityMapEnabled(): bool + { + return $this->_attributes['rejectIdCollisionInIdentityMap'] ?? false; + } } diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index f9b8cf2b89..64ca810e78 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -1635,8 +1635,10 @@ public function addToIdentityMap($entity) if (isset($this->identityMap[$className][$idHash])) { if ($this->identityMap[$className][$idHash] !== $entity) { - throw new RuntimeException(sprintf( - <<<'EXCEPTION' + if ($this->em->getConfiguration()->isRejectIdCollisionInIdentityMapEnabled()) { + throw new RuntimeException( + sprintf( + <<<'EXCEPTION' While adding an entity of class %s with an ID hash of "%s" to the identity map, another object of class %s was already present for the same ID. This exception is a safeguard against an internal inconsistency - IDs should uniquely map to @@ -1651,11 +1653,42 @@ public function addToIdentityMap($entity) Otherwise, it might be an ORM-internal inconsistency, please report it. EXCEPTION - , - get_class($entity), - $idHash, - get_class($this->identityMap[$className][$idHash]) - )); + , + get_class($entity), + $idHash, + get_class($this->identityMap[$className][$idHash]) + ) + ); + } else { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/10785', + <<<'EXCEPTION' +While adding an entity of class %s with an ID hash of "%s" to the identity map, +another object of class %s was already present for the same ID. This will trigger +an exception in ORM 3.0. + +IDs should uniquely map to entity object instances. This problem may occur if: + +- you use application-provided IDs and reuse ID values; +- database-provided IDs are reassigned after truncating the database without +clearing the EntityManager; +- you might have been using EntityManager#getReference() to create a reference +for a nonexistent ID that was subsequently (by the RDBMS) assigned to another +entity. + +Otherwise, it might be an ORM-internal inconsistency, please report it. + +To opt-in to the new exception, call +\Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap on the entity +manager's configuration. +EXCEPTION + , + get_class($entity), + $idHash, + get_class($this->identityMap[$className][$idHash]) + ); + } } return false; diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 7356a1bfe0..2be3feba4e 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -1329,6 +1329,8 @@ public function testWrongAssociationInstance(): void public function testItThrowsWhenReferenceUsesIdAssignedByDatabase(): void { + $this->_em->getConfiguration()->setRejectIdCollisionInIdentityMap(true); + $user = new CmsUser(); $user->name = 'test'; $user->username = 'test'; diff --git a/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php b/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php index e47af9d57d..31fa5ea8f6 100644 --- a/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php +++ b/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php @@ -927,8 +927,28 @@ public function testRemovedEntityIsRemovedFromOneToManyCollection(): void self::assertEmpty($user->phonenumbers->getSnapshot()); } + public function testItTriggersADeprecationNoticeWhenApplicationProvidedIdsCollide(): void + { + // We're using application-provided IDs and assign the same ID twice + // Note this is about colliding IDs in the identity map in memory. + // Duplicate database-level IDs would be spotted when the EM is flushed. + + $phone1 = new CmsPhonenumber(); + $phone1->phonenumber = '1234'; + $this->_unitOfWork->persist($phone1); + + $phone2 = new CmsPhonenumber(); + $phone2->phonenumber = '1234'; + + $this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/10785'); + + $this->_unitOfWork->persist($phone2); + } + public function testItThrowsWhenApplicationProvidedIdsCollide(): void { + $this->_emMock->getConfiguration()->setRejectIdCollisionInIdentityMap(true); + // We're using application-provided IDs and assign the same ID twice // Note this is about colliding IDs in the identity map in memory. // Duplicate database-level IDs would be spotted when the EM is flushed. From 440b244ebc34a80e0d4e3dffcf90dafe8d0ad152 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Wed, 9 Aug 2023 11:34:53 +0200 Subject: [PATCH 06/10] Fix broken changeset computation for entities loaded through fetch=EAGER + using inheritance (#10884) #10880 reports a case where the changes from #10785 cause entity updates to be missed. Upon closer inspection, this change seems to be causing it: https://github.com/doctrine/orm/pull/10785/files#diff-55a900494fc8033ab498c53929716caf0aa39d6bdd7058e7d256787a24412ee4L2990-L3003 The code was changed to use `registerManaged()` instead, which basically does the same things, but (since #10785) also includes an additional check against duplicate entity instances. But, one detail slipped through tests and reviews: `registerManaged()` also updates `\Doctrine\ORM\UnitOfWork::$originalEntityData`, which is used to compute entity changesets. An empty array `[]` was passed for $data here. This will make the changeset computation assume that a partial object was loaded and effectively ignore all field updates here: https://github.com/doctrine/orm/blob/a616914887ea160db4158d2c67752e99624f7c8a/lib/Doctrine/ORM/UnitOfWork.php#L762-L764 I think that, effectively, it is sufficient to call `registerManaged()` only in the two cases where a proxy was created. Calling `registerManaged()` with `[]` as data for a proxy object is consistent with e. g. `\Doctrine\ORM\EntityManager::getReference()`. In the case that a full entity has to be loaded, we need not call `registerManaged()` at all, since that will already happen inside `EntityManager::find()` (or, more specifically, `UnitOfWork::createEntity()` called inside it). Note that the test case has to make some provisions so that we actually reach this case: * Load an entity that uses `fetch="EAGER"` on a to-one association * That association being against a class that uses inheritance (why's that?) --- lib/Doctrine/ORM/UnitOfWork.php | 9 +- .../ORM/Functional/Ticket/GH10880Test.php | 119 ++++++++++++++++++ 2 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/GH10880Test.php diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index 64ca810e78..b4e1a8ab45 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -3048,6 +3048,7 @@ public function createEntity($className, array $data, &$hints = []) // We are negating the condition here. Other cases will assume it is valid! case $hints['fetchMode'][$class->name][$field] !== ClassMetadata::FETCH_EAGER: $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId); + $this->registerManaged($newValue, $associatedId, []); break; // Deferred eager load only works for single identifier classes @@ -3056,6 +3057,7 @@ public function createEntity($className, array $data, &$hints = []) $this->eagerLoadingEntities[$targetClass->rootEntityName][$relatedIdHash] = current($normalizedAssociatedId); $newValue = $this->em->getProxyFactory()->getProxy($assoc['targetEntity'], $normalizedAssociatedId); + $this->registerManaged($newValue, $associatedId, []); break; default: @@ -3063,13 +3065,6 @@ public function createEntity($className, array $data, &$hints = []) $newValue = $this->em->find($assoc['targetEntity'], $normalizedAssociatedId); break; } - - if ($newValue === null) { - break; - } - - $this->registerManaged($newValue, $associatedId, []); - break; } $this->originalEntityData[$oid][$field] = $newValue; diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10880Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10880Test.php new file mode 100644 index 0000000000..dd299fdfdd --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10880Test.php @@ -0,0 +1,119 @@ +setUpEntitySchema([ + GH10880BaseProcess::class, + GH10880Process::class, + GH10880ProcessOwner::class, + ]); + } + + public function testProcessShouldBeUpdated(): void + { + $process = new GH10880Process(); + $process->description = 'first value'; + + $owner = new GH10880ProcessOwner(); + $owner->process = $process; + + $this->_em->persist($process); + $this->_em->persist($owner); + $this->_em->flush(); + $this->_em->clear(); + + $ownerLoaded = $this->_em->getRepository(GH10880ProcessOwner::class)->find($owner->id); + $ownerLoaded->process->description = 'other description'; + + $queryLog = $this->getQueryLog(); + $queryLog->reset()->enable(); + $this->_em->flush(); + + $this->removeTransactionCommandsFromQueryLog(); + + self::assertCount(1, $queryLog->queries); + $query = reset($queryLog->queries); + self::assertSame('UPDATE GH10880BaseProcess SET description = ? WHERE id = ?', $query['sql']); + } + + private function removeTransactionCommandsFromQueryLog(): void + { + $log = $this->getQueryLog(); + + foreach ($log->queries as $key => $entry) { + if ($entry['sql'] === '"START TRANSACTION"' || $entry['sql'] === '"COMMIT"') { + unset($log->queries[$key]); + } + } + } +} + +/** + * @ORM\Entity + */ +class GH10880ProcessOwner +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * + * @var int + */ + public $id; + + /** + * fetch=EAGER is important to reach the part of \Doctrine\ORM\UnitOfWork::createEntity() + * that is important for this regression test + * + * @ORM\ManyToOne(targetEntity="GH10880Process", fetch="EAGER") + * + * @var GH10880Process + */ + public $process; +} + +/** + * @ORM\Entity() + * @ORM\InheritanceType("SINGLE_TABLE") + * @ORM\DiscriminatorColumn(name="type", type="string") + * @ORM\DiscriminatorMap({"process" = "GH10880Process"}) + */ +abstract class GH10880BaseProcess +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * + * @var int + */ + public $id; + + /** + * @ORM\Column(type="text") + * + * @var string + */ + public $description; +} + +/** + * @ORM\Entity + */ +class GH10880Process extends GH10880BaseProcess +{ +} From 16c015183141fab99f78c5d73c0c3b311821123d Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Wed, 9 Aug 2023 11:36:05 +0200 Subject: [PATCH 07/10] Document more clearly that the insert order is an implementation detail (#10883) --- docs/en/reference/working-with-objects.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/en/reference/working-with-objects.rst b/docs/en/reference/working-with-objects.rst index 32d53212e6..f10c0ab091 100644 --- a/docs/en/reference/working-with-objects.rst +++ b/docs/en/reference/working-with-objects.rst @@ -192,6 +192,11 @@ be properly synchronized with the database when database in the most efficient way and a single, short transaction, taking care of maintaining referential integrity. +.. note:: + + Do not make any assumptions in your code about the number of queries + it takes to flush changes, about the ordering of ``INSERT``, ``UPDATE`` + and ``DELETE`` queries or the order in which entities will be processed. Example: From 6de4b68705d7b3064440ab55acf54f35899febf3 Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Wed, 9 Aug 2023 11:38:35 +0200 Subject: [PATCH 08/10] Use a dedicated exception for the check added in #10785 (#10881) This adds a dedicated exception for the case that objects with colliding identities are to be put into the identity map. Implements #10872. --- .../EntityIdentityCollisionException.php | 42 +++++++++++++++++ lib/Doctrine/ORM/UnitOfWork.php | 46 ++++++------------- .../ORM/Functional/BasicFunctionalTest.php | 4 +- tests/Doctrine/Tests/ORM/UnitOfWorkTest.php | 4 +- 4 files changed, 59 insertions(+), 37 deletions(-) create mode 100644 lib/Doctrine/ORM/Exception/EntityIdentityCollisionException.php diff --git a/lib/Doctrine/ORM/Exception/EntityIdentityCollisionException.php b/lib/Doctrine/ORM/Exception/EntityIdentityCollisionException.php new file mode 100644 index 0000000000..a70108c6f5 --- /dev/null +++ b/lib/Doctrine/ORM/Exception/EntityIdentityCollisionException.php @@ -0,0 +1,42 @@ +identityMap[$className][$idHash])) { if ($this->identityMap[$className][$idHash] !== $entity) { if ($this->em->getConfiguration()->isRejectIdCollisionInIdentityMapEnabled()) { - throw new RuntimeException( - sprintf( - <<<'EXCEPTION' -While adding an entity of class %s with an ID hash of "%s" to the identity map, -another object of class %s was already present for the same ID. This exception -is a safeguard against an internal inconsistency - IDs should uniquely map to -entity object instances. This problem may occur if: - -- you use application-provided IDs and reuse ID values; -- database-provided IDs are reassigned after truncating the database without - clearing the EntityManager; -- you might have been using EntityManager#getReference() to create a reference - for a nonexistent ID that was subsequently (by the RDBMS) assigned to another - entity. + throw EntityIdentityCollisionException::create($this->identityMap[$className][$idHash], $entity, $idHash); + } -Otherwise, it might be an ORM-internal inconsistency, please report it. -EXCEPTION - , - get_class($entity), - $idHash, - get_class($this->identityMap[$className][$idHash]) - ) - ); - } else { - Deprecation::trigger( - 'doctrine/orm', - 'https://github.com/doctrine/orm/pull/10785', - <<<'EXCEPTION' + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/10785', + <<<'EXCEPTION' While adding an entity of class %s with an ID hash of "%s" to the identity map, another object of class %s was already present for the same ID. This will trigger an exception in ORM 3.0. @@ -1683,12 +1664,11 @@ public function addToIdentityMap($entity) \Doctrine\ORM\Configuration::setRejectIdCollisionInIdentityMap on the entity manager's configuration. EXCEPTION - , - get_class($entity), - $idHash, - get_class($this->identityMap[$className][$idHash]) - ); - } + , + get_class($entity), + $idHash, + get_class($this->identityMap[$className][$idHash]) + ); } return false; diff --git a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php index 2be3feba4e..cfd60ab8cd 100644 --- a/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/BasicFunctionalTest.php @@ -5,6 +5,7 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\ORM\EntityNotFoundException; +use Doctrine\ORM\Exception\EntityIdentityCollisionException; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\ORMInvalidArgumentException; use Doctrine\ORM\PersistentCollection; @@ -20,7 +21,6 @@ use Doctrine\Tests\Models\CMS\CmsUser; use Doctrine\Tests\OrmFunctionalTestCase; use InvalidArgumentException; -use RuntimeException; use function get_class; @@ -1347,7 +1347,7 @@ public function testItThrowsWhenReferenceUsesIdAssignedByDatabase(): void // Now the database will assign an ID to the $user2 entity, but that place // in the identity map is already taken by user error. - $this->expectException(RuntimeException::class); + $this->expectException(EntityIdentityCollisionException::class); $this->expectExceptionMessageMatches('/another object .* was already present for the same ID/'); // depending on ID generation strategy, the ID may be asssigned already here diff --git a/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php b/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php index 31fa5ea8f6..0569bfd06d 100644 --- a/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php +++ b/tests/Doctrine/Tests/ORM/UnitOfWorkTest.php @@ -13,6 +13,7 @@ use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\EntityNotFoundException; use Doctrine\ORM\Events; +use Doctrine\ORM\Exception\EntityIdentityCollisionException; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; @@ -41,7 +42,6 @@ use Doctrine\Tests\OrmTestCase; use Doctrine\Tests\PHPUnitCompatibility\MockBuilderCompatibilityTools; use PHPUnit\Framework\MockObject\MockObject; -use RuntimeException; use stdClass; use function assert; @@ -960,7 +960,7 @@ public function testItThrowsWhenApplicationProvidedIdsCollide(): void $phone2 = new CmsPhonenumber(); $phone2->phonenumber = '1234'; - $this->expectException(RuntimeException::class); + $this->expectException(EntityIdentityCollisionException::class); $this->expectExceptionMessageMatches('/another object .* was already present for the same ID/'); $this->_unitOfWork->persist($phone2); From 6b220e3c90c4cffca5ec0feff8990f569aa9b120 Mon Sep 17 00:00:00 2001 From: Bei Xiao Date: Wed, 9 Aug 2023 12:42:00 +0300 Subject: [PATCH 09/10] Fix return type of `getSingleScalarResult` (#10870) --- lib/Doctrine/ORM/AbstractQuery.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/Doctrine/ORM/AbstractQuery.php b/lib/Doctrine/ORM/AbstractQuery.php index 1ec2d5904e..f4ba885a28 100644 --- a/lib/Doctrine/ORM/AbstractQuery.php +++ b/lib/Doctrine/ORM/AbstractQuery.php @@ -1010,9 +1010,8 @@ public function getSingleResult($hydrationMode = null) * * Alias for getSingleResult(HYDRATE_SINGLE_SCALAR). * - * @return bool|float|int|string The scalar result. + * @return bool|float|int|string|null The scalar result. * - * @throws NoResultException If the query returned no result. * @throws NonUniqueResultException If the query result is not unique. */ public function getSingleScalarResult() From 597a63a86ca8c5f9d1ec2dc74fe3d1269d43434a Mon Sep 17 00:00:00 2001 From: "Alexander M. Turek" Date: Wed, 9 Aug 2023 15:05:08 +0200 Subject: [PATCH 10/10] PHPStan 1.10.28, Psalm 5.14.1 (#10895) --- composer.json | 4 ++-- lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php | 5 ++++- lib/Doctrine/ORM/Mapping/UnderscoreNamingStrategy.php | 5 ++++- lib/Doctrine/ORM/Tools/EntityGenerator.php | 4 ++-- .../ORM/Tools/Pagination/LimitSubqueryOutputWalker.php | 2 +- psalm-baseline.xml | 5 +---- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index a897bbbf8f..14646162ad 100644 --- a/composer.json +++ b/composer.json @@ -42,14 +42,14 @@ "doctrine/annotations": "^1.13 || ^2", "doctrine/coding-standard": "^9.0.2 || ^12.0", "phpbench/phpbench": "^0.16.10 || ^1.0", - "phpstan/phpstan": "~1.4.10 || 1.10.25", + "phpstan/phpstan": "~1.4.10 || 1.10.28", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.7.2", "symfony/cache": "^4.4 || ^5.4 || ^6.0", "symfony/var-exporter": "^4.4 || ^5.4 || ^6.2", "symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0", - "vimeo/psalm": "4.30.0 || 5.13.1" + "vimeo/psalm": "4.30.0 || 5.14.1" }, "conflict": { "doctrine/annotations": "<1.13 || >= 3.0" diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index abd94fd01e..827ef873b5 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -944,7 +944,10 @@ private function cacheToArray(SimpleXMLElement $cacheMapping): array private function getCascadeMappings(SimpleXMLElement $cascadeElement): array { $cascades = []; - foreach ($cascadeElement->children() as $action) { + $children = $cascadeElement->children(); + assert($children !== null); + + foreach ($children as $action) { // According to the JPA specifications, XML uses "cascade-persist" // instead of "persist". Here, both variations // are supported because YAML, Annotation and Attribute use "persist" diff --git a/lib/Doctrine/ORM/Mapping/UnderscoreNamingStrategy.php b/lib/Doctrine/ORM/Mapping/UnderscoreNamingStrategy.php index c5b7ecc2c6..e1c7b13fcf 100644 --- a/lib/Doctrine/ORM/Mapping/UnderscoreNamingStrategy.php +++ b/lib/Doctrine/ORM/Mapping/UnderscoreNamingStrategy.php @@ -30,7 +30,10 @@ class UnderscoreNamingStrategy implements NamingStrategy /** @var int */ private $case; - /** @var string */ + /** + * @var string + * @psalm-var non-empty-string + */ private $pattern; /** diff --git a/lib/Doctrine/ORM/Tools/EntityGenerator.php b/lib/Doctrine/ORM/Tools/EntityGenerator.php index 5bf086d52b..04be655188 100644 --- a/lib/Doctrine/ORM/Tools/EntityGenerator.php +++ b/lib/Doctrine/ORM/Tools/EntityGenerator.php @@ -1376,7 +1376,7 @@ protected function generateEntityStubMethod(ClassMetadataInfo $metadata, $type, $this->staticReflection[$metadata->name]['methods'][] = strtolower($methodName); $var = sprintf('%sMethodTemplate', $type); - $template = static::$$var; + $template = (string) static::$$var; $methodTypeHint = ''; $types = Type::getTypesMap(); @@ -1695,7 +1695,7 @@ protected function generateFieldMappingPropertyDocBlock(array $fieldMapping, Cla } if (isset($fieldMapping['options']['comment']) && $fieldMapping['options']['comment']) { - $options[] = '"comment"="' . str_replace('"', '""', $fieldMapping['options']['comment']) . '"'; + $options[] = '"comment"="' . str_replace('"', '""', (string) $fieldMapping['options']['comment']) . '"'; } if (isset($fieldMapping['options']['collation']) && $fieldMapping['options']['collation']) { diff --git a/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php index 02fae2564c..6bcd178d03 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php +++ b/lib/Doctrine/ORM/Tools/Pagination/LimitSubqueryOutputWalker.php @@ -404,7 +404,7 @@ private function recreateInnerSql( /** * @return string[][] - * @psalm-return array{0: list, 1: list} + * @psalm-return array{0: list, 1: list} */ private function generateSqlAliasReplacements(): array { diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 2b887162e5..fd8707f080 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + IterableResult @@ -964,9 +964,6 @@ children()]]> children()]]> - - children()]]> - getName() === 'embeddable']]> getName() === 'entity']]>