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
514 changes: 249 additions & 265 deletions README.md

Large diffs are not rendered by default.

13 changes: 6 additions & 7 deletions src/Aggregate/AggregateRoot.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,17 @@ interface AggregateRoot extends Entity
*
* @return SequenceNumber The current sequence number.
*/
public function getSequenceNumber(): SequenceNumber;
public function sequenceNumber(): SequenceNumber;

/**
* Returns the schema version of this aggregate type.
*
* <p>Resolved from the protected <code>modelVersion()</code> method, defaults to <code>0</code>
* when the method is not overridden. Used by consumers to migrate aggregate schemas when loading older
* persisted state.</p>
* <p>Defaults to <code>ModelVersion::initial()</code> (value 0) when not overridden. Used by consumers
* to migrate aggregate schemas when loading older persisted state.</p>
*
* @return SequenceNumber The declared model version, or 0 when not overridden.
* @return ModelVersion The declared model version.
*/
public function getModelVersion(): SequenceNumber;
public function modelVersion(): ModelVersion;

/**
* Returns the short class name of this aggregate.
Expand All @@ -60,5 +59,5 @@ public function getModelVersion(): SequenceNumber;
*
* @return string The short class name.
*/
public function buildAggregateName(): string;
public function aggregateName(): string;
}
40 changes: 20 additions & 20 deletions src/Aggregate/AggregateRootBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
use TinyBlocks\BuildingBlocks\Event\EventRecords;
use TinyBlocks\BuildingBlocks\Event\EventType;
use TinyBlocks\BuildingBlocks\Event\SequenceNumber;
use TinyBlocks\BuildingBlocks\Event\SnapshotData;
use TinyBlocks\BuildingBlocks\Snapshot\SnapshotData;
use TinyBlocks\Time\Instant;

trait AggregateRootBehavior
Expand All @@ -23,44 +23,44 @@ trait AggregateRootBehavior

private SequenceNumber $sequenceNumber;

public function getSequenceNumber(): SequenceNumber
public function sequenceNumber(): SequenceNumber
{
return $this->sequenceNumber ?? SequenceNumber::initial();
}

public function getModelVersion(): SequenceNumber
public function modelVersion(): ModelVersion
{
return SequenceNumber::of(value: $this->modelVersion());
return ModelVersion::initial();
}

public function buildAggregateName(): string
public function aggregateName(): string
{
return new ReflectionClass(static::class)->getShortName();
return new ReflectionClass(objectOrClass: static::class)->getShortName();
Comment thread
gustavofreze marked this conversation as resolved.
}

protected function modelVersion(): int
protected function nextSequenceNumber(): void
{
return 0;
$this->sequenceNumber = $this->sequenceNumber()->next();
}

protected function nextSequenceNumber(): void
protected function generateSnapshotData(): SnapshotData
{
$this->sequenceNumber = $this->getSequenceNumber()->next();
return new SnapshotData(payload: $this->snapshotState());
}

public function recordedEvents(): EventRecords
protected function snapshotState(): array
{
$records = $this->recordedEvents ?? EventRecords::createFromEmpty();
$state = get_object_vars($this);
unset($state['recordedEvents'], $state['sequenceNumber']);

return EventRecords::createFrom(elements: $records);
return $state;
}

protected function generateSnapshotData(): SnapshotData
public function recordedEvents(): EventRecords
{
$state = get_object_vars($this);
unset($state['recordedEvents']);
$records = $this->recordedEvents ?? EventRecords::createFromEmpty();

return new SnapshotData(payload: $state);
return EventRecords::createFrom(elements: $records);
}

protected function buildEventRecord(DomainEvent $event): EventRecord
Expand All @@ -69,12 +69,12 @@ protected function buildEventRecord(DomainEvent $event): EventRecord
id: Uuid::uuid4(),
type: EventType::fromEvent(event: $event),
event: $event,
identity: $this->getIdentity(),
identity: $this->identity(),
revision: $event->revision(),
occurredOn: Instant::now(),
snapshotData: $this->generateSnapshotData(),
aggregateType: $this->buildAggregateName(),
sequenceNumber: $this->getSequenceNumber()
aggregateType: $this->aggregateName(),
sequenceNumber: $this->sequenceNumber()
);
}
}
8 changes: 4 additions & 4 deletions src/Aggregate/EventSourcingRoot.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function recordedEvents(): EventRecords;
*
* @param Identity $identity The identity to assign to the new aggregate.
* @return static A new aggregate in its initial state.
* @throws MissingIdentityProperty When the property referenced by <code>identityName()</code> does not exist.
* @throws MissingIdentityProperty When the property referenced by <code>identityProperty()</code> does not exist.
*/
public static function blank(Identity $identity): static;

Expand All @@ -69,7 +69,7 @@ public static function blank(Identity $identity): static;
* @param iterable<EventRecord> $records The event stream to replay, ordered by sequence number.
* @param Snapshot|null $snapshot Optional snapshot to restore from before replay.
* @return static The reconstituted aggregate.
* @throws MissingIdentityProperty When the property referenced by <code>identityName()</code> does not exist.
* @throws MissingIdentityProperty When the property referenced by <code>identityProperty()</code> does not exist.
*/
public static function reconstitute(Identity $identity, iterable $records, ?Snapshot $snapshot = null): static;

Expand All @@ -83,12 +83,12 @@ public static function reconstitute(Identity $identity, iterable $records, ?Snap
*
* @return array<string, mixed> Keyed by property name.
*/
public function getSnapshotState(): array;
public function snapshotState(): array;

/**
* Restores aggregate state from the given snapshot.
*
* <p>Implementations read {@see Snapshot::getAggregateState()} and copy the relevant fields into
* <p>Implementations read {@see Snapshot::aggregateState()} and copy the relevant fields into
* their own properties. The sequence number is applied automatically by
* <code>reconstitute()</code>; implementations should not touch it.</p>
*
Expand Down
4 changes: 2 additions & 2 deletions src/Aggregate/EventSourcingRootBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public static function reconstitute(

if (!is_null($snapshot)) {
$aggregate->applySnapshot(snapshot: $snapshot);
$aggregate->sequenceNumber = $snapshot->getSequenceNumber();
$aggregate->sequenceNumber = $snapshot->sequenceNumber();
}

foreach ($records as $record) {
Expand All @@ -54,7 +54,7 @@ public function eventHandlers(): array
return [];
}

public function getSnapshotState(): array
public function snapshotState(): array
{
$state = get_object_vars($this);
unset($state['recordedEvents'], $state['sequenceNumber']);
Expand Down
21 changes: 10 additions & 11 deletions src/Aggregate/EventualAggregateRoot.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@
* Aggregate root variant that records domain events for eventual publication via transactional outbox.
*
* <p>State is persisted as the source of truth; events are emitted as side effects and delivered
* at-least-once to external consumers. The repository is expected to drain
* <code>recordedEvents()</code> after persisting the aggregate state and then call
* <code>clearRecordedEvents()</code> to reset the buffer for the next unit of work.</p>
* at-least-once to external consumers. The repository drains <code>recordedEvents()</code> after
* persisting the aggregate state.</p>
*
* <p><strong>Use-once contract:</strong> the recorded-events buffer is never cleared. After the
* repository drains <code>recordedEvents()</code> and persists the records to the outbox, the aggregate
* instance must be discarded. Re-saving the same instance attempts to push the same envelopes again and
* fails with a duplicate-event error from the outbox. Applications that need to perform multiple
* operations on the same logical aggregate within one process must reload from the repository between
* operations.</p>
*
* <p>Sibling of {@see EventSourcingRoot}, not a parent. Outbox and event sourcing are mutually exclusive
* persistence strategies: an aggregate either persists its state and emits events as side effects, or
Expand All @@ -25,19 +31,12 @@
interface EventualAggregateRoot extends AggregateRoot
{
/**
* Returns a copy of the events recorded since the last clear.
* Returns a copy of all events recorded since the aggregate was created.
*
* <p>Always returns a fresh copy: external mutation of the returned collection does not leak into the
* aggregate's internal buffer.</p>
*
* @return EventRecords A snapshot of the recorded events, safe to iterate and mutate.
*/
public function recordedEvents(): EventRecords;

/**
* Discards all recorded events.
*
* <p>Typically called by the repository after the events have been persisted to the outbox.</p>
*/
public function clearRecordedEvents(): void;
}
5 changes: 0 additions & 5 deletions src/Aggregate/EventualAggregateRootBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,6 @@ trait EventualAggregateRootBehavior
{
use AggregateRootBehavior;

public function clearRecordedEvents(): void
{
$this->recordedEvents = EventRecords::createFromEmpty();
}

protected function push(DomainEvent $event): void
{
$this->nextSequenceNumber();
Expand Down
31 changes: 31 additions & 0 deletions src/Aggregate/ModelVersion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace TinyBlocks\BuildingBlocks\Aggregate;

use TinyBlocks\BuildingBlocks\Internal\Exceptions\InvalidModelVersion;
use TinyBlocks\Vo\ValueObject;
use TinyBlocks\Vo\ValueObjectBehavior;

final readonly class ModelVersion implements ValueObject
{
use ValueObjectBehavior;

private function __construct(public int $value)
{
if ($value < 0) {
throw new InvalidModelVersion(value: $value);
}
}

public static function of(int $value): ModelVersion
{
return new ModelVersion(value: $value);
}

public static function initial(): ModelVersion
{
return new ModelVersion(value: 0);
}
}
2 changes: 1 addition & 1 deletion src/Entity/CompoundIdentity.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* unique (for example <code>(tenantId, appointmentId)</code> in multi-tenant contexts). Not a concept
* from Evans.</p>
*
* <p>All declared properties participate in the identity: <code>getIdentityValue()</code> returns them
* <p>All declared properties participate in the identity: <code>identityValue()</code> returns them
* as an associative array keyed by property name.</p>
*/
interface CompoundIdentity extends Identity
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/CompoundIdentityBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ trait CompoundIdentityBehavior
{
use ValueObjectBehavior;

public function getIdentityValue(): mixed
public function identityValue(): mixed
{
return get_object_vars($this);
}
Expand Down
14 changes: 7 additions & 7 deletions src/Entity/Entity.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* across distinct representations and lifecycle transitions. Two entities are equal when their
* identities are equal, regardless of attribute differences.</p>
*
* <p>Concrete entities implement the protected <code>identityName()</code> method returning the property
* <p>Concrete entities implement the protected <code>identityProperty()</code> method returning the property
* that holds their {@see Identity}. The default behavior uses reflection to resolve and compare it.</p>
*
* @see Eric Evans, <em>Domain-Driven Design: Tackling Complexity in the Heart of Software</em>
Expand All @@ -25,17 +25,17 @@ interface Entity
* Returns the Identity that uniquely identifies this entity.
*
* @return Identity The identity instance held by this entity.
* @throws MissingIdentityProperty When the property referenced by <code>identityName()</code> does not exist.
* @throws MissingIdentityProperty When the property referenced by <code>identityProperty()</code> does not exist.
*/
public function getIdentity(): Identity;
public function identity(): Identity;

/**
* Returns the name of the property that holds this entity's Identity.
*
* @return string The property name, resolved from <code>identityName()</code>.
* @throws MissingIdentityProperty When the property referenced by <code>identityName()</code> does not exist.
* @return string The property name, resolved from <code>identityProperty()</code>.
* @throws MissingIdentityProperty When the property referenced by <code>identityProperty()</code> does not exist.
*/
public function getIdentityName(): string;
public function identityName(): string;

/**
* Returns the raw value of this entity's identity.
Expand All @@ -45,7 +45,7 @@ public function getIdentityName(): string;
*
* @return mixed The raw identity value.
*/
public function getIdentityValue(): mixed;
public function identityValue(): mixed;

/**
* Checks whether this entity and the given one share the same identity.
Expand Down
26 changes: 13 additions & 13 deletions src/Entity/EntityBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,9 @@

trait EntityBehavior
{
protected function identityName(): string
public function identityName(): string
{
return 'id';
}

public function getIdentityName(): string
{
$name = $this->identityName();
$name = $this->identityProperty();

if (!property_exists($this, $name)) {
throw new MissingIdentityProperty(className: static::class, propertyName: $name);
Expand All @@ -24,23 +19,28 @@ public function getIdentityName(): string
return $name;
}

public function getIdentity(): Identity
protected function identityProperty(): string
{
return 'id';
}

public function identity(): Identity
{
return $this->{$this->getIdentityName()};
return $this->{$this->identityName()};
}

public function getIdentityValue(): mixed
public function identityValue(): mixed
{
return $this->getIdentity()->getIdentityValue();
return $this->identity()->identityValue();
}

public function sameIdentityOf(Entity $other): bool
{
return $this->identityEquals(other: $other->getIdentity());
return $this->identityEquals(other: $other->identity());
}

public function identityEquals(Identity $other): bool
{
return $this->getIdentity()->equals(other: $other);
return $this->identity()->equals(other: $other);
}
}
2 changes: 1 addition & 1 deletion src/Entity/Identity.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ interface Identity extends ValueObject
*
* @return mixed A scalar value for single-field identities, an associative array for composite ones.
*/
public function getIdentityValue(): mixed;
public function identityValue(): mixed;
}
2 changes: 1 addition & 1 deletion src/Entity/SingleIdentity.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* single-value and composite identities.</p>
*
* <p>Implementations should declare exactly one property holding the scalar value; the default trait
* reads it by reflection and returns it from <code>getIdentityValue()</code>.</p>
* reads it by reflection and returns it from <code>identityValue()</code>.</p>
*/
interface SingleIdentity extends Identity
{
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/SingleIdentityBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ trait SingleIdentityBehavior
{
use ValueObjectBehavior;

public function getIdentityValue(): mixed
public function identityValue(): mixed
{
$properties = get_object_vars($this);

Expand Down
Loading