Skip to content

Commit faaae31

Browse files
committed
Add support for an actor provider for extensions which use a user reference
1 parent 8cc3f95 commit faaae31

12 files changed

+331
-25
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ a release.
1818
---
1919

2020
## [Unreleased]
21+
### Added
22+
- Actor provider for use with extensions with user references (#2914)
23+
2124
### Changed
2225
- Updated minimum versions for `doctrine/orm` to ^2.20 || ^3.3
2326

doc/blameable.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,18 @@ $om->getEventManager()->addEventSubscriber($listener);
2424
```
2525

2626
Then, once your application has it available (i.e. after validating the authentication for your user during an HTTP request),
27-
you can set a reference to the user to be blamed for changes by calling the listener's `setUserValue` method.
27+
you can set a reference to the user to be blamed for changes.
28+
29+
The user reference can be set through either an [actor provider service](./utils/actor-provider.md) or by calling the
30+
listener's `setUserValue` method with a resolved user.
31+
32+
> [!TIP]
33+
> When an actor provider is given to the extension, any data set with the `setUserValue` method will be ignored.
2834
2935
```php
36+
// The $provider must be an implementation of Gedmo\Tool\ActorProviderInterface
37+
$listener->setActorProvider($provider);
38+
3039
// The $user can be either an object or a string
3140
$listener->setUserValue($user);
3241
```

doc/loggable.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,18 @@ $om->getEventManager()->addEventSubscriber($listener);
2929
```
3030

3131
Then, once your application has it available (i.e. after validating the authentication for your user during an HTTP request),
32-
you can set a reference to the user who performed actions on a loggable model by calling the listener's `setUsername` method.
32+
you can set a reference to the user who performed actions on a loggable model.
33+
34+
The user reference can be set through either an [actor provider service](./utils/actor-provider.md) or by calling the
35+
listener's `setUsername` method with a resolved user.
36+
37+
> [!TIP]
38+
> When an actor provider is given to the extension, any data set with the `setUsername` method will be ignored.
3339
3440
```php
41+
// The $provider must be an implementation of Gedmo\Tool\ActorProviderInterface
42+
$listener->setActorProvider($provider);
43+
3544
// The $user can be either an object or a string
3645
$listener->setUsername($user);
3746
```

doc/utils/actor-provider.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Actor Provider
2+
3+
The Doctrine Extensions package includes support for an "actor provider" for extensions which use a user value, such as
4+
the blameable or loggable extensions.
5+
6+
## Index
7+
8+
- [Getting Started](#getting-started)
9+
- [Benefits of Actor Providers](#benefits-of-actor-providers)
10+
11+
## Getting Started
12+
13+
Out of the box, the library does not provide an implementation for the `Gedmo\Tool\ActorProviderInterface`, so you will
14+
need to create a class in your application. Below is an example of an actor provider using Symfony's Security components:
15+
16+
```php
17+
namespace App\Utils;
18+
19+
use Gedmo\Tool\ActorProviderInterface;
20+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
21+
22+
final class SymfonyActorProvider implements ActorProviderInterface
23+
{
24+
private TokenStorageInterface $tokenStorage;
25+
26+
public function __construct(TokenStorageInterface $tokenStorage)
27+
{
28+
$this->tokenStorage = $tokenStorage;
29+
}
30+
31+
/**
32+
* @return object|string|null
33+
*/
34+
public function getActor()
35+
{
36+
$token = $this->tokenStorage->getToken();
37+
38+
return $token ? $token->getUser() : null;
39+
}
40+
}
41+
```
42+
43+
Once you've created your actor provider, you can inject it into the listeners for supported extensions by calling
44+
the `setActorProvider` method.
45+
46+
```php
47+
/** Gedmo\Blameable\BlameableListener $listener */
48+
$listener->setActorProvider($provider);
49+
```
50+
51+
## Benefits of Actor Providers
52+
53+
Unlike the previously existing APIs for the extensions which support user references, actor providers allow lazily
54+
resolving the user value when it is needed instead of eagerly fetching it when the listener is created. Actor providers
55+
would also integrate nicely with long-running processes such as FrankenPHP where the provider can be reset between
56+
requests.

src/Blameable/BlameableListener.php

+26-11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Gedmo\AbstractTrackingListener;
1414
use Gedmo\Blameable\Mapping\Event\BlameableAdapter;
1515
use Gedmo\Exception\InvalidArgumentException;
16+
use Gedmo\Tool\ActorProviderInterface;
1617

1718
/**
1819
* The Blameable listener handles the update of
@@ -26,6 +27,8 @@
2627
*/
2728
class BlameableListener extends AbstractTrackingListener
2829
{
30+
protected ?ActorProviderInterface $actorProvider = null;
31+
2932
/**
3033
* @var mixed
3134
*/
@@ -42,34 +45,46 @@ class BlameableListener extends AbstractTrackingListener
4245
*/
4346
public function getFieldValue($meta, $field, $eventAdapter)
4447
{
48+
$actor = $this->actorProvider instanceof ActorProviderInterface ? $this->actorProvider->getActor() : $this->user;
49+
4550
if ($meta->hasAssociation($field)) {
46-
if (null !== $this->user && !is_object($this->user)) {
51+
if (null !== $actor && !is_object($actor)) {
4752
throw new InvalidArgumentException('Blame is reference, user must be an object');
4853
}
4954

50-
return $this->user;
55+
return $actor;
5156
}
5257

5358
// ok so it's not an association, then it is a string, or an object
54-
if (is_object($this->user)) {
55-
if (method_exists($this->user, 'getUserIdentifier')) {
56-
return (string) $this->user->getUserIdentifier();
59+
if (is_object($actor)) {
60+
if (method_exists($actor, 'getUserIdentifier')) {
61+
return (string) $actor->getUserIdentifier();
5762
}
58-
if (method_exists($this->user, 'getUsername')) {
59-
return (string) $this->user->getUsername();
63+
if (method_exists($actor, 'getUsername')) {
64+
return (string) $actor->getUsername();
6065
}
61-
if (method_exists($this->user, '__toString')) {
62-
return $this->user->__toString();
66+
if (method_exists($actor, '__toString')) {
67+
return $actor->__toString();
6368
}
6469

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

68-
return $this->user;
73+
return $actor;
74+
}
75+
76+
/**
77+
* Set an actor provider for the user value.
78+
*/
79+
public function setActorProvider(ActorProviderInterface $actorProvider): void
80+
{
81+
$this->actorProvider = $actorProvider;
6982
}
7083

7184
/**
72-
* Set a user value to return
85+
* Set a user value to return.
86+
*
87+
* If an actor provider is also provided, it will take precedence over this value.
7388
*
7489
* @param mixed $user
7590
*

src/Loggable/LoggableListener.php

+50-1
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717
use Doctrine\Persistence\Mapping\ClassMetadata;
1818
use Doctrine\Persistence\ObjectManager;
1919
use Gedmo\Exception\InvalidArgumentException;
20+
use Gedmo\Exception\UnexpectedValueException;
2021
use Gedmo\Loggable\Mapping\Event\LoggableAdapter;
2122
use Gedmo\Mapping\MappedEventSubscriber;
23+
use Gedmo\Tool\ActorProviderInterface;
2224
use Gedmo\Tool\Wrapper\AbstractWrapper;
2325

2426
/**
@@ -55,6 +57,8 @@ class LoggableListener extends MappedEventSubscriber
5557
*/
5658
public const ACTION_REMOVE = LogEntryInterface::ACTION_REMOVE;
5759

60+
protected ?ActorProviderInterface $actorProvider = null;
61+
5862
/**
5963
* Username for identification
6064
*
@@ -85,9 +89,19 @@ class LoggableListener extends MappedEventSubscriber
8589
*/
8690
protected $pendingRelatedObjects = [];
8791

92+
/**
93+
* Set an actor provider for the user value.
94+
*/
95+
public function setActorProvider(ActorProviderInterface $actorProvider): void
96+
{
97+
$this->actorProvider = $actorProvider;
98+
}
99+
88100
/**
89101
* Set username for identification
90102
*
103+
* If an actor provider is also provided, it will take precedence over this value.
104+
*
91105
* @param mixed $username
92106
*
93107
* @throws InvalidArgumentException Invalid username
@@ -229,6 +243,41 @@ protected function getLogEntryClass(LoggableAdapter $ea, $class)
229243
return self::$configurations[$this->name][$class]['logEntryClass'] ?? $ea->getDefaultLogEntryClass();
230244
}
231245

246+
/**
247+
* Retrieve the username to use for the log entry.
248+
*
249+
* This method will try to fetch a username from the actor provider first, falling back to the {@see $this->username}
250+
* property if the provider is not set or does not provide a value.
251+
*
252+
* @throws UnexpectedValueException if the actor provider provides an unsupported username value
253+
*/
254+
protected function getUsername(): ?string
255+
{
256+
if ($this->actorProvider instanceof ActorProviderInterface) {
257+
$actor = $this->actorProvider->getActor();
258+
259+
if (is_string($actor) || null === $actor) {
260+
return $actor;
261+
}
262+
263+
if (method_exists($actor, 'getUserIdentifier')) {
264+
return (string) $actor->getUserIdentifier();
265+
}
266+
267+
if (method_exists($actor, 'getUsername')) {
268+
return (string) $actor->getUsername();
269+
}
270+
271+
if (method_exists($actor, '__toString')) {
272+
return $actor->__toString();
273+
}
274+
275+
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)));
276+
}
277+
278+
return $this->username;
279+
}
280+
232281
/**
233282
* Handle any custom LogEntry functionality that needs to be performed
234283
* before persisting it
@@ -328,7 +377,7 @@ protected function createLogEntry($action, $object, LoggableAdapter $ea)
328377
$logEntry = $logEntryMeta->newInstance();
329378

330379
$logEntry->setAction($action);
331-
$logEntry->setUsername($this->username);
380+
$logEntry->setUsername($this->getUsername());
332381
$logEntry->setObjectClass($meta->getName());
333382
$logEntry->setLoggedAt();
334383

src/Tool/ActorProviderInterface.php

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Doctrine Behavioral Extensions package.
5+
* (c) Gediminas Morkevicius <[email protected]> http://www.gediminasm.org
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
namespace Gedmo\Tool;
11+
12+
/**
13+
* Interface for a provider for an actor for extensions supporting actor/user references.
14+
*/
15+
interface ActorProviderInterface
16+
{
17+
/**
18+
* @return object|string|null
19+
*/
20+
public function getActor();
21+
}

tests/Gedmo/Blameable/BlameableTest.php

+50-4
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
namespace Gedmo\Tests\Blameable;
1313

1414
use Doctrine\Common\EventManager;
15-
use Gedmo\Blameable\Blameable;
1615
use Gedmo\Blameable\BlameableListener;
1716
use Gedmo\Tests\Blameable\Fixture\Entity\Article;
1817
use Gedmo\Tests\Blameable\Fixture\Entity\Comment;
1918
use Gedmo\Tests\Blameable\Fixture\Entity\Type;
19+
use Gedmo\Tests\TestActorProvider;
2020
use Gedmo\Tests\Tool\BaseTestCaseORM;
2121

2222
/**
@@ -26,15 +26,17 @@
2626
*/
2727
final class BlameableTest extends BaseTestCaseORM
2828
{
29+
private BlameableListener $listener;
30+
2931
protected function setUp(): void
3032
{
3133
parent::setUp();
3234

33-
$listener = new BlameableListener();
34-
$listener->setUserValue('testuser');
35+
$this->listener = new BlameableListener();
36+
$this->listener->setUserValue('testuser');
3537

3638
$evm = new EventManager();
37-
$evm->addEventSubscriber($listener);
39+
$evm->addEventSubscriber($this->listener);
3840

3941
$this->getDefaultMockSqliteEntityManager($evm);
4042
}
@@ -81,6 +83,50 @@ public function testBlameable(): void
8183
static::assertSame('testuser', $sport->getPublished());
8284
}
8385

86+
public function testBlameableWithActorProvider(): void
87+
{
88+
$this->listener->setActorProvider(new TestActorProvider('testactor'));
89+
90+
$sport = new Article();
91+
$sport->setTitle('Sport');
92+
93+
$sportComment = new Comment();
94+
$sportComment->setMessage('hello');
95+
$sportComment->setArticle($sport);
96+
$sportComment->setStatus(0);
97+
98+
$this->em->persist($sport);
99+
$this->em->persist($sportComment);
100+
$this->em->flush();
101+
$this->em->clear();
102+
103+
$sport = $this->em->getRepository(Article::class)->findOneBy(['title' => 'Sport']);
104+
static::assertSame('testactor', $sport->getCreated());
105+
static::assertSame('testactor', $sport->getUpdated());
106+
static::assertNull($sport->getPublished());
107+
108+
$sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']);
109+
static::assertSame('testactor', $sportComment->getModified());
110+
static::assertNull($sportComment->getClosed());
111+
112+
$sportComment->setStatus(1);
113+
$published = new Type();
114+
$published->setTitle('Published');
115+
116+
$sport->setTitle('Updated');
117+
$sport->setType($published);
118+
$this->em->persist($sport);
119+
$this->em->persist($published);
120+
$this->em->persist($sportComment);
121+
$this->em->flush();
122+
$this->em->clear();
123+
124+
$sportComment = $this->em->getRepository(Comment::class)->findOneBy(['message' => 'hello']);
125+
static::assertSame('testactor', $sportComment->getClosed());
126+
127+
static::assertSame('testactor', $sport->getPublished());
128+
}
129+
84130
public function testForcedValues(): void
85131
{
86132
$sport = new Article();

tests/Gedmo/Loggable/AnnotationLoggableEntityTest.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@ protected function setUp(): void
2626
parent::setUp();
2727

2828
$evm = new EventManager();
29-
$loggableListener = new LoggableListener();
30-
$loggableListener->setUsername('jules');
31-
$evm->addEventSubscriber($loggableListener);
29+
$this->listener = new LoggableListener();
30+
$this->listener->setUsername('jules');
31+
$evm->addEventSubscriber($this->listener);
3232

3333
$this->em = $this->getDefaultMockSqliteEntityManager($evm);
3434
}

0 commit comments

Comments
 (0)