Skip to content

Commit

Permalink
Merge pull request #279 from patchlevel/finish-docs
Browse files Browse the repository at this point in the history
finish docs for 2.0
  • Loading branch information
DanielBadura authored Jun 29, 2022
2 parents 7c7ce80 + 0a25377 commit 81b8c80
Show file tree
Hide file tree
Showing 15 changed files with 587 additions and 150 deletions.
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ A lightweight but also all-inclusive event sourcing library with a focus on deve
* Everything is included in the package for event sourcing
* Based on [doctrine dbal](https://github.com/doctrine/dbal) and their ecosystem
* Developer experience oriented and fully typed
* [Snapshots](docs/snapshots.md) system to quickly rebuild the aggregates
* [Pipeline](docs/pipeline.md) to build new [projections](docs/projection.md) or to migrate events
* [Scheme management](docs/store.md) and [doctrine migration](docs/store.md) support
* Dev [tools](docs/tools.md) such as a realtime event watcher
* Built in [cli commands](docs/cli.md) with [symfony](https://symfony.com/)
* [Snapshots](https://patchlevel.github.io/event-sourcing-docs/latest/snapshots/) system to quickly rebuild the aggregates
* [Pipeline](https://patchlevel.github.io/event-sourcing-docs/latest/pipeline/) to build new [projections](https://patchlevel.github.io/event-sourcing-docs/latest/projection/) or to migrate events
* [Scheme management](https://patchlevel.github.io/event-sourcing-docs/latest/store/) and [doctrine migration](https://patchlevel.github.io/event-sourcing-docs/latest/migration/) support
* Dev [tools](https://patchlevel.github.io/event-sourcing-docs/latest/watch_server/) such as a realtime event watcher
* Built in [cli commands](https://patchlevel.github.io/event-sourcing-docs/latest/cli/) with [symfony](https://symfony.com/)

## Installation

Expand All @@ -26,7 +26,8 @@ composer require patchlevel/event-sourcing

## Documentation

* [Docs](https://patchlevel.github.io/event-sourcing-docs/latest)
* Latest [Docs](https://patchlevel.github.io/event-sourcing-docs/latest)
* 1.3 [Docs](https://github.com/patchlevel/event-sourcing/blob/1.3.x/README.md)

## Integration

Expand Down
118 changes: 91 additions & 27 deletions docs/pages/aggregate.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

[DDD Aggregate - Martin Flower](https://martinfowler.com/bliki/DDD_Aggregate.html)

An AggregateRoot has to inherit from `AggregateRoot` and need to implement the method `aggregateRootId`.
An Aggregate has to inherit from `AggregateRoot` and need to implement the method `aggregateRootId`.
`aggregateRootId` is the identifier from `AggregateRoot` like a primary key for an entity.
The events will be added later, but the following is enough to make it executable:

Expand All @@ -29,7 +29,7 @@ final class Profile extends AggregateRoot
return $this->id;
}

public static function create(string $id): self
public static function register(string $id): self
{
$self = new self();
// todo: record create event
Expand All @@ -43,13 +43,13 @@ final class Profile extends AggregateRoot

The aggregate is not yet finished and has only been built to the point that you can instantiate the object.

!!! note
!!! tip

An aggregateId can be an **uuid**, you can find more about this [here](./uuid.md).

We use a so-called named constructor here to create an object of the AggregateRoot.
The constructor itself is protected and cannot be called from outside.
But it is possible to define different named constructors for different use-cases like `createFromRegistration`.
But it is possible to define different named constructors for different use-cases like `import`.

After the basic structure for an aggregate is in place, it could theoretically be saved:

Expand All @@ -58,16 +58,13 @@ use Patchlevel\EventSourcing\Repository\Repository;

final class CreateProfileHandler
{
private Repository $profileRepository;

public function __construct(Repository $profileRepository)
{
$this->profileRepository = $profileRepository;
}
public function __construct(
private readonly Repository $profileRepository
) {}

public function __invoke(CreateProfile $command): void
{
$profile = Profile::create($command->id());
$profile = Profile::register($command->id());

$this->profileRepository->save($profile);
}
Expand All @@ -88,13 +85,13 @@ final class CreateProfileHandler
## Create a new aggregate

In order that an aggregate is actually saved, at least one event must exist in the DB.
For our aggregate we create the Event `ProfileCreated`:
For our aggregate we create the Event `ProfileRegistered`:

```php
use Patchlevel\EventSourcing\Attribute\Event;

#[Event('profile.created')]
final class ProfileCreated
#[Event('profile.registered')]
final class ProfileRegistered
{
public function __construct(
public readonly string $profileId,
Expand Down Expand Up @@ -130,16 +127,16 @@ final class Profile extends AggregateRoot
return $this->name;
}

public static function create(string $id, string $name): self
public static function register(string $id, string $name): self
{
$self = new self();
$self->recordThat(new ProfileCreated($id, $name));
$self->recordThat(new ProfileRegistered($id, $name));

return $self;
}

#[Apply]
protected function applyProfileCreated(ProfileCreated $event): void
protected function applyProfileRegistered(ProfileRegistered $event): void
{
$this->id = $event->profileId;
$this->name = $event->name;
Expand All @@ -151,13 +148,13 @@ final class Profile extends AggregateRoot

Prefixing the apply methods with "apply" improves readability.

In our named constructor `create` we have now created the event and recorded it with the method `record`.
The aggregate remembers all recorded events in order to save them later.
In our named constructor `register` we have now created the event and recorded it with the method `recordThat`.
The aggregate remembers all new recorded events in order to save them later.
At the same time, a defined apply method is executed directly so that we can change our state.

So that the AggregateRoot also knows which method it should call,
we have to provide it with the `Apply` [attributes](https://www.php.net/manual/en/language.attributes.overview.php).
We did that in the `applyProfileCreated` method.
we have to mark it with the `Apply` [attributes](https://www.php.net/manual/en/language.attributes.overview.php).
We did that in the `applyProfileRegistered` method.
In this method we change the `Profile` properties `id` and `name` with the transferred values.

### Modify an aggregate
Expand Down Expand Up @@ -206,10 +203,10 @@ final class Profile extends AggregateRoot
return $this->name;
}

public static function create(string $id, string $name): static
public static function register(string $id, string $name): static
{
$self = new static();
$self->recordThat(new ProfileCreated($id, $name));
$self->recordThat(new ProfileRegistered($id, $name));

return $self;
}
Expand All @@ -220,7 +217,7 @@ final class Profile extends AggregateRoot
}

#[Apply]
protected function applyProfileCreated(ProfileCreated $event): void
protected function applyProfileRegistered(ProfileRegistered $event): void
{
$this->id = $event->profileId;
$this->name = $event->name;
Expand Down Expand Up @@ -276,6 +273,7 @@ The `applyNameChanged` method was also called again internally to adjust the sta

When the `save` method is called on the repository,
all newly recorded events are then fetched and written to the database.
In this specific case only the `NameChanged` changed event.

## Multiple apply attributes on the same method

Expand Down Expand Up @@ -461,10 +459,10 @@ final class Profile extends AggregateRoot
private string $id;
private Name $name;

public static function create(string $id, Name $name): static
public static function register(string $id, Name $name): static
{
$self = new static();
$self->recordThat(new ProfileCreated($id, $name));
$self->recordThat(new ProfileRegistered($id, $name));

return $self;
}
Expand Down Expand Up @@ -528,7 +526,7 @@ use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Apply;
use Patchlevel\EventSourcing\Attribute\SuppressMissingApply;

#[Aggregate('profile')]
#[Aggregate('hotel')]
#[SuppressMissingApply([FullyBooked::class])]
final class Hotel extends AggregateRoot
{
Expand Down Expand Up @@ -559,6 +557,72 @@ final class Hotel extends AggregateRoot
}
```

## Working with dates

An aggregate should always be deterministic. In other words, whenever I execute methods on the aggregate,
I always get the same result. This also makes testing much easier.

But that often doesn't seem to be possible, e.g. if you want to save a createAt date.
But you can pass this information by yourself.

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

#[Aggregate('profile')]
final class Profile extends AggregateRoot
{
private string $id;
private Name $name;
private DateTimeImmutable $registeredAt;

public static function register(string $id, string $name, DateTimeImmutable $registeredAt): static
{
$self = new static();
$self->recordThat(new ProfileRegistered($id, $name, $registeredAt));

return $self;
}

// ...
}
```

But if you still want to make sure that the time is "now" and not in the past or future, you can pass a clock.

```php
use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
use Patchlevel\EventSourcing\Attribute\Aggregate;
use Patchlevel\EventSourcing\Attribute\Apply;
use Patchlevel\EventSourcing\Clock\Clock;

#[Aggregate('profile')]
final class Profile extends AggregateRoot
{
private string $id;
private Name $name;
private DateTimeImmutable $registeredAt;

public static function register(string $id, string $name, Clock $clock): static
{
$self = new static();
$self->recordThat(new ProfileRegistered($id, $name, $clock->now()));

return $self;
}

// ...
}
```

Now you can pass the `SystemClock` to determine the current time.
Or for test purposes the `FrozenClock`, which always returns the same time.

!!! note

You can find out more about clock [here](./clock.md).

## 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
11 changes: 11 additions & 0 deletions docs/pages/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ The creation, deletion and rebuilding of the projections is also possible via th

The [pipeline](./pipeline.md) will be used to rebuild the projection.

## Outbox commands

Interacting with the outbox store is also possible via the cli.

* OutboxInfoCommand: `event-sourcing:outbox:info`
* OutboxConsumeCommand: `event-sourcing:outbox:consume`

!!! note

You can find out more about outbox [here](outbox.md).

## CLI example

A cli php file can look like this:
Expand Down
62 changes: 48 additions & 14 deletions docs/pages/event_bus.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
This library uses the core principle called [event bus](https://martinfowler.com/articles/201701-event-driven.html).

For all events that are persisted (when the `save` method has been executed on the [repository](./repository.md)),
the event wrapped in a message will be dispatched to the `event bus`. All listeners are then called for each event/message.
the event wrapped in a message will be dispatched to the `event bus`. All listeners are then called for each
event/message.

## Message

Expand All @@ -15,27 +16,60 @@ A message contains the following information:
* playhead
* event
* recorded on
* custom headers

Each event is packed into a message and dispatched using the event bus.

```php
use Patchlevel\EventSourcing\Clock\SystemClock;
use Patchlevel\EventSourcing\EventBus\Message;

$event = new NameChanged('foo');
$message = new Message(
Profile::class, // aggregate class
'bca7576c-536f-4428-b694-7b1f00c714b7', // aggregate id
2, // playhead
$event // event object
);
$clock = SystemClock();
$message = Message::create(new NameChanged('foo'))
->withAggregateClass(Profile::class)
->withAggregateId('bca7576c-536f-4428-b694-7b1f00c714b7')
->withPlayhead(2)
->withRecordedOn($clock->now());

$eventBus->dispatch($message);
```

You don't have to create the message yourself,
it is automatically created, saved and dispatched in the repository.
!!! note

The message object is immutable.

You don't have to create the message yourself,
it is automatically created, saved and dispatched in the [repository](repository.md).

### Custom headers

You can also enrich your own header or metadata information.
This information is then accessible in the message object and is also stored in the database.

```php
use Patchlevel\EventSourcing\EventBus\Message;

$message = Message::create(new NameChanged('foo'))
// ...
->withCustomHeader('application-id', 'app');
```

!!! note

You can read about how to pass additional headers to the message object in the [message decorator](message_decorator.md) docs.

You can also access your custom headers.

```php
use Patchlevel\EventSourcing\EventBus\Message;

$message->customHeader('application-id'); // app
$message->customHeaders(); // ['application-id' => 'app']
```

## Event Bus

## Default event bus
### Default event bus

The library also delivers a light-weight event bus. This can only register listener and dispatch events.

Expand All @@ -53,10 +87,10 @@ $eventBus->addListener($projectionListener);
you can also add listeners after `ProjectionListener`
to access the [projections](./projection.md).

## Symfony event bus
### Symfony event bus

You can also use the [symfony message bus](https://symfony.com/doc/current/components/messenger.html)
which is much more powerful.
You can also use the [symfony message bus](https://symfony.com/doc/current/components/messenger.html)
which is much more powerful.

To use the optional symfony messenger you first have to `install` the packet.

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ final class ProfileCreated

So that the events can be saved in the database, they must be serialized and deserialized.
That's what the serializer is for.
The library comes with a `JsonSerializer` that can be given further instructions using attributes.
The library comes with a `DefaultEventSerializer` that can be given further instructions using attributes.

```php
use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;
Expand Down
Loading

0 comments on commit 81b8c80

Please sign in to comment.