diff --git a/CHANGELOG.md b/CHANGELOG.md index e6fd39c05c..f095823696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,14 @@ a release. --- ## [Unreleased] +### Added +- SoftDeleteable: Add option to enable or disable handling of the `postFlush` event (#2958) + +### Changed +- SoftDeleteable: Handling of the `postFlush` event is now disabled by default (#2958) + +### Fixed +- SoftDeleteable: Prevent cascade persist from re-inserting soft-deleted entities still referenced in the identity map (#2958) ## [3.20.0] - 2025-04-04 ### Fixed diff --git a/src/SoftDeleteable/SoftDeleteableListener.php b/src/SoftDeleteable/SoftDeleteableListener.php index 133aeb1927..aef9d734fe 100644 --- a/src/SoftDeleteable/SoftDeleteableListener.php +++ b/src/SoftDeleteable/SoftDeleteableListener.php @@ -51,6 +51,11 @@ class SoftDeleteableListener extends MappedEventSubscriber */ public const POST_SOFT_DELETE = 'postSoftDelete'; + /** + * Whether the postFlush event should be handled. + */ + private bool $handlePostFlushEvent; + /** * Objects soft-deleted on flush. * @@ -58,6 +63,13 @@ class SoftDeleteableListener extends MappedEventSubscriber */ private array $softDeletedObjects = []; + public function __construct(bool $handlePostFlushEvent = false) + { + parent::__construct(); + + $this->handlePostFlushEvent = $handlePostFlushEvent; + } + /** * @return string[] */ @@ -138,7 +150,9 @@ public function onFlush(EventArgs $args) ); } - $this->softDeletedObjects[] = $object; + if ($this->handlePostFlushEvent) { + $this->softDeletedObjects[] = $object; + } } } } @@ -150,6 +164,10 @@ public function onFlush(EventArgs $args) */ public function postFlush(EventArgs $args) { + if (!$this->handlePostFlushEvent) { + return; + } + $ea = $this->getEventAdapter($args); $om = $ea->getObjectManager(); foreach ($this->softDeletedObjects as $index => $object) { @@ -172,6 +190,16 @@ public function loadClassMetadata(EventArgs $eventArgs) $this->loadMetadataForObjectClass($eventArgs->getObjectManager(), $eventArgs->getClassMetadata()); } + public function setHandlePostFlushEvent(bool $handlePostFlushEvent): void + { + $this->handlePostFlushEvent = $handlePostFlushEvent; + } + + public function shouldHandlePostFlushEvent(): bool + { + return $this->handlePostFlushEvent; + } + protected function getNamespace() { return __NAMESPACE__; diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php index bb467ef452..928bd38757 100644 --- a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Article.php @@ -58,6 +58,12 @@ class Article #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article', cascade: ['persist', 'remove'])] private $comments; + /** + * @ORM\ManyToOne(targetEntity="Author", cascade={"persist"}, inversedBy="articles") + */ + #[ORM\ManyToOne(targetEntity: Author::class, cascade: ['persist'], inversedBy: 'articles')] + private ?Author $author = null; + public function __construct() { $this->comments = new ArrayCollection(); @@ -100,4 +106,14 @@ public function getComments(): Collection { return $this->comments; } + + public function setAuthor(?Author $author): void + { + $this->author = $author; + } + + public function getAuthor(): ?Author + { + return $this->author; + } } diff --git a/tests/Gedmo/SoftDeleteable/Fixture/Entity/Author.php b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Author.php new file mode 100644 index 0000000000..935fc60765 --- /dev/null +++ b/tests/Gedmo/SoftDeleteable/Fixture/Entity/Author.php @@ -0,0 +1,106 @@ + http://www.gediminasm.org + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gedmo\Tests\SoftDeleteable\Fixture\Entity; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Mapping as ORM; +use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity; + +/** + * @ORM\Entity + * + * @Gedmo\SoftDeleteable(fieldName="deletedAt") + */ +#[ORM\Entity] +#[Gedmo\SoftDeleteable(fieldName: 'deletedAt')] +class Author +{ + use SoftDeleteableEntity; + + /** + * @var int|null + * + * @ORM\Id + * @ORM\GeneratedValue(strategy="IDENTITY") + * @ORM\Column(type="integer") + */ + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'IDENTITY')] + #[ORM\Column(type: Types::INTEGER)] + private $id; + + /** + * @ORM\Column(type="string") + */ + #[ORM\Column(type: Types::STRING)] + private ?string $firstname = null; + + /** + * @ORM\Column(type="string") + */ + #[ORM\Column(type: Types::STRING)] + private ?string $lastname = null; + + /** + * @var Collection + * + * @ORM\OneToMany(targetEntity="Article", mappedBy="author", cascade={"persist"}) + */ + #[ORM\OneToMany(targetEntity: Article::class, mappedBy: 'author', cascade: ['persist'])] + private Collection $articles; + + public function __construct() + { + $this->articles = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function setFirstname(?string $firstname): void + { + $this->firstname = $firstname; + } + + public function getFirstname(): ?string + { + return $this->firstname; + } + + public function setLastname(?string $lastname): void + { + $this->lastname = $lastname; + } + + public function getLastname(): ?string + { + return $this->lastname; + } + + public function addArticle(Article $article): void + { + $this->articles[] = $article; + } + + /** + * @return Collection + */ + public function getArticles(): Collection + { + return $this->articles; + } +} diff --git a/tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php b/tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php index d377e68e56..129351b3b9 100644 --- a/tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php +++ b/tests/Gedmo/SoftDeleteable/SoftDeleteableEntityTest.php @@ -19,6 +19,7 @@ use Gedmo\SoftDeleteable\SoftDeleteableListener; use Gedmo\Tests\Clock; use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Article; +use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Author; use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Child; use Gedmo\Tests\SoftDeleteable\Fixture\Entity\Comment; use Gedmo\Tests\SoftDeleteable\Fixture\Entity\MegaPage; @@ -522,8 +523,10 @@ public function testShouldFilterBeQueryCachedCorrectlyWhenToggledForEntity(): vo static::assertCount(0, $data); } - public function testSoftDeletedObjectIsRemovedPostFlush(): void + public function testSoftDeletedObjectIsRemovedPostFlushWhenEnabled(): void { + $this->softDeleteableListener->setHandlePostFlushEvent(true); + $repo = $this->em->getRepository(Article::class); $commentRepo = $this->em->getRepository(Comment::class); @@ -558,6 +561,44 @@ public function testSoftDeletedObjectIsRemovedPostFlush(): void static::assertNull($commentRepo->find($comment->getId())); } + public function testSoftDeletedEntityIsNotReinsertedPostFlushWhenDisabled(): void + { + $authorRepo = $this->em->getRepository(Author::class); + + $author = new Author(); + $firstname = 'first_name'; + $author->setFirstname($firstname); + $lastname = 'last_name'; + $author->setLastname($lastname); + + $article = new Article(); + $title = 'Title 1'; + $article->setTitle($title); + $article->setAuthor($author); + + $this->em->persist($article); + $this->em->flush(); + + $this->em->clear(); + + $author = $authorRepo->findOneBy(['firstname' => $firstname]); + $article = $author->getArticles()[0]; + + static::assertSame($lastname, $author->getLastname()); + static::assertNull($author->getDeletedAt()); + static::assertSame($title, $article->getTitle()); + + $this->em->remove($author); + $this->em->flush(); + + // Flush again + $this->em->flush(); + + // Check whether the entity was re-inserted + $this->em->getFilters()->disable(self::SOFT_DELETEABLE_FILTER_NAME); + static::assertSame(1, $authorRepo->count(['firstname' => $firstname])); + } + public function testPostSoftDeleteEventIsDispatched(): void { $this->em->getEventManager()->addEventSubscriber(new WithPreAndPostSoftDeleteEventArgsTypeListener()); @@ -614,6 +655,7 @@ protected function getUsedEntityFixtures(): array { return [ Article::class, + Author::class, Page::class, MegaPage::class, Module::class,