Skip to content

Add support for an actor provider for extensions which use a user reference #2914

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 24, 2025
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ a release.
---

## [Unreleased]
### Added
- Actor provider for use with extensions with user references (#2914)

### Changed
- Updated minimum versions for `doctrine/orm` to ^2.20 || ^3.3

Expand Down
11 changes: 10 additions & 1 deletion doc/blameable.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,18 @@ $om->getEventManager()->addEventSubscriber($listener);
```

Then, once your application has it available (i.e. after validating the authentication for your user during an HTTP request),
you can set a reference to the user to be blamed for changes by calling the listener's `setUserValue` method.
you can set a reference to the user to be blamed for changes.

The user reference can be set through either an [actor provider service](./utils/actor-provider.md) or by calling the
listener's `setUserValue` method with a resolved user.

> [!TIP]
> When an actor provider is given to the extension, any data set with the `setUserValue` method will be ignored.

```php
// The $provider must be an implementation of Gedmo\Tool\ActorProviderInterface
$listener->setActorProvider($provider);

// The $user can be either an object or a string
$listener->setUserValue($user);
```
Expand Down
11 changes: 10 additions & 1 deletion doc/loggable.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,18 @@ $om->getEventManager()->addEventSubscriber($listener);
```

Then, once your application has it available (i.e. after validating the authentication for your user during an HTTP request),
you can set a reference to the user who performed actions on a loggable model by calling the listener's `setUsername` method.
you can set a reference to the user who performed actions on a loggable model.

The user reference can be set through either an [actor provider service](./utils/actor-provider.md) or by calling the
listener's `setUsername` method with a resolved user.

> [!TIP]
> When an actor provider is given to the extension, any data set with the `setUsername` method will be ignored.

```php
// The $provider must be an implementation of Gedmo\Tool\ActorProviderInterface
$listener->setActorProvider($provider);

// The $user can be either an object or a string
$listener->setUsername($user);
```
Expand Down
56 changes: 56 additions & 0 deletions doc/utils/actor-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Actor Provider

The Doctrine Extensions package includes support for an "actor provider" for extensions which use a user value, such as
the blameable or loggable extensions.

## Index

- [Getting Started](#getting-started)
- [Benefits of Actor Providers](#benefits-of-actor-providers)

## Getting Started

Out of the box, the library does not provide an implementation for the `Gedmo\Tool\ActorProviderInterface`, so you will
need to create a class in your application. Below is an example of an actor provider using Symfony's Security components:

```php
namespace App\Utils;

use Gedmo\Tool\ActorProviderInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

final class SymfonyActorProvider implements ActorProviderInterface
{
private TokenStorageInterface $tokenStorage;

public function __construct(TokenStorageInterface $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}

/**
* @return object|string|null
*/
public function getActor()
{
$token = $this->tokenStorage->getToken();

return $token ? $token->getUser() : null;
}
}
```

Once you've created your actor provider, you can inject it into the listeners for supported extensions by calling
the `setActorProvider` method.

```php
/** Gedmo\Blameable\BlameableListener $listener */
$listener->setActorProvider($provider);
```

## Benefits of Actor Providers

Unlike the previously existing APIs for the extensions which support user references, actor providers allow lazily
resolving the user value when it is needed instead of eagerly fetching it when the listener is created. Actor providers
would also integrate nicely with long-running processes such as FrankenPHP where the provider can be reset between
requests.
37 changes: 26 additions & 11 deletions src/Blameable/BlameableListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use Gedmo\AbstractTrackingListener;
use Gedmo\Blameable\Mapping\Event\BlameableAdapter;
use Gedmo\Exception\InvalidArgumentException;
use Gedmo\Tool\ActorProviderInterface;

/**
* The Blameable listener handles the update of
Expand All @@ -26,6 +27,8 @@
*/
class BlameableListener extends AbstractTrackingListener
{
protected ?ActorProviderInterface $actorProvider = null;

/**
* @var mixed
*/
Expand All @@ -42,34 +45,46 @@
*/
public function getFieldValue($meta, $field, $eventAdapter)
{
$actor = $this->actorProvider instanceof ActorProviderInterface ? $this->actorProvider->getActor() : $this->user;

if ($meta->hasAssociation($field)) {
if (null !== $this->user && !is_object($this->user)) {
if (null !== $actor && !is_object($actor)) {
throw new InvalidArgumentException('Blame is reference, user must be an object');
}

return $this->user;
return $actor;
}

// ok so it's not an association, then it is a string, or an object
if (is_object($this->user)) {
if (method_exists($this->user, 'getUserIdentifier')) {
return (string) $this->user->getUserIdentifier();
if (is_object($actor)) {
if (method_exists($actor, 'getUserIdentifier')) {
return (string) $actor->getUserIdentifier();

Check warning on line 61 in src/Blameable/BlameableListener.php

View check run for this annotation

Codecov / codecov/patch

src/Blameable/BlameableListener.php#L61

Added line #L61 was not covered by tests
}
if (method_exists($this->user, 'getUsername')) {
return (string) $this->user->getUsername();
if (method_exists($actor, 'getUsername')) {
return (string) $actor->getUsername();
}
if (method_exists($this->user, '__toString')) {
return $this->user->__toString();
if (method_exists($actor, '__toString')) {
return $actor->__toString();
}

throw new InvalidArgumentException('Field expects string, user must be a string, or object should have method getUserIdentifier, getUsername or __toString');
}

return $this->user;
return $actor;
}

/**
* Set an actor provider for the user value.
*/
public function setActorProvider(ActorProviderInterface $actorProvider): void
{
$this->actorProvider = $actorProvider;
}

/**
* Set a user value to return
* Set a user value to return.
*
* If an actor provider is also provided, it will take precedence over this value.
*
* @param mixed $user
*
Expand Down
51 changes: 50 additions & 1 deletion src/Loggable/LoggableListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
use Doctrine\Persistence\Mapping\ClassMetadata;
use Doctrine\Persistence\ObjectManager;
use Gedmo\Exception\InvalidArgumentException;
use Gedmo\Exception\UnexpectedValueException;
use Gedmo\Loggable\Mapping\Event\LoggableAdapter;
use Gedmo\Mapping\MappedEventSubscriber;
use Gedmo\Tool\ActorProviderInterface;
use Gedmo\Tool\Wrapper\AbstractWrapper;

/**
Expand Down Expand Up @@ -55,6 +57,8 @@
*/
public const ACTION_REMOVE = LogEntryInterface::ACTION_REMOVE;

protected ?ActorProviderInterface $actorProvider = null;

/**
* Username for identification
*
Expand Down Expand Up @@ -85,9 +89,19 @@
*/
protected $pendingRelatedObjects = [];

/**
* Set an actor provider for the user value.
*/
public function setActorProvider(ActorProviderInterface $actorProvider): void
{
$this->actorProvider = $actorProvider;
}

/**
* Set username for identification
*
* If an actor provider is also provided, it will take precedence over this value.
*
* @param mixed $username
*
* @throws InvalidArgumentException Invalid username
Expand Down Expand Up @@ -229,6 +243,41 @@
return self::$configurations[$this->name][$class]['logEntryClass'] ?? $ea->getDefaultLogEntryClass();
}

/**
* Retrieve the username to use for the log entry.
*
* This method will try to fetch a username from the actor provider first, falling back to the {@see $this->username}
* property if the provider is not set or does not provide a value.
*
* @throws UnexpectedValueException if the actor provider provides an unsupported username value
*/
protected function getUsername(): ?string
{
if ($this->actorProvider instanceof ActorProviderInterface) {
$actor = $this->actorProvider->getActor();

if (is_string($actor) || null === $actor) {
return $actor;
}

if (method_exists($actor, 'getUserIdentifier')) {
return (string) $actor->getUserIdentifier();

Check warning on line 264 in src/Loggable/LoggableListener.php

View check run for this annotation

Codecov / codecov/patch

src/Loggable/LoggableListener.php#L263-L264

Added lines #L263 - L264 were not covered by tests
}

if (method_exists($actor, 'getUsername')) {
return (string) $actor->getUsername();

Check warning on line 268 in src/Loggable/LoggableListener.php

View check run for this annotation

Codecov / codecov/patch

src/Loggable/LoggableListener.php#L267-L268

Added lines #L267 - L268 were not covered by tests
}

if (method_exists($actor, '__toString')) {
return $actor->__toString();

Check warning on line 272 in src/Loggable/LoggableListener.php

View check run for this annotation

Codecov / codecov/patch

src/Loggable/LoggableListener.php#L271-L272

Added lines #L271 - L272 were not covered by tests
}

throw new UnexpectedValueException(\sprintf('The loggable extension requires the actor provider to return a string or an object implementing the "getUserIdentifier()", "getUsername()", or "__toString()" methods. "%s" cannot be used as an actor.', get_class($actor)));

Check warning on line 275 in src/Loggable/LoggableListener.php

View check run for this annotation

Codecov / codecov/patch

src/Loggable/LoggableListener.php#L275

Added line #L275 was not covered by tests
}

return $this->username;
}

/**
* Handle any custom LogEntry functionality that needs to be performed
* before persisting it
Expand Down Expand Up @@ -328,7 +377,7 @@
$logEntry = $logEntryMeta->newInstance();

$logEntry->setAction($action);
$logEntry->setUsername($this->username);
$logEntry->setUsername($this->getUsername());
$logEntry->setObjectClass($meta->getName());
$logEntry->setLoggedAt();

Expand Down
21 changes: 21 additions & 0 deletions src/Tool/ActorProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* 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\Tool;

/**
* Interface for a provider for an actor for extensions supporting actor/user references.
*/
interface ActorProviderInterface
{
/**
* @return object|string|null
*/
public function getActor();
}
54 changes: 50 additions & 4 deletions tests/Gedmo/Blameable/BlameableTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
namespace Gedmo\Tests\Blameable;

use Doctrine\Common\EventManager;
use Gedmo\Blameable\Blameable;
use Gedmo\Blameable\BlameableListener;
use Gedmo\Tests\Blameable\Fixture\Entity\Article;
use Gedmo\Tests\Blameable\Fixture\Entity\Comment;
use Gedmo\Tests\Blameable\Fixture\Entity\Type;
use Gedmo\Tests\TestActorProvider;
use Gedmo\Tests\Tool\BaseTestCaseORM;

/**
Expand All @@ -26,15 +26,17 @@
*/
final class BlameableTest extends BaseTestCaseORM
{
private BlameableListener $listener;

protected function setUp(): void
{
parent::setUp();

$listener = new BlameableListener();
$listener->setUserValue('testuser');
$this->listener = new BlameableListener();
$this->listener->setUserValue('testuser');

$evm = new EventManager();
$evm->addEventSubscriber($listener);
$evm->addEventSubscriber($this->listener);

$this->getDefaultMockSqliteEntityManager($evm);
}
Expand Down Expand Up @@ -81,6 +83,50 @@ public function testBlameable(): void
static::assertSame('testuser', $sport->getPublished());
}

public function testBlameableWithActorProvider(): void
{
$this->listener->setActorProvider(new TestActorProvider('testactor'));

$sport = new Article();
$sport->setTitle('Sport');

$sportComment = new Comment();
$sportComment->setMessage('hello');
$sportComment->setArticle($sport);
$sportComment->setStatus(0);

$this->em->persist($sport);
$this->em->persist($sportComment);
$this->em->flush();
$this->em->clear();

$sport = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']);
static::assertSame('testactor', $sport->getCreated());
static::assertSame('testactor', $sport->getUpdated());
static::assertNull($sport->getPublished());

$sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']);
static::assertSame('testactor', $sportComment->getModified());
static::assertNull($sportComment->getClosed());

$sportComment->setStatus(1);
$published = new Type();
$published->setTitle('Published');

$sport->setTitle('Updated');
$sport->setType($published);
$this->em->persist($sport);
$this->em->persist($published);
$this->em->persist($sportComment);
$this->em->flush();
$this->em->clear();

$sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']);
static::assertSame('testactor', $sportComment->getClosed());

static::assertSame('testactor', $sport->getPublished());
}

public function testForcedValues(): void
{
$sport = new Article();
Expand Down
6 changes: 3 additions & 3 deletions tests/Gedmo/Loggable/AnnotationLoggableEntityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ protected function setUp(): void
parent::setUp();

$evm = new EventManager();
$loggableListener = new LoggableListener();
$loggableListener->setUsername('jules');
$evm->addEventSubscriber($loggableListener);
$this->listener = new LoggableListener();
$this->listener->setUsername('jules');
$evm->addEventSubscriber($this->listener);

$this->em = $this->getDefaultMockSqliteEntityManager($evm);
}
Expand Down
Loading