Skip to content

Commit

Permalink
POC: aggregate id
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidBadura committed Dec 15, 2023
1 parent 320a4e3 commit 99b25ae
Show file tree
Hide file tree
Showing 37 changed files with 193 additions and 124 deletions.
18 changes: 18 additions & 0 deletions src/Aggregate/AggregateIdNotFound.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Aggregate;

use RuntimeException;

use function sprintf;

final class AggregateIdNotFound extends RuntimeException
{
/** @param class-string $className */
public function __construct(string $className)
{
parent::__construct(sprintf('class %s has no property marked as aggregate id', $className));
}
}
2 changes: 1 addition & 1 deletion src/Aggregate/AggregateRoot.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

interface AggregateRoot
{
public function aggregateRootId(): string;
public function aggregateRootId(): AggregateRootId;

/** @param iterable<object> $events */
public function catchUp(iterable $events): void;
Expand Down
24 changes: 24 additions & 0 deletions src/Aggregate/AggregateRootAttributeBehaviour.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@

namespace Patchlevel\EventSourcing\Aggregate;

use ReflectionProperty;

use function array_key_exists;

trait AggregateRootAttributeBehaviour
{
use AggregateRootBehaviour;
use AggregateRootMetadataAwareBehaviour;

private AggregateRootId|null $_aggregateRootId = null;

protected function apply(object $event): void
{
$metadata = static::metadata();
Expand All @@ -26,4 +30,24 @@ protected function apply(object $event): void
$method = $metadata->applyMethods[$event::class];
$this->$method($event);
}

public function aggregateRootId(): AggregateRootId
{
if ($this->_aggregateRootId instanceof AggregateRootId) {
return $this->_aggregateRootId;
}

$metadata = static::metadata();

$reflection = new ReflectionProperty($this, $metadata->idProperty);

/** @var mixed $aggregateId */
$aggregateId = $reflection->getValue($this);

if (!$aggregateId instanceof AggregateRootId) {
throw new AggregateIdNotFound($this::class);
}

return $this->_aggregateRootId = $aggregateId;
}
}
10 changes: 10 additions & 0 deletions src/Aggregate/AggregateRootId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Aggregate;

interface AggregateRootId
{
public function toString(): string;
}
18 changes: 18 additions & 0 deletions src/Aggregate/BasicAggregateRootId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Aggregate;

final class BasicAggregateRootId implements AggregateRootId
{
public function __construct(
private readonly string $id,
) {
}

public function toString(): string
{
return $this->id;
}
}
12 changes: 12 additions & 0 deletions src/Attribute/AggregateId.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
final class AggregateId
{
}
17 changes: 17 additions & 0 deletions src/Metadata/AggregateRoot/AggregateIdNotFound.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Metadata\AggregateRoot;

use RuntimeException;

use function sprintf;

final class AggregateIdNotFound extends RuntimeException
{
public function __construct(string $className)
{
parent::__construct(sprintf('class %s has no property marked as aggregate id', $className));
}
}
1 change: 1 addition & 0 deletions src/Metadata/AggregateRoot/AggregateRootMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public function __construct(
/** @var class-string<T> */
public readonly string $className,
public readonly string $name,
public readonly string $idProperty,
/** @var array<class-string, string> */
public readonly array $applyMethods,
/** @var array<class-string, true> */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\AggregateId;
use Patchlevel\EventSourcing\Attribute\Apply;
use Patchlevel\EventSourcing\Attribute\Snapshot as AttributeSnapshot;
use Patchlevel\EventSourcing\Attribute\SuppressMissingApply;
Expand Down Expand Up @@ -41,13 +42,15 @@ public function metadata(string $aggregate): AggregateRootMetadata
$reflector = new ReflectionClass($aggregate);

$aggregateName = $this->findAggregateName($reflector);
$idProperty = $this->findIdProperty($reflector);
[$suppressEvents, $suppressAll] = $this->findSuppressMissingApply($reflector);
$applyMethods = $this->findApplyMethods($reflector, $aggregate);
$snapshot = $this->findSnapshot($reflector);

$metadata = new AggregateRootMetadata(
$aggregate,
$aggregateName,
$idProperty,
$applyMethods,
$suppressEvents,
$suppressAll,
Expand Down Expand Up @@ -97,6 +100,23 @@ private function findAggregateName(ReflectionClass $reflector): string
return $aggregateAttribute->name();
}

private function findIdProperty(ReflectionClass $reflector): string
{
$properties = $reflector->getProperties();

foreach ($properties as $property) {
$attributes = $property->getAttributes(AggregateId::class);

if ($attributes === []) {
continue;
}

return $property->getName();
}

throw new AggregateIdNotFound($reflector->getName());
}

private function findSnapshot(ReflectionClass $reflector): Snapshot|null
{
$attributeReflectionList = $reflector->getAttributes(AttributeSnapshot::class);
Expand Down
5 changes: 3 additions & 2 deletions src/Repository/AggregateDetached.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
namespace Patchlevel\EventSourcing\Repository;

use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
use Patchlevel\EventSourcing\Aggregate\AggregateRootId;

use function sprintf;

final class AggregateDetached extends RepositoryException
{
/** @param class-string<AggregateRoot> $aggregateClass */
public function __construct(string $aggregateClass, string $aggregateId)
public function __construct(string $aggregateClass, AggregateRootId $aggregateId)
{
parent::__construct(
sprintf(
'An error occurred while saving the aggregate "%s" with the ID "%s", causing the uncommitted events to be lost. Please reload the aggregate.',
$aggregateClass,
$aggregateId,
$aggregateId->toString(),
),
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/Repository/AggregateNotFound.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

namespace Patchlevel\EventSourcing\Repository;

use Patchlevel\EventSourcing\Aggregate\AggregateRootId;

use function sprintf;

final class AggregateNotFound extends RepositoryException
{
public function __construct(string $aggregateClass, string $id)
public function __construct(string $aggregateClass, AggregateRootId $id)
{
parent::__construct(sprintf('aggregate "%s::%s" not found', $aggregateClass, $id));
parent::__construct(sprintf('aggregate "%s::%s" not found', $aggregateClass, $id->toString()));
}
}
5 changes: 3 additions & 2 deletions src/Repository/AggregateUnknown.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@
namespace Patchlevel\EventSourcing\Repository;

use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
use Patchlevel\EventSourcing\Aggregate\AggregateRootId;

use function sprintf;

final class AggregateUnknown extends RepositoryException
{
/** @param class-string<AggregateRoot> $aggregateClass */
public function __construct(string $aggregateClass, string $aggregateId)
public function __construct(string $aggregateClass, AggregateRootId $aggregateId)
{
parent::__construct(
sprintf(
'The aggregate %s with the ID "%s" was not loaded from this repository. Please reload the aggregate.',
$aggregateClass,
$aggregateId,
$aggregateId->toString(),
),
);
}
Expand Down
19 changes: 10 additions & 9 deletions src/Repository/DefaultRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Patchlevel\EventSourcing\Repository;

use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
use Patchlevel\EventSourcing\Aggregate\AggregateRootId;
use Patchlevel\EventSourcing\Clock\SystemClock;
use Patchlevel\EventSourcing\EventBus\Decorator\MessageDecorator;
use Patchlevel\EventSourcing\EventBus\EventBus;
Expand Down Expand Up @@ -57,7 +58,7 @@ public function __construct(
}

/** @return T */
public function load(string $id): AggregateRoot
public function load(AggregateRootId $id): AggregateRoot
{
if ($this->snapshotStore && $this->metadata->snapshot) {
try {
Expand All @@ -69,23 +70,23 @@ public function load(string $id): AggregateRoot
sprintf(
'snapshot for aggregate "%s" with the id "%s" not found',
$this->metadata->className,
$id,
$id->toString(),
),
);
} catch (SnapshotVersionInvalid) {
$this->logger->debug(
sprintf(
'snapshot for aggregate "%s" with the id "%s" is invalid',
$this->metadata->className,
$id,
$id->toString(),
),
);
}
}

$criteria = (new CriteriaBuilder())
->aggregateClass($this->metadata->className)
->aggregateId($id)
->aggregateId($id->toString())
->archived(false)
->build();

Expand Down Expand Up @@ -117,11 +118,11 @@ public function load(string $id): AggregateRoot
return $aggregate;
}

public function has(string $id): bool
public function has(AggregateRootId $id): bool
{
$criteria = (new CriteriaBuilder())
->aggregateClass($this->metadata->className)
->aggregateId($id)
->aggregateId($id->toString())
->build();

return $this->store->count($criteria) > 0;
Expand Down Expand Up @@ -162,7 +163,7 @@ public function save(AggregateRoot $aggregate): void
static function (object $event) use ($aggregate, &$playhead, $messageDecorator, $clock) {
$message = Message::create($event)
->withAggregateClass($aggregate::class)
->withAggregateId($aggregate->aggregateRootId())
->withAggregateId($aggregate->aggregateRootId()->toString())
->withPlayhead(++$playhead)
->withRecordedOn($clock->now());

Expand Down Expand Up @@ -194,15 +195,15 @@ static function (object $event) use ($aggregate, &$playhead, $messageDecorator,
*
* @return T
*/
private function loadFromSnapshot(string $aggregateClass, string $id): AggregateRoot
private function loadFromSnapshot(string $aggregateClass, AggregateRootId $id): AggregateRoot
{
assert($this->snapshotStore instanceof SnapshotStore);

$aggregate = $this->snapshotStore->load($aggregateClass, $id);

$criteria = (new CriteriaBuilder())
->aggregateClass($this->metadata->className)
->aggregateId($id)
->aggregateId($id->toString())
->fromPlayhead($aggregate->playhead())
->build();

Expand Down
6 changes: 4 additions & 2 deletions src/Repository/PlayheadMismatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@

namespace Patchlevel\EventSourcing\Repository;

use Patchlevel\EventSourcing\Aggregate\AggregateRootId;

use function sprintf;

final class PlayheadMismatch extends RepositoryException
{
public function __construct(string $aggregateClass, string $aggregateId, int $playhead, int $eventCount)
public function __construct(string $aggregateClass, AggregateRootId $aggregateId, int $playhead, int $eventCount)
{
parent::__construct(sprintf(
'There is a mismatch between the playhead [%s] and the event count [%s] for the aggregate [%s] with the id [%s]',
$playhead,
$eventCount,
$aggregateClass,
$aggregateId,
$aggregateId->toString(),
));
}
}
5 changes: 3 additions & 2 deletions src/Repository/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
namespace Patchlevel\EventSourcing\Repository;

use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
use Patchlevel\EventSourcing\Aggregate\AggregateRootId;

/** @template T of AggregateRoot */
interface Repository
{
/** @return T */
public function load(string $id): AggregateRoot;
public function load(AggregateRootId $id): AggregateRoot;

public function has(string $id): bool;
public function has(AggregateRootId $id): bool;

/** @param T $aggregate */
public function save(AggregateRoot $aggregate): void;
Expand Down
Loading

0 comments on commit 99b25ae

Please sign in to comment.