Skip to content
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
8 changes: 8 additions & 0 deletions docs/pages/UPGRADE-4.0.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Upgrade 4.0

## Aggregates

### Child Aggregate

We removed our experimental feature of child aggregates.
This was our first attempt to split aggregates into smaller parts,
but we found a better way to do this with the `Micro Aggregate` feature.

## Subscription

The constructor of the `DefaultSubscriptionEngine` class has been changed.
Expand Down
91 changes: 1 addition & 90 deletions docs/pages/aggregate.md
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ Or for test purposes the `FrozenClock`, which always returns the same time.

In some cases, it makes sense to split an aggregate into several smaller aggregates.
This can be the case if the aggregate becomes too large or if the aggregate is used in different contexts.
We currently support two patterns for this: Micro Aggregates and Child Aggregates (experimental).
For these cases you can use Micro Aggregates.

### Micro Aggregates

Expand Down Expand Up @@ -740,96 +740,7 @@ final class Shipping extends BasicAggregateRoot
}
}
```
### Child Aggregates

??? example "Experimental"

This feature is still experimental and may change in the future.
Use it with caution.

Another way to split an aggregate is to use child aggregates.
The difference to Micro Aggregates, child aggregates can only be accessed by the root aggregate
and are not separate aggregates.

In the following example, we have an `Order` aggregate that has a `Shipping` child aggregate.

```php
use Patchlevel\EventSourcing\Aggregate\BasicChildAggregate;
use Patchlevel\EventSourcing\Attribute\Apply;

final class Shipping extends BasicChildAggregate
{
private bool $arrived = false;

public function __construct(
private string $trackingId,
) {
}

public function arrive(): void
{
$this->recordThat(new Arrived());
}

#[Apply]
public function applyArrived(Arrived $event): void
{
$this->arrived = true;
}

public function isArrived(): bool
{
return $this->arrived;
}
}
```
!!! warning

The apply method must be public, otherwise the root aggregate cannot call it.

!!! note

Supress missing apply methods need to be defined in the root aggregate.

And the `Order` aggregate root looks like this:

```php
use Patchlevel\EventSourcing\Aggregate\BasicAggregateRoot;
use Patchlevel\EventSourcing\Aggregate\Uuid;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Apply;
use Patchlevel\EventSourcing\Attribute\ChildAggregate;
use Patchlevel\EventSourcing\Attribute\Id;

#[Aggregate('order')]
final class Order extends BasicAggregateRoot
{
#[Id]
private Uuid $id;

#[ChildAggregate]
private Shipping $shipping;

public static function create(Uuid $id, string $trackingId): static
{
$self = new static();
$self->recordThat(new OrderCreated($id, $trackingId));

return $self;
}

#[Apply]
public function applyOrderCreated(OrderCreated $event): void
{
$this->shipping = new Shipping($event->trackingId);
}

public function arrive(): void
{
$this->shipping->arrive();
}
}
```
## Aggregate Root Registry

The library needs to know about all aggregates so that the correct aggregate class is used to load from the database.
Expand Down
60 changes: 1 addition & 59 deletions src/Aggregate/AggregateRootAttributeBehaviour.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@
namespace Patchlevel\EventSourcing\Aggregate;

use Patchlevel\Hydrator\Attribute\Ignore;
use Patchlevel\Hydrator\Attribute\PostHydrate;
use ReflectionProperty;

use function array_key_exists;
use function count;
use function explode;

trait AggregateRootAttributeBehaviour
{
Expand All @@ -20,11 +17,7 @@
#[Ignore]
private AggregateRootId|null $cachedAggregateRootId = null;

/** @var (callable(object $event): void)|null */
#[Ignore]
private $recorder = null;

protected function apply(object $event): void

Check warning on line 20 in src/Aggregate/AggregateRootAttributeBehaviour.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "ProtectedVisibility": @@ @@ use AggregateRootMetadataAwareBehaviour; #[Ignore] private AggregateRootId|null $cachedAggregateRootId = null; - protected function apply(object $event): void + private function apply(object $event): void { $metadata = static::metadata(); if (!array_key_exists($event::class, $metadata->applyMethods)) {

Check warning on line 20 in src/Aggregate/AggregateRootAttributeBehaviour.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "ProtectedVisibility": @@ @@ use AggregateRootMetadataAwareBehaviour; #[Ignore] private AggregateRootId|null $cachedAggregateRootId = null; - protected function apply(object $event): void + private function apply(object $event): void { $metadata = static::metadata(); if (!array_key_exists($event::class, $metadata->applyMethods)) {
{
$metadata = static::metadata();

Expand All @@ -38,63 +31,12 @@

$method = $metadata->applyMethods[$event::class];

if ($metadata->childAggregates === []) {
$this->$method($event);

return;
}

$parts = explode('.', $method);

if (count($parts) === 2) {
[$property, $method] = $parts;

$child = $this->getChildAggregateByPropertyName($property);

if ($child !== null) {
$child->$method($event);
}
} else {
$this->$method($event);
}

$this->passRecorderToChildAggregates();
}

#[PostHydrate]
private function passRecorderToChildAggregates(): void
{
$metadata = static::metadata();
$this->recorder ??= $this->recordThat(...);

foreach ($metadata->childAggregates as $propertyName) {
$child = $this->getChildAggregateByPropertyName($propertyName);

if ($child === null) {
continue;
}

$child->setRecorder($this->recorder);
}
}

private function getChildAggregateByPropertyName(string $propertyName): ChildAggregate|null
{
$reflectionProperty = new ReflectionProperty($this::class, $propertyName);

if (!$reflectionProperty->isInitialized($this)) {
return null;
}

/** @var ChildAggregate|null $child */
$child = $reflectionProperty->getValue($this);

return $child;
$this->$method($event);
}

public function aggregateRootId(): AggregateRootId
{
if ($this->cachedAggregateRootId instanceof AggregateRootId) {

Check warning on line 39 in src/Aggregate/AggregateRootAttributeBehaviour.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "InstanceOf_": @@ @@ } public function aggregateRootId(): AggregateRootId { - if ($this->cachedAggregateRootId instanceof AggregateRootId) { + if (false) { return $this->cachedAggregateRootId; } $metadata = static::metadata();
return $this->cachedAggregateRootId;
}

Expand Down
14 changes: 0 additions & 14 deletions src/Aggregate/BasicChildAggregate.php

This file was deleted.

12 changes: 0 additions & 12 deletions src/Aggregate/ChildAggregate.php

This file was deleted.

26 changes: 0 additions & 26 deletions src/Aggregate/ChildAggregateBehaviour.php

This file was deleted.

13 changes: 0 additions & 13 deletions src/Attribute/ChildAggregate.php

This file was deleted.

2 changes: 0 additions & 2 deletions src/Metadata/AggregateRoot/AggregateRootMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ public function __construct(
public readonly array $suppressEvents,
public readonly bool $suppressAll,
public readonly Snapshot|null $snapshot,
/** @var list<string> */
public readonly array $childAggregates = [],
string|null $streamName = null,
) {
$this->streamName = $streamName ?? $this->name . '-{id}';
Expand Down
Loading
Loading