Skip to content
Open
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
6 changes: 6 additions & 0 deletions docs/aggregate.md
Original file line number Diff line number Diff line change
Expand Up @@ -944,10 +944,16 @@ use Patchlevel\EventSourcing\Metadata\AggregateRoot\AttributeAggregateRootRegist

$aggregateRegistry = (new AttributeAggregateRootRegistryFactory())->create([/* paths... */]);
```
:::tip
Scanning the paths on every request costs time. In production you can wrap the factory
in a [metadata cache](metadata-cache.md).
:::

## Learn more

* [How to create own aggregate id](aggregate-id.md)
* [How to store and load aggregates](repository.md)
* [How to snapshot aggregates](snapshots.md)
* [How to create Projections](subscription.md)
* [How to split streams](split-stream.md)
* [How to cache metadata](metadata-cache.md)
82 changes: 82 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ You can:
* Create and delete `databases`
* Create, update and delete `schemas`
* Manage `subscriptions`
* Inspect your `aggregates`, `events` and `subscribers`
* Migrate events from one `store` to another

## Database commands

Expand Down Expand Up @@ -53,6 +55,86 @@ The inspector is a tool to inspect the event streams.
* ShowAggregateCommand: `event-sourcing:show-aggregate`
* WatchCommand: `event-sourcing:watch`

## Debug command

The debug command prints everything the library knows about your application:
all registered aggregates, all registered events and all subscribers with their subscribe methods.
It is the fastest way to check if a class was picked up by the attribute scanning.

* DebugCommand: `event-sourcing:debug` (alias `debug:event-sourcing`)

```php
use Patchlevel\EventSourcing\Console\Command\DebugCommand;
use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
use Patchlevel\EventSourcing\Metadata\Event\EventRegistry;
use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberAccessorRepository;
use Symfony\Component\Console\Application;

/**
* @var Application $cli
* @var AggregateRootRegistry $aggregateRootRegistry
* @var EventRegistry $eventRegistry
* @var SubscriberAccessorRepository $subscriberAccessorRepository
*/
$cli->add(
new DebugCommand(
$aggregateRootRegistry,
$eventRegistry,
$subscriberAccessorRepository,
),
);
```
:::note
The subscriber repository is optional. If you don't pass it, the subscriber section is skipped.
:::

## Store migration command

The store migration command copies all events from one store into another one.
You need it when you switch the store implementation,
for example from the `DoctrineDbalStore` to the [StreamDoctrineDbalStore](store.md#streamdoctrinedbalstore).

* StoreMigrateCommand: `event-sourcing:store:migrate`

```php
use Patchlevel\EventSourcing\Console\Command\StoreMigrateCommand;
use Patchlevel\EventSourcing\Message\Translator\AggregateToStreamHeaderTranslator;
use Patchlevel\EventSourcing\Store\Store;
use Symfony\Component\Console\Application;

/**
* @var Application $cli
* @var Store $oldStore
* @var Store $newStore
*/
$cli->add(
new StoreMigrateCommand(
$oldStore,
$newStore,
[new AggregateToStreamHeaderTranslator()],
),
);
```
The third constructor argument is a list of [translators](message.md#translator)
that are applied to every message before it is written into the new store.
The `AggregateToStreamHeaderTranslator` converts the `AggregateHeader` into the stream based headers
and is what you need for a migration to the `StreamDoctrineDbalStore`.

Events are written in batches. You can control the batch size with the `buffer` option:

```bash
bin/console event-sourcing:store:migrate --buffer=5000
```
:::danger
The command writes into the target store, it does not clean it up first.
Make sure the target store is empty and create a backup before you run the migration.
:::

:::note
The schema of the new store has to exist before you run the command.
You can create it with the [schema commands](#schema-commands).
:::

## CLI example

A cli php file can look like this:
Expand Down
68 changes: 68 additions & 0 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,68 @@ The serializer needs the path information where the event classes are located
so that it can instantiate the correct classes.
Internally, an EventRegistry is used, which will be described later.

## Encoder

The serializer turns an event into an array first and then encodes that array into a string.
The encoding is done by an `Encoder`. By default the `JsonEncoder` is used, which encodes the payload as JSON.

If you want to change how the payload is encoded, you can pass your own encoder to the serializer.

```php
use Patchlevel\EventSourcing\Metadata\Event\AttributeEventRegistryFactory;
use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer;
use Patchlevel\EventSourcing\Serializer\Encoder\JsonEncoder;

$serializer = new DefaultEventSerializer(
(new AttributeEventRegistryFactory())->create(['src/Domain']),
encoder: new JsonEncoder(),
);
```
The `Encoder` interface has two methods:

```php
use Patchlevel\EventSourcing\Serializer\Encoder\Encoder;

final class MyEncoder implements Encoder
{
/**
* @param array<string, mixed> $data
* @param array<string, mixed> $options
*/
public function encode(array $data, array $options = []): string
{
// your encoding
}

/**
* @param array<string, mixed> $options
*
* @return array<string, mixed>
*/
public function decode(string $data, array $options = []): array
{
// your decoding
}
}
```
An `encode` call that fails must throw an `EncodeNotPossible` exception,
a failing `decode` call a `DecodeNotPossible` exception.

The options are passed through from the serializer. The `JsonEncoder` understands
`Encoder::OPTION_PRETTY_PRINT`, which is used by the [cli](cli.md) to print readable payloads.

```php
use Patchlevel\EventSourcing\Serializer\Encoder\Encoder;
use Patchlevel\EventSourcing\Serializer\EventSerializer;

/** @var EventSerializer $serializer */
$data = $serializer->serialize($event, [Encoder::OPTION_PRETTY_PRINT => true]);
```
:::warning
The encoder decides the format of everything that is already in your store.
If you change it, old events can no longer be decoded.
:::

## Normalizer

Sometimes you also want to add more complex data as a payload. For example DateTime or value objects.
Expand Down Expand Up @@ -140,10 +202,16 @@ use Patchlevel\EventSourcing\Metadata\Event\AttributeEventRegistryFactory;

$eventRegistry = (new AttributeEventRegistryFactory())->create([/* paths... */]);
```
:::tip
Scanning the paths on every request costs time. In production you can wrap the factory
in a [metadata cache](metadata-cache.md).
:::

## Learn more

* [How to normalize events](normalizer.md)
* [How to subscribe on events](subscription.md)
* [How to store events](store.md)
* [How to upcast events](upcasting.md)
* [How to use messages](message.md)
* [How to cache metadata](metadata-cache.md)
44 changes: 43 additions & 1 deletion docs/message.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,53 @@ $message->headers(); // [AggregateHeader object]
## Built-in headers

The message object has some built-in headers which are used internally.
Which of them you get depends on the [store](store.md) you use.

* `AggregateHeader` - Contains the aggregate name, aggregate id, playhead and recorded on.
These headers are set by every store:

* `IndexHeader` - The global position of the message in the store.
* `ArchivedHeader` - Flag if the message is archived.

The `DoctrineDbalStore` is aggregate based and adds:

* `AggregateHeader` - Contains the aggregate name, aggregate id, playhead and recorded on.
* `StreamStartHeader` - Flag if the message is the first message in a new stream.

The `StreamDoctrineDbalStore` is stream based and splits the same information into single headers:

* `StreamNameHeader` - The name of the stream, for example `profile-e3e3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e`.
* `PlayheadHeader` - The position of the message inside its stream.
* `RecordedOnHeader` - The point in time when the message was saved.
* `EventIdHeader` - The unique id of the event.

```php
use Patchlevel\EventSourcing\Message\Message;
use Patchlevel\EventSourcing\Store\Header\EventIdHeader;
use Patchlevel\EventSourcing\Store\Header\IndexHeader;
use Patchlevel\EventSourcing\Store\Header\PlayheadHeader;
use Patchlevel\EventSourcing\Store\Header\RecordedOnHeader;
use Patchlevel\EventSourcing\Store\Header\StreamNameHeader;

/** @var Message $message */
$message->header(IndexHeader::class)->index; // 42
$message->header(StreamNameHeader::class)->streamName; // 'profile-e3e3e3e3-...'
$message->header(PlayheadHeader::class)->playhead; // 2
$message->header(RecordedOnHeader::class)->recordedOn; // DateTimeImmutable
$message->header(EventIdHeader::class)->eventId; // 'a4a4a4a4-4a4a-...'
```
:::warning
The `PlayheadHeader` is only added if the stream is playhead based.
Streams that are written without a playhead, for example custom streams,
do not have this header. Use `hasHeader` before you access it.
:::

:::note
The `AggregateHeader` and the stream based headers never appear on the same message.
If you migrate from the `DoctrineDbalStore` to the `StreamDoctrineDbalStore`, you can convert
them with the `AggregateToStreamHeaderTranslator`, see the
[store migration command](cli.md#store-migration-command).
:::

## Custom headers

You can also add custom headers to the message object. For example, you can add an application id.
Expand Down
Loading
Loading