From ae2be60a40adde9ac519ca2629a6806435628407 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Thu, 6 Mar 2025 09:11:07 -0500 Subject: [PATCH 01/14] Add support for blameable on remove --- doc/blameable.md | 40 +++++++++---- doc/sortable.md | 29 +++++++--- src/Blameable/Mapping/Driver/Attribute.php | 2 +- tests/Gedmo/Blameable/BlameableTest.php | 58 ++++++++++--------- .../Blameable/Fixture/Entity/Article.php | 25 +++++++- 5 files changed, 106 insertions(+), 48 deletions(-) diff --git a/doc/blameable.md b/doc/blameable.md index cfd2678cba..00591b5405 100644 --- a/doc/blameable.md +++ b/doc/blameable.md @@ -4,10 +4,19 @@ The **Blameable** behavior automates the update of user information on your Doct ## Index -- [Getting Started](#getting-started) -- [Configuring Blameable Objects](#configuring-blameable-objects) -- [Using Traits](#using-traits) -- [Logging Changes For Specific Actions](#logging-changes-for-specific-actions) +- [Blameable Behavior Extension for Doctrine](#blameable-behavior-extension-for-doctrine) + - [Index](#index) + - [Getting Started](#getting-started) + - [Configuring Blameable Objects](#configuring-blameable-objects) + - [Attribute Configuration](#attribute-configuration) + - [XML Configuration](#xml-configuration) + - [Annotation Configuration](#annotation-configuration) + - [Supported Field Types](#supported-field-types) + - [Supported Events](#supported-events) + - [Using Traits](#using-traits) + - [Logging Changes For Specific Actions](#logging-changes-for-specific-actions) + - [Single Field Changed To Specific Value](#single-field-changed-to-specific-value) + - [One of Many Fields Changed](#one-of-many-fields-changed) ## Getting Started @@ -136,6 +145,15 @@ The blameable extension supports the following field types for a blameable field [`symfony/doctrine-bridge`](https://github.com/symfony/doctrine-bridge)) - A many-to-one association (ORM) or reference many reference (MongoDB ODM) +### Supported Events + +The blameable extension supports the following events: + +- `#[Blameable(on: 'create')]` - The blameable field is updated when the object is created +- `#[Blameable(on: 'update')]` - The blameable field is updated when the object is updated +- `#[Blameable(on: 'change')]` - The blameable field is updated when a specific field or value is changed +- `#[Blameable(on: 'remove')]` - The blameable field is updated when the object is removed/deleted (works in conjunction with soft-delete) + ## Using Traits The blameable extension provides traits which can be used to quickly add fields, and optionally the mapping configuration, @@ -150,7 +168,7 @@ provided as a convenience for a common configuration, for other use cases it is ## Logging Changes For Specific Actions -In addition to supporting logging the user for general create and update actions, the extension can also be configured to +In addition to supporting logging the user for general create, update, and remove actions, the extension can also be configured to log the user who made a change for specific fields or values. ### Single Field Changed To Specific Value @@ -178,14 +196,14 @@ class Article public bool $published = false; /** - * Field to track the user who last made any change to this article. + * Field to track the user who last made any change to this article. */ #[ORM\Column(type: Types::STRING)] #[Gedmo\Blameable] public ?string $updatedBy = null; /** - * Field to track the user who published this article. + * Field to track the user who published this article. */ #[ORM\Column(type: Types::STRING, nullable: true)] #[Gedmo\Blameable(on: 'change', field: 'published', value: true)] @@ -216,14 +234,14 @@ class Article public ?Category $category = null; /** - * Field to track the user who last made any change to this article. + * Field to track the user who last made any change to this article. */ #[ORM\Column(type: Types::STRING)] #[Gedmo\Blameable] public ?string $updatedBy = null; /** - * Field to track the user who archived this article. + * Field to track the user who archived this article. */ #[ORM\Column(type: Types::STRING, nullable: true)] #[Gedmo\Blameable(on: 'change', field: 'category.archived', value: true)] @@ -262,14 +280,14 @@ class Article public ?string $metaKeywords = null; /** - * Field to track the user who last made any change to this article. + * Field to track the user who last made any change to this article. */ #[ORM\Column(type: Types::STRING)] #[Gedmo\Blameable] public ?string $updatedBy = null; /** - * Field to track the user who last modified this article's SEO metadata. + * Field to track the user who last modified this article's SEO metadata. */ #[ORM\Column(type: Types::STRING, nullable: true)] #[Gedmo\Blameable(on: 'change', field: ['metaDescription', 'metaKeywords', 'category.metaDescription', 'category.metaKeywords'])] diff --git a/doc/sortable.md b/doc/sortable.md index a0dfa6f4eb..19eb36a7f6 100644 --- a/doc/sortable.md +++ b/doc/sortable.md @@ -9,13 +9,19 @@ Features: - Annotation, Attribute and Xml mapping support for extensions Contents: -- [Setup and autoloading](#setup-and-autoloading) -- [Sortable mapping](#sortable-mapping) - - [Annotations](#annotation-mapping-example) - - [Attributes](#attribute-mapping-example) - - [Xml](#xml-mapping-example) -- [Basic usage examples](#basic-usage-examples) -- [Custom comparison method](#custom-comparison) +- [Sortable behavior extension for Doctrine](#sortable-behavior-extension-for-doctrine) + - [Setup and autoloading](#setup-and-autoloading) + - [Sortable mapping](#sortable-mapping) + - [Annotation mapping example](#annotation-mapping-example) + - [Attribute mapping example](#attribute-mapping-example) + - [Xml mapping example](#xml-mapping-example) + - [Basic usage examples](#basic-usage-examples) + - [To save **Items** at the end of the sorting list simply do:](#to-save-items-at-the-end-of-the-sorting-list-simply-do) + - [Save **Item** at a given position](#save-item-at-a-given-position) + - [Reordering the sorted list](#reordering-the-sorted-list) + - [Using a foreign\_key / relation as SortableGroup](#using-a-foreign_key--relation-as-sortablegroup) + - [Custom comparison](#custom-comparison) + - [Blameable Support](#blameable-support) ## Setup and autoloading Read the [documentation](./annotations.md#em-setup) or check the [example code](../example) @@ -340,3 +346,12 @@ class Item implements Comparable } } ``` + +## Blameable Support + +You can also use the [Blameable](./blameable.md) extension to automatically set a `deletedBy` field when an entity is removed/deleted. + +```php +#[Gedmo\Blameable(on: 'remove')] +private string $deletedBy; +``` diff --git a/src/Blameable/Mapping/Driver/Attribute.php b/src/Blameable/Mapping/Driver/Attribute.php index de2ce4158c..84330efbc2 100644 --- a/src/Blameable/Mapping/Driver/Attribute.php +++ b/src/Blameable/Mapping/Driver/Attribute.php @@ -74,7 +74,7 @@ public function readExtendedMetadata($meta, array &$config) } } - if (!in_array($blameable->on, ['update', 'create', 'change'], true)) { + if (!in_array($blameable->on, ['update', 'create', 'change', 'remove'], true)) { throw new InvalidMappingException("Field - [{$field}] trigger 'on' is not one of [update, create, change] in class - {$meta->getName()}"); } diff --git a/tests/Gedmo/Blameable/BlameableTest.php b/tests/Gedmo/Blameable/BlameableTest.php index b233150a98..411f8355f4 100644 --- a/tests/Gedmo/Blameable/BlameableTest.php +++ b/tests/Gedmo/Blameable/BlameableTest.php @@ -43,44 +43,50 @@ protected function setUp(): void public function testBlameable(): void { - $sport = new Article(); - $sport->setTitle('Sport'); + $article = new Article(); + $article->setTitle('Sport'); - $sportComment = new Comment(); - $sportComment->setMessage('hello'); - $sportComment->setArticle($sport); - $sportComment->setStatus(0); + $comment = new Comment(); + $comment->setMessage('hello'); + $comment->setArticle($article); + $comment->setStatus(0); - $this->em->persist($sport); - $this->em->persist($sportComment); + $this->em->persist($article); + $this->em->persist($comment); $this->em->flush(); $this->em->clear(); - $sport = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); - static::assertSame('testuser', $sport->getCreated()); - static::assertSame('testuser', $sport->getUpdated()); - static::assertNull($sport->getPublished()); + $article = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); + $this->assertSame('testuser', $article->getCreated()); + $this->assertSame('testuser', $article->getUpdated()); + $this->assertNull($article->getPublished()); - $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); - static::assertSame('testuser', $sportComment->getModified()); - static::assertNull($sportComment->getClosed()); + $comment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + $this->assertSame('testuser', $comment->getModified()); + $this->assertNull($comment->getClosed()); - $sportComment->setStatus(1); - $published = new Type(); - $published->setTitle('Published'); + $comment->setStatus(1); + $type = new Type(); + $type->setTitle('Published'); - $sport->setTitle('Updated'); - $sport->setType($published); - $this->em->persist($sport); - $this->em->persist($published); - $this->em->persist($sportComment); + $article->setTitle('Updated'); + $article->setType($type); + $this->em->persist($article); + $this->em->persist($type); + $this->em->persist($comment); $this->em->flush(); $this->em->clear(); - $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); - static::assertSame('testuser', $sportComment->getClosed()); + $comment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + $this->assertSame('testuser', $comment->getClosed()); + $this->assertSame('testuser', $article->getPublished()); + + // Now delete event + $article = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Updated']); + $this->em->remove($article); + $this->em->flush(); - static::assertSame('testuser', $sport->getPublished()); + $this->assertSame('testuser', $article->getDeleted()); } public function testBlameableWithActorProvider(): void diff --git a/tests/Gedmo/Blameable/Fixture/Entity/Article.php b/tests/Gedmo/Blameable/Fixture/Entity/Article.php index f780508a6c..ea32a668b9 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/Article.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/Article.php @@ -51,9 +51,9 @@ class Article implements Blameable private $comments; /** - * @Gedmo\Blameable(on="create") - * * @ORM\Column(name="created", type="string") + * + * @Gedmo\Blameable(on="create") */ #[ORM\Column(name: 'created', type: Types::STRING)] #[Gedmo\Blameable(on: 'create')] @@ -62,12 +62,21 @@ class Article implements Blameable /** * @ORM\Column(name="updated", type="string") * - * @Gedmo\Blameable + * @Gedmo\Blameable(on="update") */ #[Gedmo\Blameable] #[ORM\Column(name: 'updated', type: Types::STRING)] private ?string $updated = null; + /** + * @ORM\Column(name="deleted", type="string") + * + * @Gedmo\Blameable(on="remove") + */ + #[Gedmo\Blameable] + #[ORM\Column(name: 'deleted', type: Types::STRING)] + private ?string $deleted = null; + /** * @ORM\Column(name="published", type="string", nullable=true) * @@ -151,4 +160,14 @@ public function setUpdated(?string $updated): void { $this->updated = $updated; } + + public function getDeleted(): ?string + { + return $this->deleted; + } + + public function setDeleted(?string $deleted): void + { + $this->deleted = $deleted; + } } From e7f6499fbbfbf687e3698c25ef47313ff382b2d9 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Thu, 6 Mar 2025 09:55:19 -0500 Subject: [PATCH 02/14] Added remove tracking for soft-delete --- src/AbstractTrackingListener.php | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/AbstractTrackingListener.php b/src/AbstractTrackingListener.php index a818601ffc..9aafe83cb6 100644 --- a/src/AbstractTrackingListener.php +++ b/src/AbstractTrackingListener.php @@ -48,6 +48,7 @@ public function getSubscribedEvents() 'prePersist', 'onFlush', 'loadClassMetadata', + 'preRemove', ]; } @@ -204,6 +205,35 @@ public function prePersist(EventArgs $args) } } + /** + * Checks for a soft delete event as remove + */ + public function preRemove(EventArgs $args): void + { + $ea = $this->getEventAdapter($args); + $om = $ea->getObjectManager(); + $uow = $om->getUnitOfWork(); + $object = $ea->getObject(); + $meta = $om->getClassMetadata($object::class); + $config = $this->getConfiguration($om, $meta->getName()); + + if ($config) { + if (isset($config['remove'])) { + foreach ($config['remove'] as $field) { + if ($meta->getReflectionProperty($field)->getValue($object) === null) { // let manual values + $oldValue = $meta->getReflectionProperty($field)->getValue($object); + $newValue = $this->getFieldValue($meta, $field, $ea); + $this->updateField($object, $ea, $meta, $field); + $uow->propertyChanged($object, $field, $oldValue, $newValue); + $uow->scheduleExtraUpdate($object, [ + $field => [$oldValue, $newValue], + ]); + } + } + } + } + } + /** * Get the value for an updated field. * From 8a1630fdaf8083b746b337962abc6671904b9fdc Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Thu, 6 Mar 2025 09:57:01 -0500 Subject: [PATCH 03/14] Reverted changes in BlameableTest and created new test class for blameable with soft-delete --- tests/Gedmo/Blameable/BlameableTest.php | 58 ++++----- .../Blameable/BlameableWithSoftDeleteTest.php | 114 ++++++++++++++++++ .../Blameable/Fixture/Entity/Article.php | 5 + 3 files changed, 145 insertions(+), 32 deletions(-) create mode 100644 tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php diff --git a/tests/Gedmo/Blameable/BlameableTest.php b/tests/Gedmo/Blameable/BlameableTest.php index 411f8355f4..b233150a98 100644 --- a/tests/Gedmo/Blameable/BlameableTest.php +++ b/tests/Gedmo/Blameable/BlameableTest.php @@ -43,50 +43,44 @@ protected function setUp(): void public function testBlameable(): void { - $article = new Article(); - $article->setTitle('Sport'); + $sport = new Article(); + $sport->setTitle('Sport'); - $comment = new Comment(); - $comment->setMessage('hello'); - $comment->setArticle($article); - $comment->setStatus(0); + $sportComment = new Comment(); + $sportComment->setMessage('hello'); + $sportComment->setArticle($sport); + $sportComment->setStatus(0); - $this->em->persist($article); - $this->em->persist($comment); + $this->em->persist($sport); + $this->em->persist($sportComment); $this->em->flush(); $this->em->clear(); - $article = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); - $this->assertSame('testuser', $article->getCreated()); - $this->assertSame('testuser', $article->getUpdated()); - $this->assertNull($article->getPublished()); + $sport = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); + static::assertSame('testuser', $sport->getCreated()); + static::assertSame('testuser', $sport->getUpdated()); + static::assertNull($sport->getPublished()); - $comment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); - $this->assertSame('testuser', $comment->getModified()); - $this->assertNull($comment->getClosed()); + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertSame('testuser', $sportComment->getModified()); + static::assertNull($sportComment->getClosed()); - $comment->setStatus(1); - $type = new Type(); - $type->setTitle('Published'); + $sportComment->setStatus(1); + $published = new Type(); + $published->setTitle('Published'); - $article->setTitle('Updated'); - $article->setType($type); - $this->em->persist($article); - $this->em->persist($type); - $this->em->persist($comment); + $sport->setTitle('Updated'); + $sport->setType($published); + $this->em->persist($sport); + $this->em->persist($published); + $this->em->persist($sportComment); $this->em->flush(); $this->em->clear(); - $comment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); - $this->assertSame('testuser', $comment->getClosed()); - $this->assertSame('testuser', $article->getPublished()); - - // Now delete event - $article = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Updated']); - $this->em->remove($article); - $this->em->flush(); + $sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + static::assertSame('testuser', $sportComment->getClosed()); - $this->assertSame('testuser', $article->getDeleted()); + static::assertSame('testuser', $sport->getPublished()); } public function testBlameableWithActorProvider(): void diff --git a/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php b/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php new file mode 100644 index 0000000000..89467bb8ae --- /dev/null +++ b/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php @@ -0,0 +1,114 @@ + 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\Blameable; + +use Doctrine\Common\EventManager; +use Gedmo\Blameable\BlameableListener; +use Gedmo\SoftDeleteable\Filter\SoftDeleteableFilter; +use Gedmo\SoftDeleteable\SoftDeleteableListener; +use Gedmo\Tests\Blameable\Fixture\Entity\Article; +use Gedmo\Tests\Blameable\Fixture\Entity\Comment; +use Gedmo\Tests\Blameable\Fixture\Entity\Type; +use Gedmo\Tests\Clock; +use Gedmo\Tests\TestActorProvider; +use Gedmo\Tests\Tool\BaseTestCaseORM; + +/** + * These are tests for Blameable behavior + * + * @author Gediminas Morkevicius + */ +final class BlameableWithSoftDeleteTest extends BaseTestCaseORM +{ + private const SOFT_DELETEABLE_FILTER_NAME = 'soft-deleteable'; + + private BlameableListener $blameableListener; + private SoftDeleteableListener $softDeleteableListener; + + protected function setUp(): void + { + parent::setUp(); + + $this->blameableListener = new BlameableListener(); + $this->blameableListener->setUserValue('testuser'); + + $this->softDeleteableListener = new SoftDeleteableListener(); + $this->softDeleteableListener->setClock(new Clock()); + + $evm = new EventManager(); + $evm->addEventSubscriber($this->blameableListener); + $evm->addEventSubscriber($this->softDeleteableListener); + + $config = $this->getDefaultConfiguration(); + $config->addFilter(self::SOFT_DELETEABLE_FILTER_NAME, SoftDeleteableFilter::class); + $this->em = $this->getDefaultMockSqliteEntityManager($evm, $config); + $this->em->getFilters()->enable(self::SOFT_DELETEABLE_FILTER_NAME); + } + + public function testBlameable(): void + { + $article = new Article(); + $article->setTitle('Sport'); + + $comment = new Comment(); + $comment->setMessage('hello'); + $comment->setArticle($article); + $comment->setStatus(0); + + $this->em->persist($article); + $this->em->persist($comment); + $this->em->flush(); + $this->em->clear(); + + $article = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); + $this->assertSame('testuser', $article->getCreated()); + $this->assertSame('testuser', $article->getUpdated()); + $this->assertNull($article->getPublished()); + + $comment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + $this->assertSame('testuser', $comment->getModified()); + $this->assertNull($comment->getClosed()); + + $comment->setStatus(1); + $type = new Type(); + $type->setTitle('Published'); + + $article->setTitle('Updated'); + $article->setType($type); + $this->em->persist($article); + $this->em->persist($type); + $this->em->persist($comment); + $this->em->flush(); + $this->em->clear(); + + $comment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); + $this->assertSame('testuser', $comment->getClosed()); + $this->assertSame('testuser', $article->getPublished()); + + // Now delete event + $article = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Updated']); + $this->em->remove($article); + $this->em->flush(); + + $this->assertTrue($article->isDeleted()); + $this->assertSame('testuser', $article->getDeleted()); + } + + protected function getUsedEntityFixtures(): array + { + return [ + Article::class, + Comment::class, + Type::class, + ]; + } +} diff --git a/tests/Gedmo/Blameable/Fixture/Entity/Article.php b/tests/Gedmo/Blameable/Fixture/Entity/Article.php index ea32a668b9..2e769987a0 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/Article.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/Article.php @@ -17,13 +17,18 @@ use Doctrine\ORM\Mapping as ORM; use Gedmo\Blameable\Blameable; use Gedmo\Mapping\Annotation as Gedmo; +use Gedmo\SoftDeleteable\Traits\SoftDeleteableEntity; /** * @ORM\Entity */ #[ORM\Entity] +#[Gedmo\SoftDeleteable] class Article implements Blameable { + + use SoftDeleteableEntity; + /** * @var int|null * From 978929e3091cfbc1814b24cbe5ff6eb956ad764b Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Sun, 9 Mar 2025 16:19:56 -0400 Subject: [PATCH 04/14] CS fixes --- src/AbstractTrackingListener.php | 4 ++-- src/Mapping/MappedEventSubscriber.php | 3 --- .../Blameable/BlameableWithSoftDeleteTest.php | 18 +++++++++--------- .../Gedmo/Blameable/Fixture/Entity/Article.php | 1 - 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/AbstractTrackingListener.php b/src/AbstractTrackingListener.php index 9aafe83cb6..03a81e4cf0 100644 --- a/src/AbstractTrackingListener.php +++ b/src/AbstractTrackingListener.php @@ -214,13 +214,13 @@ public function preRemove(EventArgs $args): void $om = $ea->getObjectManager(); $uow = $om->getUnitOfWork(); $object = $ea->getObject(); - $meta = $om->getClassMetadata($object::class); + $meta = $om->getClassMetadata(get_class($object)); $config = $this->getConfiguration($om, $meta->getName()); if ($config) { if (isset($config['remove'])) { foreach ($config['remove'] as $field) { - if ($meta->getReflectionProperty($field)->getValue($object) === null) { // let manual values + if (null === $meta->getReflectionProperty($field)->getValue($object)) { // let manual values $oldValue = $meta->getReflectionProperty($field)->getValue($object); $newValue = $this->getFieldValue($meta, $field, $ea); $this->updateField($object, $ea, $meta, $field); diff --git a/src/Mapping/MappedEventSubscriber.php b/src/Mapping/MappedEventSubscriber.php index d5b87c5388..2dfc3e5dda 100644 --- a/src/Mapping/MappedEventSubscriber.php +++ b/src/Mapping/MappedEventSubscriber.php @@ -41,7 +41,6 @@ * extended drivers * * @phpstan-template TConfig of array - * @phpstan-template TEventAdapter of AdapterInterface * * @author Gediminas Morkevicius */ @@ -256,8 +255,6 @@ public function loadMetadataForObjectClass(ObjectManager $objectManager, $metada * @throws InvalidArgumentException if event is not recognized * * @return AdapterInterface - * - * @phpstan-return TEventAdapter */ protected function getEventAdapter(EventArgs $args) { diff --git a/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php b/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php index 89467bb8ae..ca11e32c4b 100644 --- a/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php +++ b/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php @@ -70,13 +70,13 @@ public function testBlameable(): void $this->em->clear(); $article = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']); - $this->assertSame('testuser', $article->getCreated()); - $this->assertSame('testuser', $article->getUpdated()); - $this->assertNull($article->getPublished()); + static::assertSame('testuser', $article->getCreated()); + static::assertSame('testuser', $article->getUpdated()); + static::assertNull($article->getPublished()); $comment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); - $this->assertSame('testuser', $comment->getModified()); - $this->assertNull($comment->getClosed()); + static::assertSame('testuser', $comment->getModified()); + static::assertNull($comment->getClosed()); $comment->setStatus(1); $type = new Type(); @@ -91,16 +91,16 @@ public function testBlameable(): void $this->em->clear(); $comment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']); - $this->assertSame('testuser', $comment->getClosed()); - $this->assertSame('testuser', $article->getPublished()); + static::assertSame('testuser', $comment->getClosed()); + static::assertSame('testuser', $article->getPublished()); // Now delete event $article = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Updated']); $this->em->remove($article); $this->em->flush(); - $this->assertTrue($article->isDeleted()); - $this->assertSame('testuser', $article->getDeleted()); + static::assertTrue($article->isDeleted()); + static::assertSame('testuser', $article->getDeleted()); } protected function getUsedEntityFixtures(): array diff --git a/tests/Gedmo/Blameable/Fixture/Entity/Article.php b/tests/Gedmo/Blameable/Fixture/Entity/Article.php index 2e769987a0..920aa6517c 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/Article.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/Article.php @@ -26,7 +26,6 @@ #[Gedmo\SoftDeleteable] class Article implements Blameable { - use SoftDeleteableEntity; /** From f0aea30a54249353c47ca8908f8da07e52f9a080 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Sun, 9 Mar 2025 16:30:18 -0400 Subject: [PATCH 05/14] Updated CHANGELOG --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43f2f02cf3..1c27b2d1a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ a release. ## [Unreleased] +## Added +- Support for blameable on `remove` event (#2929) + ## [3.19.0] - 2025-02-24 ### Added - Actor provider for use with extensions with user references (#2914) @@ -103,7 +106,7 @@ a release. - Dropped support for doctrine/dbal < 3.2 ### Deprecated -- Calling `Gedmo\Mapping\Event\Adapter\ORM::getObjectManager()` and `getObject()` on EventArgs that do not implement `getObjectManager()` and `getObject()` (such as old EventArgs implementing `getEntityManager()` and `getEntity()`) +- Calling `Gedmo\Mapping\Event\Adapter\ORM::getObjectManager()` and `getObject()` on EventArgs that do not implement `getObjectManager()` and `getObject()` (such as old EventArgs implementing `getEntityManager()` and `getEntity()`) - Calling `Gedmo\Uploadable\Event\UploadableBaseEventArgs::getEntityManager()` and `getEntity()`. Call `getObjectManager()` and `getObject()` instead. ## [3.13.0] - 2023-09-06 From 8cddb626bc3c82f298e9d6758976720f33d37a4a Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Sun, 9 Mar 2025 17:44:45 -0400 Subject: [PATCH 06/14] Simplified TOC --- doc/blameable.md | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/doc/blameable.md b/doc/blameable.md index 00591b5405..af78c338f8 100644 --- a/doc/blameable.md +++ b/doc/blameable.md @@ -4,19 +4,10 @@ The **Blameable** behavior automates the update of user information on your Doct ## Index -- [Blameable Behavior Extension for Doctrine](#blameable-behavior-extension-for-doctrine) - - [Index](#index) - - [Getting Started](#getting-started) - - [Configuring Blameable Objects](#configuring-blameable-objects) - - [Attribute Configuration](#attribute-configuration) - - [XML Configuration](#xml-configuration) - - [Annotation Configuration](#annotation-configuration) - - [Supported Field Types](#supported-field-types) - - [Supported Events](#supported-events) - - [Using Traits](#using-traits) - - [Logging Changes For Specific Actions](#logging-changes-for-specific-actions) - - [Single Field Changed To Specific Value](#single-field-changed-to-specific-value) - - [One of Many Fields Changed](#one-of-many-fields-changed) +- [Getting Started](#getting-started) +- [Configuring Blameable Objects](#configuring-blameable-objects) +- [Using Traits](#using-traits) +- [Logging Changes For Specific Actions](#logging-changes-for-specific-actions) ## Getting Started From f0d86135bc3c2b7f6d3a6af214f44727a0a12fc9 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Sun, 9 Mar 2025 17:46:51 -0400 Subject: [PATCH 07/14] Simplified TOC --- doc/sortable.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/doc/sortable.md b/doc/sortable.md index 19eb36a7f6..e2a2d65c36 100644 --- a/doc/sortable.md +++ b/doc/sortable.md @@ -3,31 +3,27 @@ **Sortable** behavior will maintain a position field for ordering entities. Features: + - Automatic handling of position index - Group entity ordering by one or more fields - Can be nested with other behaviors - Annotation, Attribute and Xml mapping support for extensions Contents: -- [Sortable behavior extension for Doctrine](#sortable-behavior-extension-for-doctrine) - - [Setup and autoloading](#setup-and-autoloading) - - [Sortable mapping](#sortable-mapping) - - [Annotation mapping example](#annotation-mapping-example) - - [Attribute mapping example](#attribute-mapping-example) - - [Xml mapping example](#xml-mapping-example) - - [Basic usage examples](#basic-usage-examples) - - [To save **Items** at the end of the sorting list simply do:](#to-save-items-at-the-end-of-the-sorting-list-simply-do) - - [Save **Item** at a given position](#save-item-at-a-given-position) - - [Reordering the sorted list](#reordering-the-sorted-list) - - [Using a foreign\_key / relation as SortableGroup](#using-a-foreign_key--relation-as-sortablegroup) - - [Custom comparison](#custom-comparison) - - [Blameable Support](#blameable-support) + +- [Setup and autoloading](#setup-and-autoloading) +- [Sortable mapping](#sortable-mapping) +- [Basic usage examples](#basic-usage-examples) +- [Custom comparison](#custom-comparison) +- [Blameable Support](#blameable-support) ## Setup and autoloading + Read the [documentation](./annotations.md#em-setup) or check the [example code](../example) on how to setup and use the extensions in most optimized way. ## Sortable mapping + - [SortableGroup](../src/Mapping/Annotation/SortableGroup.php) - used to specify property for **grouping** - [SortablePosition](../src/Mapping/Annotation/SortablePosition.php) - used to specify property to store **position** index From 2253cefe09ea17284b8b57ad770196f084cf843c Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Sun, 9 Mar 2025 17:53:02 -0400 Subject: [PATCH 08/14] Add the preRemove event listener for only blameable --- src/AbstractTrackingListener.php | 1 - src/Blameable/BlameableListener.php | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/AbstractTrackingListener.php b/src/AbstractTrackingListener.php index 03a81e4cf0..f281e89b73 100644 --- a/src/AbstractTrackingListener.php +++ b/src/AbstractTrackingListener.php @@ -48,7 +48,6 @@ public function getSubscribedEvents() 'prePersist', 'onFlush', 'loadClassMetadata', - 'preRemove', ]; } diff --git a/src/Blameable/BlameableListener.php b/src/Blameable/BlameableListener.php index a39e4012e3..a4b264da24 100644 --- a/src/Blameable/BlameableListener.php +++ b/src/Blameable/BlameableListener.php @@ -34,6 +34,14 @@ class BlameableListener extends AbstractTrackingListener */ protected $user; + /** + * {@inheritdoc} + */ + public function getSubscribedEvents(): array + { + return array_merge(parent::getSubscribedEvents(), ['preRemove']); + } + /** * Get the user value to set on a blameable field * From 1b594d0aac02b3fd0f653f4878e60af2139e17ff Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Sun, 16 Mar 2025 19:13:34 -0400 Subject: [PATCH 09/14] CS fixes --- tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php b/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php index ca11e32c4b..d052a4ced8 100644 --- a/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php +++ b/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php @@ -19,7 +19,6 @@ use Gedmo\Tests\Blameable\Fixture\Entity\Comment; use Gedmo\Tests\Blameable\Fixture\Entity\Type; use Gedmo\Tests\Clock; -use Gedmo\Tests\TestActorProvider; use Gedmo\Tests\Tool\BaseTestCaseORM; /** From 5614da8800b0d96e2b75006d440833800f683de6 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Mon, 17 Mar 2025 14:19:13 -0400 Subject: [PATCH 10/14] Define nullable for column (fixture handling) --- .../Blameable/BlameableWithSoftDeleteTest.php | 2 +- .../Blameable/Fixture/Entity/Article.php | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php b/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php index d052a4ced8..a7b386c1c2 100644 --- a/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php +++ b/tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php @@ -99,7 +99,7 @@ public function testBlameable(): void $this->em->flush(); static::assertTrue($article->isDeleted()); - static::assertSame('testuser', $article->getDeleted()); + static::assertSame('testuser', $article->getDeletedBy()); } protected function getUsedEntityFixtures(): array diff --git a/tests/Gedmo/Blameable/Fixture/Entity/Article.php b/tests/Gedmo/Blameable/Fixture/Entity/Article.php index 920aa6517c..dd33bab764 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/Article.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/Article.php @@ -41,9 +41,9 @@ class Article implements Blameable private $id; /** - * @ORM\Column(name="title", type="string", length=128) + * @ORM\Column(name="title", type="string", length=128, nullable=true) */ - #[ORM\Column(name: 'title', type: Types::STRING, length: 128)] + #[ORM\Column(name: 'title', type: Types::STRING, length: 128, nullable: true)] private ?string $title = null; /** @@ -55,31 +55,31 @@ class Article implements Blameable private $comments; /** - * @ORM\Column(name="created", type="string") + * @ORM\Column(name="created", type="string", nullable=true) * * @Gedmo\Blameable(on="create") */ - #[ORM\Column(name: 'created', type: Types::STRING)] + #[ORM\Column(name: 'created', type: Types::STRING, nullable: true)] #[Gedmo\Blameable(on: 'create')] private ?string $created = null; /** - * @ORM\Column(name="updated", type="string") + * @ORM\Column(name="updated", type="string", nullable=true) * * @Gedmo\Blameable(on="update") */ #[Gedmo\Blameable] - #[ORM\Column(name: 'updated', type: Types::STRING)] + #[ORM\Column(name: 'updated', type: Types::STRING, nullable: true)] private ?string $updated = null; /** - * @ORM\Column(name="deleted", type="string") + * @ORM\Column(name="deleted_by", type="string", nullable=true) * * @Gedmo\Blameable(on="remove") */ #[Gedmo\Blameable] - #[ORM\Column(name: 'deleted', type: Types::STRING)] - private ?string $deleted = null; + #[ORM\Column(name: 'deleted_by', type: Types::STRING, nullable: true)] + private ?string $deletedBy = null; /** * @ORM\Column(name="published", type="string", nullable=true) @@ -165,13 +165,13 @@ public function setUpdated(?string $updated): void $this->updated = $updated; } - public function getDeleted(): ?string + public function getDeletedBy(): ?string { - return $this->deleted; + return $this->deletedBy; } - public function setDeleted(?string $deleted): void + public function setDeletedBy(?string $deletedBy): void { - $this->deleted = $deleted; + $this->deletedBy = $deletedBy; } } From c6cb29f7aec2138a36df9939b1d19a742ccff7e6 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Mon, 17 Mar 2025 14:25:35 -0400 Subject: [PATCH 11/14] Explicitly define Blameable attribute event --- tests/Gedmo/Blameable/Fixture/Entity/Article.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Gedmo/Blameable/Fixture/Entity/Article.php b/tests/Gedmo/Blameable/Fixture/Entity/Article.php index dd33bab764..eb6ec90d02 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/Article.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/Article.php @@ -68,7 +68,7 @@ class Article implements Blameable * * @Gedmo\Blameable(on="update") */ - #[Gedmo\Blameable] + #[Gedmo\Blameable(on: 'update')] #[ORM\Column(name: 'updated', type: Types::STRING, nullable: true)] private ?string $updated = null; @@ -77,7 +77,7 @@ class Article implements Blameable * * @Gedmo\Blameable(on="remove") */ - #[Gedmo\Blameable] + #[Gedmo\Blameable(on: 'remove')] #[ORM\Column(name: 'deleted_by', type: Types::STRING, nullable: true)] private ?string $deletedBy = null; From a729496a23fc8f488ed023a1ef803e1fe33aeedc Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Mon, 17 Mar 2025 14:36:06 -0400 Subject: [PATCH 12/14] Add deprecated annotation for PHP 7.x support --- tests/Gedmo/Blameable/Fixture/Entity/Article.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Gedmo/Blameable/Fixture/Entity/Article.php b/tests/Gedmo/Blameable/Fixture/Entity/Article.php index eb6ec90d02..3dd8b63c6c 100644 --- a/tests/Gedmo/Blameable/Fixture/Entity/Article.php +++ b/tests/Gedmo/Blameable/Fixture/Entity/Article.php @@ -21,6 +21,8 @@ /** * @ORM\Entity + * + * @Gedmo\SoftDeleteable */ #[ORM\Entity] #[Gedmo\SoftDeleteable] From 1ddb7ea2144971720d9bae86f500c658599c98c1 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Wed, 26 Mar 2025 00:34:53 -0400 Subject: [PATCH 13/14] Restored superfulous PHPStan docblock tags needed for it to actually work properly. --- src/Mapping/MappedEventSubscriber.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Mapping/MappedEventSubscriber.php b/src/Mapping/MappedEventSubscriber.php index 2dfc3e5dda..d5b87c5388 100644 --- a/src/Mapping/MappedEventSubscriber.php +++ b/src/Mapping/MappedEventSubscriber.php @@ -41,6 +41,7 @@ * extended drivers * * @phpstan-template TConfig of array + * @phpstan-template TEventAdapter of AdapterInterface * * @author Gediminas Morkevicius */ @@ -255,6 +256,8 @@ public function loadMetadataForObjectClass(ObjectManager $objectManager, $metada * @throws InvalidArgumentException if event is not recognized * * @return AdapterInterface + * + * @phpstan-return TEventAdapter */ protected function getEventAdapter(EventArgs $args) { From c016b37daaa2f94fb2b0418a89bd6b24e475b587 Mon Sep 17 00:00:00 2001 From: Jacob Thomason Date: Wed, 26 Mar 2025 00:50:40 -0400 Subject: [PATCH 14/14] CS fixes --- phpstan-baseline.neon | 5 ++--- src/AbstractTrackingListener.php | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index a5fd58ccdf..e6709a3bfe 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3,13 +3,13 @@ parameters: - message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\\:\:getReflectionProperty\(\)\.$#' identifier: method.notFound - count: 4 + count: 6 path: src/AbstractTrackingListener.php - message: '#^Call to an undefined method Doctrine\\Persistence\\ObjectManager\:\:getUnitOfWork\(\)\.$#' identifier: method.notFound - count: 3 + count: 4 path: src/AbstractTrackingListener.php - @@ -1247,4 +1247,3 @@ parameters: identifier: deadCode.unreachable count: 1 path: tests/Gedmo/Tree/MaterializedPathODMMongoDBTreeLockingTest.php - diff --git a/src/AbstractTrackingListener.php b/src/AbstractTrackingListener.php index f281e89b73..7ff9097e5a 100644 --- a/src/AbstractTrackingListener.php +++ b/src/AbstractTrackingListener.php @@ -206,6 +206,10 @@ public function prePersist(EventArgs $args) /** * Checks for a soft delete event as remove + * + * @param LifecycleEventArgs $args + * + * @phpstan-param LifecycleEventArgs $args */ public function preRemove(EventArgs $args): void {