Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ a release.
### Added
- IP address provider for use with extensions with IP address references (#2928)

## 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)
Expand Down Expand Up @@ -105,7 +108,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
Expand Down
23 changes: 16 additions & 7 deletions doc/blameable.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,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,
Expand All @@ -150,7 +159,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
Expand Down Expand Up @@ -178,14 +187,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)]
Expand Down Expand Up @@ -216,14 +225,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)]
Expand Down Expand Up @@ -262,14 +271,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'])]
Expand Down
19 changes: 15 additions & 4 deletions doc/sortable.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +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:

- [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)
- [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

Expand Down Expand Up @@ -340,3 +342,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;
```
5 changes: 2 additions & 3 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ parameters:
-
message: '#^Call to an undefined method Doctrine\\Persistence\\Mapping\\ClassMetadata\<object\>\:\: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

-
Expand Down Expand Up @@ -1247,4 +1247,3 @@ parameters:
identifier: deadCode.unreachable
count: 1
path: tests/Gedmo/Tree/MaterializedPathODMMongoDBTreeLockingTest.php

33 changes: 33 additions & 0 deletions src/AbstractTrackingListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,39 @@ public function prePersist(EventArgs $args)
}
}

/**
* Checks for a soft delete event as remove
*
* @param LifecycleEventArgs $args
*
* @phpstan-param LifecycleEventArgs<ObjectManager> $args
*/
public function preRemove(EventArgs $args): void
{
$ea = $this->getEventAdapter($args);
$om = $ea->getObjectManager();
$uow = $om->getUnitOfWork();
$object = $ea->getObject();
$meta = $om->getClassMetadata(get_class($object));
$config = $this->getConfiguration($om, $meta->getName());

if ($config) {
if (isset($config['remove'])) {
foreach ($config['remove'] as $field) {
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);
$uow->propertyChanged($object, $field, $oldValue, $newValue);
$uow->scheduleExtraUpdate($object, [
$field => [$oldValue, $newValue],
]);
}
}
}
}
}

/**
* Get the value for an updated field.
*
Expand Down
8 changes: 8 additions & 0 deletions src/Blameable/BlameableListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
2 changes: 1 addition & 1 deletion src/Blameable/Mapping/Driver/Attribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()}");
}

Expand Down
113 changes: 113 additions & 0 deletions tests/Gedmo/Blameable/BlameableWithSoftDeleteTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Doctrine Behavioral Extensions package.
* (c) Gediminas Morkevicius <[email protected]> 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\Tool\BaseTestCaseORM;

/**
* These are tests for Blameable behavior
*
* @author Gediminas Morkevicius <[email protected]>
*/
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']);
static::assertSame('testuser', $article->getCreated());
static::assertSame('testuser', $article->getUpdated());
static::assertNull($article->getPublished());

$comment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']);
static::assertSame('testuser', $comment->getModified());
static::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']);
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();

static::assertTrue($article->isDeleted());
static::assertSame('testuser', $article->getDeletedBy());
}

protected function getUsedEntityFixtures(): array
{
return [
Article::class,
Comment::class,
Type::class,
];
}
}
Loading