From a3ea95316a4ef3c25c2ba0c5033858de7e0706cd Mon Sep 17 00:00:00 2001 From: Matthias Pigulla Date: Tue, 31 Jan 2023 10:09:26 +0000 Subject: [PATCH] Removing an entity that is contained in a m2m association fails when `flush()` is called twice Here is a failing test for #10483. When an entity is removed that is part of a many-to-many collection, that collection will be updated during the `flush()` operation to no longer contain the entity. However, afterwards the collection will be in a dirty state and the internal snapshot still contains the removed entity. That causes the collection to be processed again when `flush()` is called another time. This time, the ORM assumes the entity has been removed from the collection. It will try to build a `DELETE` operation but fails since the entity (from the snapshot) is no longer in the identity map. --- .../ORM/Functional/Ticket/GH10483Test.php | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 tests/Doctrine/Tests/ORM/Functional/Ticket/GH10483Test.php diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10483Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10483Test.php new file mode 100644 index 00000000000..f74d87f0bb2 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH10483Test.php @@ -0,0 +1,114 @@ +createSchemaForModels( + GH10483Role::class, + GH10483User::class + ); + } + + public function testFlushChangedUserAfterRoleHasBeenDeleted(): void + { + $em = $this->_em; + + $role = new GH10483Role(); + $role->name = 'test'; + $em->persist($role); + + $user = new GH10483User(); + $user->name = 'test'; + $user->roles->add($role); + $em->persist($user); + + $em->flush(); + + self::assertFalse($user->roles->isDirty()); + + $em->remove($role); + $em->flush(); + + // UnitOfWork::computeAssociationChanges(), lines 968 ff. will remove the removed entity from the collection: + self::assertEmpty($user->roles); + + // The UoW left the collection in a dirty state, is that correct? + self::assertTrue($user->roles->isDirty()); // <-- might need to assert "false" (?) + + // The collection's snapshot still contains the removed $role entity, is that correct? + self::assertSame([$role], $user->roles->getSnapshot()); // <-- might need to assert snapshot being empty (?) + + // Since the collection is dirty and/or has a snapshot that differs from the state, + // this flush will try to remove the $role from the collection, and fails when looking for + // it in the identity map + $em->flush(); + } +} + +/** + * @ORM\Entity + */ +class GH10483Role +{ + /** + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + * @ORM\Column(type="integer") + * + * @var int + */ + public $id; + + /** + * @ORM\Column + * + * @var string + */ + public $name; +} + +/** + * @ORM\Entity + */ +class GH10483User +{ + /** + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + * @ORM\Column(type="integer") + * + * @var int + */ + public $id; + + /** + * @ORM\Column + * + * @var string + */ + public $name; + + /** + * @ORM\ManyToMany(targetEntity="GH10483Role") + * + * @var Collection + */ + public $roles; + + public function __construct() + { + $this->roles = new ArrayCollection(); + } +}