[Event Sourcing] Making Progress
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Cheeper\Chapter7\DomainModel\EventStore\Event" table="events">
|
||||
<id name="aggregateId" type="string" column="aggregate_id" length="36">
|
||||
<generator strategy="NONE"/>
|
||||
</id>
|
||||
|
||||
<field name="content" column="content" type="text"/>
|
||||
<field name="occurredOn" column="occurred_on" type="timestamp" nullable="true" />
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Chapter9\DomainModel;
|
||||
|
||||
//snippet aggregate-root
|
||||
class AggregateRoot
|
||||
{
|
||||
/** @var DomainEvent[] */
|
||||
private array $recordedEvents = [];
|
||||
|
||||
protected function recordApplyAndPublishThat(DomainEvent $event): void {
|
||||
$this->recordThat($event);
|
||||
$this->applyThat($event);
|
||||
$this->publishThat($event);
|
||||
}
|
||||
|
||||
protected function recordThat(DomainEvent $event): void
|
||||
{
|
||||
$this->recordedEvents[] = $event;
|
||||
}
|
||||
|
||||
protected function applyThat(DomainEvent $event): void
|
||||
{
|
||||
$className = (new \ReflectionClass($event))->getShortName();
|
||||
|
||||
$modifier = 'apply' . $className;
|
||||
|
||||
$this->$modifier($event);
|
||||
}
|
||||
|
||||
protected function publishThat(DomainEvent $event): void
|
||||
{
|
||||
DomainEventPublisher::instance()->publish($event);
|
||||
}
|
||||
|
||||
/** @return DomainEvent[] */
|
||||
public function recordedEvents(): array
|
||||
{
|
||||
return $this->recordedEvents;
|
||||
}
|
||||
|
||||
public function clearEvents(): void
|
||||
{
|
||||
$this->recordedEvents = [];
|
||||
}
|
||||
}
|
||||
//end-snippet
|
||||
109
src/Cheeper/Chapter9/DomainModel/Author/Author.php
Normal file
109
src/Cheeper/Chapter9/DomainModel/Author/Author.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Chapter9\DomainModel\Author;
|
||||
|
||||
use Cheeper\AllChapters\DomainModel\Author\AuthorId;
|
||||
use Cheeper\AllChapters\DomainModel\Author\BirthDate;
|
||||
use Cheeper\AllChapters\DomainModel\Author\EmailAddress;
|
||||
use Cheeper\AllChapters\DomainModel\Author\UserName;
|
||||
use Cheeper\AllChapters\DomainModel\Author\Website;
|
||||
use Cheeper\Chapter7\DomainModel\DomainEvent;
|
||||
use Cheeper\Chapter9\DomainModel\EventSourcedTrait;
|
||||
use Cheeper\Chapter9\DomainModel\EventStream;
|
||||
use DateTimeImmutable;
|
||||
use ReflectionClass;
|
||||
|
||||
// snippet code
|
||||
final class Author
|
||||
{
|
||||
use EventSourcedTrait;
|
||||
|
||||
private string $authorId;
|
||||
private string $userName;
|
||||
private string $email;
|
||||
private ?string $name = null;
|
||||
private ?string $biography = null;
|
||||
private ?string $location = null;
|
||||
private ?string $website = null;
|
||||
private ?DateTimeImmutable $birthDate = null;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
// We need an empty constructor
|
||||
// or an alternative way of instantiating
|
||||
// an empty instance of this object
|
||||
}
|
||||
|
||||
public static function signUp(
|
||||
AuthorId $authorId,
|
||||
UserName $userName,
|
||||
EmailAddress $email,
|
||||
?string $name = null,
|
||||
?string $biography = null,
|
||||
?string $location = null,
|
||||
?Website $website = null,
|
||||
?BirthDate $birthDate = null
|
||||
): static {
|
||||
// Regular semantic constructors
|
||||
// still apply as a proper design
|
||||
$obj = new static();
|
||||
|
||||
$obj->authorId = $authorId->toString();
|
||||
$obj->userName = $userName->userName();
|
||||
$obj->email = $email->value();
|
||||
$obj->name = $name;
|
||||
$obj->biography = $biography;
|
||||
$obj->location = $location;
|
||||
$obj->website = $website?->toString();
|
||||
$obj->birthDate = $birthDate?->date();
|
||||
|
||||
//
|
||||
$this->notifyDomainEvent(
|
||||
$this->buildNewAuthorSignedDomainEvent()
|
||||
);
|
||||
}
|
||||
|
||||
public static function reconstitute(EventStream $history): self
|
||||
{
|
||||
$aggregate = new Author();
|
||||
$aggregate->replay($history);
|
||||
|
||||
return $aggregate;
|
||||
}
|
||||
|
||||
public function replay(EventStream $history): void
|
||||
{
|
||||
foreach ($history as $event) {
|
||||
$this->applyThat($event);
|
||||
}
|
||||
}
|
||||
|
||||
protected function applyThat(DomainEvent $event): void
|
||||
{
|
||||
$className = (new ReflectionClass($event))->getShortName();
|
||||
$modifier = 'apply' . $className;
|
||||
$this->$modifier($event);
|
||||
}
|
||||
|
||||
protected function applyNewAuthorSigned(NewAuthorSigned $event)
|
||||
{
|
||||
$this->authorId = $event->authorId();
|
||||
$this->userName = $event->authorUsername();
|
||||
$this->email = $event->authorEmail();
|
||||
$this->name = $event->authorName();
|
||||
$this->biography = $event->authorBiography();
|
||||
$this->location = $event->authorLocation();
|
||||
$this->website = $event->authorWebsite();
|
||||
$this->birthDate = $event->authorBirthDate();
|
||||
}
|
||||
|
||||
protected function applyAuthorEmailChangedSigned(AuthorEmailChanged $event)
|
||||
{
|
||||
$this->email = $event->authorEmail();
|
||||
}
|
||||
|
||||
//...
|
||||
}
|
||||
// end-snippet
|
||||
90
src/Cheeper/Chapter9/DomainModel/Author/NewAuthorSigned.php
Normal file
90
src/Cheeper/Chapter9/DomainModel/Author/NewAuthorSigned.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Chapter9\DomainModel\Author;
|
||||
|
||||
use Cheeper\AllChapters\DomainModel\Clock;
|
||||
use Cheeper\Chapter7\Application\MessageTrait;
|
||||
use Cheeper\Chapter7\DomainModel\DomainEvent;
|
||||
use DateTimeImmutable;
|
||||
|
||||
// snippet code
|
||||
class NewAuthorSigned implements DomainEvent
|
||||
{
|
||||
use MessageTrait;
|
||||
|
||||
private function __construct(
|
||||
private string $authorId,
|
||||
private string $authorUsername,
|
||||
private string $authorEmail,
|
||||
private ?string $authorName = null,
|
||||
private ?string $authorBiography = null,
|
||||
private ?string $authorLocation = null,
|
||||
private ?string $authorWebsite = null,
|
||||
private ?DateTimeImmutable $authorBirthDate = null,
|
||||
private DateTimeImmutable $occurredOn
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromAuthor(Author $author): static
|
||||
{
|
||||
return new static(
|
||||
$author->authorId()->toString(),
|
||||
$author->userName()->toString(),
|
||||
$author->email()->value(),
|
||||
$author->name(),
|
||||
$author->biography(),
|
||||
$author->location(),
|
||||
$author->website(),
|
||||
$author->birthDate()->date(),
|
||||
Clock::instance()->now()
|
||||
);
|
||||
}
|
||||
|
||||
public function authorId(): string
|
||||
{
|
||||
return $this->authorId;
|
||||
}
|
||||
|
||||
public function authorUsername(): string
|
||||
{
|
||||
return $this->authorUsername;
|
||||
}
|
||||
|
||||
public function authorEmail(): string
|
||||
{
|
||||
return $this->authorEmail;
|
||||
}
|
||||
|
||||
public function authorName(): ?string
|
||||
{
|
||||
return $this->authorName;
|
||||
}
|
||||
|
||||
public function authorBiography(): ?string
|
||||
{
|
||||
return $this->authorBiography;
|
||||
}
|
||||
|
||||
public function authorLocation(): ?string
|
||||
{
|
||||
return $this->authorLocation;
|
||||
}
|
||||
|
||||
public function authorWebsite(): ?string
|
||||
{
|
||||
return $this->authorWebsite;
|
||||
}
|
||||
|
||||
public function authorBirthDate(): ?DateTimeImmutable
|
||||
{
|
||||
return $this->authorBirthDate;
|
||||
}
|
||||
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
}
|
||||
// end-snippet
|
||||
48
src/Cheeper/Chapter9/DomainModel/EventSourcedTrait.php
Normal file
48
src/Cheeper/Chapter9/DomainModel/EventSourcedTrait.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Chapter9\DomainModel;
|
||||
|
||||
use Cheeper\Chapter7\DomainModel\DomainEvent;
|
||||
use ReflectionClass;
|
||||
|
||||
// snippet code
|
||||
trait EventSourcedTrait
|
||||
{
|
||||
/** @var DomainEvent[] */
|
||||
private array $domainEvents = [];
|
||||
|
||||
/** @return DomainEvent[] */
|
||||
public function domainEvents(): array
|
||||
{
|
||||
return $this->domainEvents;
|
||||
}
|
||||
|
||||
public function notifyDomainEvent(DomainEvent $domainEvent): void
|
||||
{
|
||||
$this->domainEvents[] = $domainEvent;
|
||||
}
|
||||
|
||||
public function resetDomainEvent(): void
|
||||
{
|
||||
$this->domainEvents = [];
|
||||
}
|
||||
|
||||
protected function applyThat(DomainEvent $event): void
|
||||
{
|
||||
$className = (new ReflectionClass($event))->getShortName();
|
||||
|
||||
$modifier = 'apply' . $className;
|
||||
|
||||
$this->$modifier($event);
|
||||
}
|
||||
|
||||
public function replay(EventStream $history): void
|
||||
{
|
||||
foreach ($history as $event) {
|
||||
$this->applyThat($event);
|
||||
}
|
||||
}
|
||||
}
|
||||
// end-snippet
|
||||
54
src/Cheeper/Chapter9/DomainModel/EventStream.php
Normal file
54
src/Cheeper/Chapter9/DomainModel/EventStream.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Cheeper\Chapter9\DomainModel;
|
||||
|
||||
use Cheeper\Chapter7\DomainModel\DomainEvent;
|
||||
|
||||
class EventStream implements \Iterator
|
||||
{
|
||||
private string $aggregateId;
|
||||
/** @var DomainEvent[] */
|
||||
private array $events;
|
||||
|
||||
/**
|
||||
* @param string $aggregateId
|
||||
* @param DomainEvent[] $events
|
||||
*/
|
||||
public function __construct(string $aggregateId, array $events)
|
||||
{
|
||||
$this->aggregateId = $aggregateId;
|
||||
$this->events = $events;
|
||||
}
|
||||
|
||||
public function getAggregateId(): string
|
||||
{
|
||||
return $this->aggregateId;
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
reset($this->events);
|
||||
}
|
||||
|
||||
public function current(): mixed
|
||||
{
|
||||
return current($this->events);
|
||||
}
|
||||
|
||||
public function key(): mixed
|
||||
{
|
||||
return key($this->events);
|
||||
}
|
||||
|
||||
public function next(): void
|
||||
{
|
||||
next($this->events);
|
||||
}
|
||||
|
||||
public function valid(): bool
|
||||
{
|
||||
return key($this->events) !== null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\ES\Infrastructure;
|
||||
|
||||
use Zumba\JsonSerializer\JsonSerializer;
|
||||
|
||||
use Architecture\CQRS\Domain\DomainEvent;
|
||||
use Architecture\ES\Domain\EventStream;
|
||||
use Predis\Client;
|
||||
|
||||
//snippet event-store
|
||||
class EventStore
|
||||
{
|
||||
private Client $redis;
|
||||
private JsonSerializer $serializer;
|
||||
|
||||
public function __construct(Client $redis, JsonSerializer $serializer)
|
||||
{
|
||||
$this->redis = $redis;
|
||||
$this->serializer = $serializer;
|
||||
}
|
||||
|
||||
public function append(EventStream $eventstream): void
|
||||
{
|
||||
/** @var DomainEvent */
|
||||
foreach ($eventstream as $event) {
|
||||
$data = $this->serializer->serialize($event);
|
||||
|
||||
$date = (new \DateTimeImmutable())->format('YmdHis');
|
||||
|
||||
$this->redis->rpush(
|
||||
'events:' . $eventstream->getAggregateId(),
|
||||
$this->serializer->serialize([
|
||||
'type' => get_class($event),
|
||||
'created_on' => $date,
|
||||
'data' => $data
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function getEventsFor(string $id): EventStream
|
||||
{
|
||||
return $this->fromVersion($id, 0);
|
||||
}
|
||||
|
||||
public function fromVersion(string $id, int $version): EventStream
|
||||
{
|
||||
$serializedEvents = (array) $this->redis->lrange(
|
||||
'events:' . $id,
|
||||
$version,
|
||||
-1
|
||||
);
|
||||
|
||||
/** @var DomainEvent[] */
|
||||
$events = [];
|
||||
|
||||
/** @var string */
|
||||
foreach ($serializedEvents as $serializedEvent) {
|
||||
$event = (array) $this->serializer->unserialize($serializedEvent);
|
||||
|
||||
$eventData = (string) $event['data'];
|
||||
|
||||
/** @var DomainEvent */
|
||||
$events[] = $this->serializer->unserialize($eventData);
|
||||
}
|
||||
|
||||
return new EventStream($id, $events);
|
||||
}
|
||||
|
||||
public function countEventsFor(string $id): int
|
||||
{
|
||||
return (int) $this->redis->llen('events:' . $id);
|
||||
}
|
||||
}
|
||||
//end-snippet
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Architecture\ES\Infrastructure\V0;
|
||||
|
||||
use Architecture\CQRS\Domain\PostId;
|
||||
use Architecture\CQRS\Infrastructure\Projection\Projector;
|
||||
use Architecture\ES\Domain\Post;
|
||||
use Architecture\ES\Domain\PostRepository;
|
||||
use Architecture\ES\Domain\EventStream;
|
||||
use Architecture\ES\Infrastructure\EventStore;
|
||||
|
||||
//snippet event-store-post-repository
|
||||
class EventStorePostRepository implements PostRepository
|
||||
{
|
||||
private EventStore $eventStore;
|
||||
private Projector $projector;
|
||||
|
||||
public function __construct(EventStore $eventStore, Projector $projector)
|
||||
{
|
||||
$this->eventStore = $eventStore;
|
||||
$this->projector = $projector;
|
||||
}
|
||||
|
||||
public function save(Post $post): void
|
||||
{
|
||||
$events = $post->recordedEvents();
|
||||
|
||||
$this->eventStore->append(new EventStream($post->id()->id(), $events));
|
||||
$post->clearEvents();
|
||||
|
||||
$this->projector->project($events);
|
||||
}
|
||||
|
||||
public function byId(PostId $id): Post
|
||||
{
|
||||
/** @var Post */
|
||||
return Post::reconstitute(
|
||||
$this->eventStore->getEventsFor($id->id())
|
||||
);
|
||||
}
|
||||
}
|
||||
//end-snippet
|
||||
Reference in New Issue
Block a user