Skip to content

Commit a20607b

Browse files
authored
Merge pull request #332 from patchlevel/projector-helper
add projector helper
2 parents e357e64 + 218afa3 commit a20607b

File tree

6 files changed

+264
-55
lines changed

6 files changed

+264
-55
lines changed

baseline.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,17 @@
412412
<code>new ProjectionListener($projectionRepository-&gt;reveal())</code>
413413
</DeprecatedClass>
414414
</file>
415+
<file src="tests/Unit/Projection/ProjectorHelperTest.php">
416+
<DeprecatedInterface occurrences="7">
417+
<code>class implements Projector {</code>
418+
<code>class implements Projector {</code>
419+
<code>class implements Projector {</code>
420+
<code>class implements Projector {</code>
421+
<code>class implements Projector {</code>
422+
<code>class implements Projector {</code>
423+
<code>class implements Projector {</code>
424+
</DeprecatedInterface>
425+
</file>
415426
<file src="tests/Unit/Projection/SyncProjectorListenerTest.php">
416427
<DeprecatedInterface occurrences="2">
417428
<code>class implements Projector {</code>

docs/pages/projection.md

Lines changed: 65 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,9 @@ use Patchlevel\EventSourcing\Projection\ProjectorId;
2424

2525
final class ProfileProjection implements Projector
2626
{
27-
private Connection $connection;
28-
29-
public function __construct(Connection $connection)
30-
{
31-
$this->connection = $connection;
27+
public function __construct(
28+
private readonly Connection $connection
29+
) {
3230
}
3331

3432
public function projectorId(): ProjectorId
@@ -67,6 +65,21 @@ final class ProfileProjection implements Projector
6765
}
6866
```
6967

68+
Each projector needs an `projectorId` composed of a unique name and a version number.
69+
With the help of this information, a projection can be clearly identified. More on that later.
70+
71+
Projectors can also have one `create` and `drop` method that is executed when the projection is created or deleted.
72+
In some cases it may be that no schema has to be created for the projection, as the target does it automatically.
73+
To do this, you must add either the `Create` or `Drop` attribute to the method. The method name itself doesn't matter.
74+
75+
Otherwise, a projector can have any number of handle methods that are called for certain defined events.
76+
In order to say which method is responsible for which event, you need the `Handle` attribute.
77+
As the first parameter, you must pass the event class to which the reaction should then take place.
78+
The method itself must expect a `Message`, which then contains the event. The method name itself doesn't matter.
79+
80+
As soon as the event has been dispatched, the appropriate methods are then executed.
81+
Several projectors can also listen to the same event.
82+
7083
!!! danger
7184

7285
You should not execute any actions with projectors,
@@ -77,68 +90,75 @@ final class ProfileProjection implements Projector
7790
If you are using psalm then you can install the event sourcing [plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin)
7891
to make the event method return the correct type.
7992

80-
Projectors have a `create` and a `drop` method that is executed when the projection is created or deleted.
81-
In some cases it may be that no schema has to be created for the projection, as the target does it automatically.
93+
## Projector Repository
8294

83-
In order for the projector to know which method is responsible for which event,
84-
the methods must be given the `Handle` attribute with the respective event class name.
85-
86-
As soon as the event has been dispatched, the appropriate methods are then executed.
87-
Several projectors can also listen to the same event.
88-
89-
## Register projector
90-
91-
So that the projectors are known and also executed, you have to add them to the `ProjectionHandler`.
92-
Then add this to the event bus using the `ProjectionListener`.
95+
The projector repository can hold and make available all projectors.
9396

9497
```php
95-
use Patchlevel\EventSourcing\EventBus\DefaultEventBus;
96-
use Patchlevel\EventSourcing\Projection\MetadataAwareProjectionHandler;
97-
use Patchlevel\EventSourcing\Projection\ProjectionListener;
98-
99-
$profileProjection = new ProfileProjection($connection);
100-
$messageProjection = new MessageProjection($connection);
98+
use Patchlevel\EventSourcing\Projection\DefaultProjectorRepository;
10199

102-
$projectionHandler = new MetadataAwareProjectionHandler([
103-
$profileProjection,
104-
$messageProjection,
100+
$projectorRepository = new DefaultProjectorRepository([
101+
new ProfileProjection($connection)
105102
]);
106-
107-
$eventBus->addListener(new ProjectionListener($projectionHandler));
108103
```
109104

110-
!!! note
111-
112-
You can find out more about the event bus [here](./event_bus.md).
113-
114105
## Setup Projection
115106

116107
A projection schama or database usually has to be created beforehand.
117108
And with a rebuild, the projection has to be deleted.
118-
To make this possible, projections have two methods `create` and `drop` that can be defined and executed.
109+
The Projector Helper can help with this:
119110

120111
### Create Projection Schema
121112

122-
Or for all projections in the `MetadataAwareProjectionHandler`:
113+
With this you can prepare the projection:
123114

124115
```php
125-
$projectionRepository = new MetadataAwareProjectionHandler([
126-
$profileProjection,
127-
$messageProjection,
128-
]);
116+
use Patchlevel\EventSourcing\Projection\ProjectorHelper;
129117

130-
$projectionRepository->create();
118+
(new ProjectorHelper())->createProjection(new ProfileProjection($connection));
119+
(new ProjectorHelper())->createProjection(...$projectionRepository->projectors());
131120
```
132121

133122
### Drop Projection Schema
134123

135-
Or for all projections in the `MetadataAwareProjectionHandler`:
124+
The projection can also be removed again:
136125

137126
```php
138-
$projectionRepository = new MetadataAwareProjectionHandler([
139-
$profileProjection,
140-
$messageProjection,
141-
]);
127+
use Patchlevel\EventSourcing\Projection\ProjectorHelper;
142128

143-
$projectionRepository->drop();
129+
(new ProjectorHelper())->dropProjection(new ProfileProjection($connection));
130+
(new ProjectorHelper())->dropProjection(...$projectionRepository->projectors());
144131
```
132+
133+
## Handle Message
134+
135+
The helper also offers methods to process messages:
136+
137+
```php
138+
use Patchlevel\EventSourcing\Projection\ProjectorHelper;
139+
140+
(new ProjectorHelper())->handleMessage($message, new ProfileProjection($connection));
141+
(new ProjectorHelper())->handleMessage($message, ...$projectionRepository->projectors());
142+
```
143+
144+
## Sync Projector Listener
145+
146+
The simplest configuration is to run the projectors synchronously.
147+
Says that you listen to the event bus and update the projections directly.
148+
Here you can use the `SyncProjectorListener`.
149+
150+
```php
151+
use Patchlevel\EventSourcing\Projection\SyncProjectorListener
152+
153+
$eventBus->addListener(
154+
new SyncProjectorListener($projectorRepository)
155+
);
156+
```
157+
158+
!!! note
159+
160+
You can find out more about the event bus [here](./event_bus.md).
161+
162+
!!! note
163+
164+
In order to exploit the full potential, the [projectionist](./projectionist.md) should be used in production.

src/Projection/MetadataAwareProjectionHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
use Patchlevel\EventSourcing\Metadata\Projection\ProjectionMetadataFactory;
1010

1111
/**
12-
* @deprecated use MetadataProjectorResolver
12+
* @deprecated use ProjectorHelper
1313
*/
1414
final class MetadataAwareProjectionHandler implements ProjectionHandler
1515
{

src/Projection/ProjectorHelper.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Projection;
6+
7+
use Patchlevel\EventSourcing\EventBus\Message;
8+
9+
final class ProjectorHelper
10+
{
11+
public function __construct(
12+
private readonly ProjectorResolver $projectorResolver = new MetadataProjectorResolver()
13+
) {
14+
}
15+
16+
public function handleMessage(Message $message, Projector ...$projectors): void
17+
{
18+
foreach ($projectors as $projector) {
19+
$handleMethod = $this->projectorResolver->resolveHandleMethod($projector, $message);
20+
21+
if (!$handleMethod) {
22+
continue;
23+
}
24+
25+
$handleMethod($message);
26+
}
27+
}
28+
29+
public function createProjection(Projector ...$projectors): void
30+
{
31+
foreach ($projectors as $projector) {
32+
$createMethod = $this->projectorResolver->resolveCreateMethod($projector);
33+
34+
if (!$createMethod) {
35+
continue;
36+
}
37+
38+
$createMethod();
39+
}
40+
}
41+
42+
public function dropProjection(Projector ...$projectors): void
43+
{
44+
foreach ($projectors as $projector) {
45+
$dropMethod = $this->projectorResolver->resolveDropMethod($projector);
46+
47+
if (!$dropMethod) {
48+
continue;
49+
}
50+
51+
$dropMethod();
52+
}
53+
}
54+
}

src/Projection/SyncProjectorListener.php

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,7 @@ public function __construct(
1717

1818
public function __invoke(Message $message): void
1919
{
20-
foreach ($this->projectorRepository->projectors() as $projector) {
21-
$handleMethod = $this->projectorResolver->resolveHandleMethod($projector, $message);
22-
23-
if (!$handleMethod) {
24-
continue;
25-
}
26-
27-
$handleMethod($message);
28-
}
20+
(new ProjectorHelper($this->projectorResolver))
21+
->handleMessage($message, ...$this->projectorRepository->projectors());
2922
}
3023
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Unit\Projection;
6+
7+
use Patchlevel\EventSourcing\Attribute\Create;
8+
use Patchlevel\EventSourcing\Attribute\Drop;
9+
use Patchlevel\EventSourcing\Attribute\Handle;
10+
use Patchlevel\EventSourcing\EventBus\Message;
11+
use Patchlevel\EventSourcing\Projection\Projector;
12+
use Patchlevel\EventSourcing\Projection\ProjectorHelper;
13+
use Patchlevel\EventSourcing\Projection\ProjectorId;
14+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\Email;
15+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileCreated;
16+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileId;
17+
use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileVisited;
18+
use PHPUnit\Framework\TestCase;
19+
20+
/** @covers \Patchlevel\EventSourcing\Projection\ProjectorHelper */
21+
final class ProjectorHelperTest extends TestCase
22+
{
23+
public function testHandle(): void
24+
{
25+
$projector = new class implements Projector {
26+
public static ?Message $handledMessage = null;
27+
28+
public function projectorId(): ProjectorId
29+
{
30+
return new ProjectorId('test', 1);
31+
}
32+
33+
#[Handle(ProfileCreated::class)]
34+
public function handleProfileCreated(Message $message): void
35+
{
36+
self::$handledMessage = $message;
37+
}
38+
};
39+
40+
$event = new ProfileCreated(
41+
ProfileId::fromString('1'),
42+
Email::fromString('[email protected]')
43+
);
44+
45+
$message = new Message(
46+
$event
47+
);
48+
49+
$helper = new ProjectorHelper();
50+
$helper->handleMessage($message, $projector);
51+
52+
self::assertSame($message, $projector::$handledMessage);
53+
}
54+
55+
public function testHandleNotSupportedEvent(): void
56+
{
57+
$projector = new class implements Projector {
58+
public static ?Message $handledMessage = null;
59+
60+
public function projectorId(): ProjectorId
61+
{
62+
return new ProjectorId('test', 1);
63+
}
64+
65+
#[Handle(ProfileCreated::class)]
66+
public function handleProfileCreated(Message $message): void
67+
{
68+
self::$handledMessage = $message;
69+
}
70+
};
71+
72+
$event = new ProfileVisited(
73+
ProfileId::fromString('1')
74+
);
75+
76+
$message = new Message(
77+
$event
78+
);
79+
80+
$helper = new ProjectorHelper();
81+
$helper->handleMessage($message, $projector);
82+
83+
self::assertNull($projector::$handledMessage);
84+
}
85+
86+
public function testCreate(): void
87+
{
88+
$projector = new class implements Projector {
89+
public static bool $called = false;
90+
91+
public function projectorId(): ProjectorId
92+
{
93+
return new ProjectorId('test', 1);
94+
}
95+
96+
#[Create]
97+
public function method(): void
98+
{
99+
self::$called = true;
100+
}
101+
};
102+
103+
$helper = new ProjectorHelper();
104+
$helper->createProjection($projector);
105+
106+
self::assertTrue($projector::$called);
107+
}
108+
109+
public function testDrop(): void
110+
{
111+
$projector = new class implements Projector {
112+
public static bool $called = false;
113+
114+
public function projectorId(): ProjectorId
115+
{
116+
return new ProjectorId('test', 1);
117+
}
118+
119+
#[Drop]
120+
public function method(): void
121+
{
122+
self::$called = true;
123+
}
124+
};
125+
126+
$helper = new ProjectorHelper();
127+
$helper->dropProjection($projector);
128+
129+
self::assertTrue($projector::$called);
130+
}
131+
}

0 commit comments

Comments
 (0)