[Event Sourcing] Making Progress

This commit is contained in:
Carlos Buenosvinos
2022-04-06 23:21:25 +02:00
parent 307dd4419b
commit 0193a8e1ed
8 changed files with 433 additions and 49 deletions

View File

@@ -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>

View File

@@ -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

View 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

View 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

View 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

View 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;
}
}

View File

@@ -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

View File

@@ -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