Skip to content

Commit e037b51

Browse files
authored
Merge pull request #817 from patchlevel/shared-apply-context
add shared apply context
2 parents 45c94fd + dd49672 commit e037b51

11 files changed

Lines changed: 158 additions & 12 deletions

File tree

deptrac-baseline.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ deptrac:
1010
- Patchlevel\EventSourcing\Subscription\RunMode
1111
Patchlevel\EventSourcing\Attribute\Projector:
1212
- Patchlevel\EventSourcing\Subscription\RunMode
13+
Patchlevel\EventSourcing\Attribute\SharedApplyContext:
14+
- Patchlevel\EventSourcing\Aggregate\AggregateRoot
1315
Patchlevel\EventSourcing\Attribute\Stream:
1416
- Patchlevel\EventSourcing\Aggregate\AggregateRoot
1517
Patchlevel\EventSourcing\Attribute\Subscriber:

docs/pages/aggregate.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@ final class Profile extends BasicAggregateRoot
303303
}
304304
}
305305
```
306+
!!! tip
307+
308+
You don't necessarily need to define multiple `Apply` attributes with the event class
309+
if you define the event types in the method using a union type.
310+
306311
## Suppress missing apply methods
307312

308313
Sometimes you have events that do not change the state of the aggregate itself,
@@ -358,6 +363,38 @@ final class Profile extends BasicAggregateRoot
358363

359364
When all events are suppressed, debugging becomes more difficult if you forget an apply method.
360365

366+
## Shared apply context
367+
368+
When working with [micro-aggregates](./aggregate.md#micro-aggregates),
369+
it’s common that events are applied by different aggregates.
370+
As a result, an aggregate may receive events it does not handle, which can lead to multiple “missing apply” warnings.
371+
372+
The `SharedApplyContext` attribute allows you to declare that several aggregates share the same apply context.
373+
With this configuration, a missing apply is only reported if none of the shared aggregates handle the event.
374+
375+
```php
376+
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
377+
use Patchlevel\EventSourcing\Attribute\Aggregate;
378+
use Patchlevel\EventSourcing\Attribute\SharedApplyContext;
379+
use Patchlevel\EventSourcing\Attribute\Stream;
380+
381+
#[Aggregate('profile')]
382+
#[SharedApplyContext([PersonalInformation::class])]
383+
final class Profile extends BasicAggregateRoot
384+
{
385+
}
386+
387+
#[Aggregate('personal_information')]
388+
#[Stream(Profile::class)]
389+
#[SharedApplyContext([Profile::class])]
390+
final class PersonalInformation extends BasicAggregateRoot
391+
{
392+
}
393+
```
394+
!!! warning
395+
396+
You need to define the `SharedApplyContext` attribute on all aggregates that share the apply context.
397+
361398
## Stream Name
362399

363400
!!! warning
@@ -675,8 +712,10 @@ use Patchlevel\EventSourcing\Aggregate\Uuid;
675712
use Patchlevel\EventSourcing\Attribute\Aggregate;
676713
use Patchlevel\EventSourcing\Attribute\Apply;
677714
use Patchlevel\EventSourcing\Attribute\Id;
715+
use Patchlevel\EventSourcing\Attribute\SharedApplyContext;
678716

679717
#[Aggregate('order')]
718+
#[SharedApplyContext([Shipping::class])]
680719
final class Order extends BasicAggregateRoot
681720
{
682721
#[Id]
@@ -706,10 +745,12 @@ use Patchlevel\EventSourcing\Aggregate\Uuid;
706745
use Patchlevel\EventSourcing\Attribute\Aggregate;
707746
use Patchlevel\EventSourcing\Attribute\Apply;
708747
use Patchlevel\EventSourcing\Attribute\Id;
748+
use Patchlevel\EventSourcing\Attribute\SharedApplyContext;
709749
use Patchlevel\EventSourcing\Attribute\Stream;
710750

711751
#[Aggregate('shipping')]
712752
#[Stream(Order::class)]
753+
#[SharedApplyContext([Order::class])]
713754
final class Shipping extends BasicAggregateRoot
714755
{
715756
#[Id]
@@ -740,6 +781,11 @@ final class Shipping extends BasicAggregateRoot
740781
}
741782
}
742783
```
784+
!!! tip
785+
786+
With the [SharedApplyContext](./aggregate.md#shared-apply-context) attribute,
787+
you can suppress missing applies for events that are handled by other aggregates.
788+
743789
### Child Aggregates
744790

745791
??? example "Experimental"

phpstan-baseline.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,12 @@ parameters:
444444
count: 1
445445
path: tests/Unit/Fixture/ProfileWithHandler.php
446446

447+
-
448+
message: '#^Property Patchlevel\\EventSourcing\\Tests\\Unit\\Fixture\\ProfileWithSharedApplyContext\:\:\$id is unused\.$#'
449+
identifier: property.unused
450+
count: 1
451+
path: tests/Unit/Fixture/ProfileWithSharedApplyContext.php
452+
447453
-
448454
message: '#^Property Patchlevel\\EventSourcing\\Tests\\Unit\\Fixture\\ProfileWithSuppressAll\:\:\$id is unused\.$#'
449455
identifier: property.unused
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Attribute;
6+
7+
use Attribute;
8+
use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
9+
10+
#[Attribute(Attribute::TARGET_CLASS)]
11+
final readonly class SharedApplyContext
12+
{
13+
/** @param list<class-string<AggregateRoot>> $aggregates */
14+
public function __construct(
15+
public array $aggregates,
16+
) {
17+
}
18+
}

src/Metadata/AggregateRoot/AttributeAggregateRootMetadataFactory.php

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Patchlevel\EventSourcing\Attribute\Apply;
1010
use Patchlevel\EventSourcing\Attribute\ChildAggregate;
1111
use Patchlevel\EventSourcing\Attribute\Id;
12+
use Patchlevel\EventSourcing\Attribute\SharedApplyContext;
1213
use Patchlevel\EventSourcing\Attribute\Snapshot as AttributeSnapshot;
1314
use Patchlevel\EventSourcing\Attribute\Stream;
1415
use Patchlevel\EventSourcing\Attribute\SuppressMissingApply;
@@ -73,25 +74,42 @@ public function metadata(string $aggregate): AggregateRootMetadata
7374
private function findSuppressMissingApply(ReflectionClass $reflector): array
7475
{
7576
$suppressEvents = [];
76-
$suppressAll = false;
7777

7878
$attributes = $reflector->getAttributes(SuppressMissingApply::class);
7979

80-
foreach ($attributes as $attribute) {
81-
$instance = $attribute->newInstance();
80+
if ($attributes !== []) {
81+
$instance = $attributes[0]->newInstance();
8282

8383
if ($instance->suppressAll) {
84-
$suppressAll = true;
85-
86-
continue;
84+
return [[], true];
8785
}
8886

8987
foreach ($instance->suppressEvents as $event) {
9088
$suppressEvents[$event] = true;
9189
}
9290
}
9391

94-
return [$suppressEvents, $suppressAll];
92+
$attributes = $reflector->getAttributes(SharedApplyContext::class);
93+
94+
if ($attributes !== []) {
95+
$instance = $attributes[0]->newInstance();
96+
97+
foreach ($instance->aggregates as $aggregateClass) {
98+
$reflectionClass = new ReflectionClass($aggregateClass);
99+
100+
$applyMethods = $this->findApplyMethods(
101+
$reflectionClass,
102+
$aggregateClass,
103+
$this->findChildAggregates($reflectionClass),
104+
);
105+
106+
foreach ($applyMethods as $eventClass => $method) {
107+
$suppressEvents[$eventClass] = true;
108+
}
109+
}
110+
}
111+
112+
return [$suppressEvents, false];
95113
}
96114

97115
private function findAggregateName(ReflectionClass $reflector): string

tests/Integration/MicroAggregate/PersonalInformation.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
use Patchlevel\EventSourcing\Attribute\Aggregate;
99
use Patchlevel\EventSourcing\Attribute\Apply;
1010
use Patchlevel\EventSourcing\Attribute\Id;
11+
use Patchlevel\EventSourcing\Attribute\SharedApplyContext;
1112
use Patchlevel\EventSourcing\Attribute\Stream;
1213
use Patchlevel\EventSourcing\Tests\Integration\MicroAggregate\Events\NameChanged;
1314
use Patchlevel\EventSourcing\Tests\Integration\MicroAggregate\Events\ProfileCreated;
1415

1516
#[Aggregate('personal_information')]
1617
#[Stream(Profile::class)]
18+
#[SharedApplyContext([Profile::class])]
1719
final class PersonalInformation extends BasicAggregateRoot
1820
{
1921
#[Id]

tests/Integration/MicroAggregate/Profile.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@
88
use Patchlevel\EventSourcing\Attribute\Aggregate;
99
use Patchlevel\EventSourcing\Attribute\Apply;
1010
use Patchlevel\EventSourcing\Attribute\Id;
11+
use Patchlevel\EventSourcing\Attribute\SharedApplyContext;
1112
use Patchlevel\EventSourcing\Attribute\Snapshot;
12-
use Patchlevel\EventSourcing\Attribute\SuppressMissingApply;
13-
use Patchlevel\EventSourcing\Tests\Integration\MicroAggregate\Events\NameChanged;
1413
use Patchlevel\EventSourcing\Tests\Integration\MicroAggregate\Events\ProfileCreated;
1514

1615
#[Aggregate('profile')]
1716
#[Snapshot('default', 1)]
18-
#[SuppressMissingApply([NameChanged::class])]
17+
#[SharedApplyContext([PersonalInformation::class])]
1918
final class Profile extends BasicAggregateRoot
2019
{
2120
#[Id]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Unit\Fixture;
6+
7+
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
8+
use Patchlevel\EventSourcing\Attribute\Aggregate;
9+
use Patchlevel\EventSourcing\Attribute\Apply;
10+
11+
#[Aggregate('other')]
12+
final class OtherAggregate extends BasicAggregateRoot
13+
{
14+
#[Apply(ProfileCreated::class)]
15+
public function applyProfileCreated(): void
16+
{
17+
}
18+
}

tests/Unit/Fixture/Profile.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,7 @@ public function splitIt(): void
6666
$this->recordThat(new SplittingEvent($this->email, $this->visits));
6767
}
6868

69-
#[Apply(ProfileCreated::class)]
70-
#[Apply(ProfileVisited::class)]
69+
#[Apply]
7170
protected function applyProfileCreated(ProfileCreated|ProfileVisited $event): void
7271
{
7372
if ($event instanceof ProfileCreated) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\EventSourcing\Tests\Unit\Fixture;
6+
7+
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
8+
use Patchlevel\EventSourcing\Attribute\Aggregate;
9+
use Patchlevel\EventSourcing\Attribute\Id;
10+
use Patchlevel\EventSourcing\Attribute\SharedApplyContext;
11+
12+
#[Aggregate('profile_shared_context')]
13+
#[SharedApplyContext([OtherAggregate::class])]
14+
final class ProfileWithSharedApplyContext extends BasicAggregateRoot
15+
{
16+
#[Id]
17+
private ProfileId $id;
18+
}

0 commit comments

Comments
 (0)