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
81 changes: 81 additions & 0 deletions docs/UPGRADE-4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,87 @@ and replaced with the following headers:
* `Patchlevel\EventSourcing\Store\Header\PlayheadHeader`
* `Patchlevel\EventSourcing\Store\Header\RecordedOnHeader`

### Header registration

`Patchlevel\EventSourcing\Metadata\Message\MessageHeaderRegistryFactory::create()` now expects a
`Patchlevel\EventSourcing\Metadata\ClassLocator` instead of a list of paths.
Only the located headers are registered, the library headers are no longer added implicitly.

Before:

```php
use Patchlevel\EventSourcing\Metadata\Message\AttributeMessageHeaderRegistryFactory;

$registry = (new AttributeMessageHeaderRegistryFactory())->create(['src/Header']);
```
After:

```php
use Patchlevel\EventSourcing\Attribute\Header;
use Patchlevel\EventSourcing\Metadata\FilesystemClassLocator;
use Patchlevel\EventSourcing\Metadata\Message\AttributeMessageHeaderRegistryFactory;

$registry = (new AttributeMessageHeaderRegistryFactory())->create(
new FilesystemClassLocator(['src/Header'], Header::class),
);
```
`MessageHeaderRegistry::createWithInternalHeaders()` has been removed.
Use the `AttributeMessageHeaderRegistryFactory` with an `InMemoryClassLocator` instead.

Before:

```php
use Patchlevel\EventSourcing\Metadata\Message\MessageHeaderRegistry;

$registry = MessageHeaderRegistry::createWithInternalHeaders(['application' => ApplicationHeader::class]);
```
After:

```php
use Patchlevel\EventSourcing\Metadata\InMemoryClassLocator;
use Patchlevel\EventSourcing\Metadata\Message\AttributeMessageHeaderRegistryFactory;

$registry = (new AttributeMessageHeaderRegistryFactory())->create(
new InMemoryClassLocator([ApplicationHeader::class]),
);
```
Every located class must have a `#[Header]` attribute, otherwise a `ClassIsNotAHeader` exception is thrown.
Header names must be unique. If two located classes use the same name, a `HeaderAlreadyInRegistry` exception is thrown.

`DefaultHeadersSerializer::createDefault()` no longer registers any header.
The stores keep their own headers in separate columns, so they don't need them.
If you serialize the store headers yourself, register them with the locator of the store:
`Patchlevel\EventSourcing\Store\Header\StreamStoreHeaderLocator` or
`Patchlevel\EventSourcing\Store\Header\TaggableStoreHeaderLocator`.

### StreamStartHeader

`Patchlevel\EventSourcing\Store\StreamStartHeader` has been moved to
`Patchlevel\EventSourcing\Repository\MessageDecorator\StreamStartHeader`.
The header name `newStreamStart` is unchanged, so stored messages stay readable.

If you use the split stream feature, you need to register the header in the headers serializer of your store
with the `SplitStreamHeaderLocator`.
Keep it registered as long as your store contains messages with this header.

```php
use Doctrine\DBAL\Connection;
use Patchlevel\EventSourcing\Message\Serializer\DefaultHeadersSerializer;
use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamHeaderLocator;
use Patchlevel\EventSourcing\Serializer\EventSerializer;
use Patchlevel\EventSourcing\Store\StreamDoctrineDbalStore;

/**
* @var Connection $connection
* @var EventSerializer $eventSerializer
*/
$store = new StreamDoctrineDbalStore(
$connection,
$eventSerializer,
DefaultHeadersSerializer::createFromLocator(new SplitStreamHeaderLocator()),
);
```

### AggregateToStreamHeaderTranslator

`Patchlevel\EventSourcing\Message\Translator\AggregateToStreamHeaderTranslator` has been removed.
Expand Down
62 changes: 60 additions & 2 deletions docs/message.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ $message->headers(); // [StreamNameHeader object, PlayheadHeader object, ...]
```
## Built-in headers

The message object has some built-in headers which are used internally.
The library ships some headers which are set by the stores and the repository.

* `StreamNameHeader` - The name of the stream the message belongs to, in the format `[aggregateName]-[aggregateId]`.
* `PlayheadHeader` - The position of the message within its stream.
Expand All @@ -57,7 +57,7 @@ The message object has some built-in headers which are used internally.
* `IndexHeader` - The global position of the message in the store.
* `TagsHeader` - The tags attached to the message (experimental).
* `ArchivedHeader` - Flag if the message is archived.
* `StreamStartHeader` - Flag if the message is the first message in a new stream.
* `StreamStartHeader` - Flag if the message is the first message in a new stream, set by the [split stream](split-stream.md) feature.

```php
use Patchlevel\EventSourcing\Message\Message;
Expand Down Expand Up @@ -122,6 +122,64 @@ use Patchlevel\EventSourcing\Message\Message;
/** @var Message $message */
$message->header(ApplicationHeader::class);
```
### Register headers

The `DefaultHeadersSerializer` needs to know your header classes to resolve the header names.
The easiest way is to scan the directories where your headers are located.

```php
use Patchlevel\EventSourcing\Message\Serializer\DefaultHeadersSerializer;

$serializer = DefaultHeadersSerializer::createFromPaths(['src/Header']);
```
If you already know your header classes, or a library wants to provide its own headers,
you can pass a `ClassLocator` instead. The `InMemoryClassLocator` takes a list of classes,
the `ChainClassLocator` combines multiple locators.

```php
use Patchlevel\EventSourcing\Attribute\Header;
use Patchlevel\EventSourcing\Message\Serializer\DefaultHeadersSerializer;
use Patchlevel\EventSourcing\Metadata\ChainClassLocator;
use Patchlevel\EventSourcing\Metadata\FilesystemClassLocator;
use Patchlevel\EventSourcing\Metadata\InMemoryClassLocator;

$serializer = DefaultHeadersSerializer::createFromLocator(
new ChainClassLocator([
new FilesystemClassLocator(['src/Header'], Header::class),
new InMemoryClassLocator([ApplicationHeader::class]),
]),
);
```
The header name is always taken from the `#[Header]` attribute.
Only the located headers are registered, nothing is added implicitly.

The stores keep their own headers like `StreamNameHeader` or `PlayheadHeader` in separate columns,
so you don't need to register them for the store. If you serialize these headers yourself,
you can use the locator of the store, e.g. `StreamStoreHeaderLocator` or `TaggableStoreHeaderLocator`.
Features which add headers to the messages provide their own locator,
like the `SplitStreamHeaderLocator` for the [split stream](split-stream.md) feature.

```php
use Patchlevel\EventSourcing\Attribute\Header;
use Patchlevel\EventSourcing\Message\Serializer\DefaultHeadersSerializer;
use Patchlevel\EventSourcing\Metadata\ChainClassLocator;
use Patchlevel\EventSourcing\Metadata\FilesystemClassLocator;
use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamHeaderLocator;
use Patchlevel\EventSourcing\Store\Header\StreamStoreHeaderLocator;

$serializer = DefaultHeadersSerializer::createFromLocator(
new ChainClassLocator([
new StreamStoreHeaderLocator(),
new SplitStreamHeaderLocator(),
new FilesystemClassLocator(['src/Header'], Header::class),
]),
);
```
:::warning
Header names must be unique. If two located classes use the same name,
a `HeaderAlreadyInRegistry` exception is thrown.
:::

## Missing headers

When a message is deserialized, every header name is resolved to its registered header class.
Expand Down
34 changes: 34 additions & 0 deletions docs/split-stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,40 @@ $repositoryManager = new DefaultRepositoryManager(
You can find out more about the [message decorator](message-decorator.md).
:::

The decorator marks the first message of a new stream with the `StreamStartHeader`.
This header is stored with the message, so it must be registered in the headers serializer of your store.
The `SplitStreamHeaderLocator` provides it.

```php
use Doctrine\DBAL\Connection;
use Patchlevel\EventSourcing\Attribute\Header;
use Patchlevel\EventSourcing\Message\Serializer\DefaultHeadersSerializer;
use Patchlevel\EventSourcing\Metadata\ChainClassLocator;
use Patchlevel\EventSourcing\Metadata\FilesystemClassLocator;
use Patchlevel\EventSourcing\Repository\MessageDecorator\SplitStreamHeaderLocator;
use Patchlevel\EventSourcing\Serializer\EventSerializer;
use Patchlevel\EventSourcing\Store\StreamDoctrineDbalStore;

/**
* @var Connection $connection
* @var EventSerializer $eventSerializer
*/
$store = new StreamDoctrineDbalStore(
$connection,
$eventSerializer,
DefaultHeadersSerializer::createFromLocator(
new ChainClassLocator([
new SplitStreamHeaderLocator(),
new FilesystemClassLocator(['src/Header'], Header::class),
]),
),
);
```
:::warning
Keep the `SplitStreamHeaderLocator` registered as long as your store contains messages with this header,
even if you remove the `SplitStreamDecorator` later. Otherwise these messages can no longer be loaded.
:::

:::tip
You can use multiple decorators with the `ChainMessageDecorator`.
:::
Expand Down
2 changes: 1 addition & 1 deletion src/Console/OutputStyle.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

use Patchlevel\EventSourcing\Message\Message;
use Patchlevel\EventSourcing\Message\Serializer\HeadersSerializer;
use Patchlevel\EventSourcing\Repository\MessageDecorator\StreamStartHeader;
use Patchlevel\EventSourcing\Serializer\Encoder\Encoder;
use Patchlevel\EventSourcing\Serializer\EventSerializer;
use Patchlevel\EventSourcing\Store\ArchivedHeader;
use Patchlevel\EventSourcing\Store\Header\PlayheadHeader;
use Patchlevel\EventSourcing\Store\Header\RecordedOnHeader;
use Patchlevel\EventSourcing\Store\Header\StreamNameHeader;
use Patchlevel\EventSourcing\Store\StreamStartHeader;
use Symfony\Component\Console\Style\SymfonyStyle;
use Throwable;

Expand Down
26 changes: 19 additions & 7 deletions src/Message/Serializer/DefaultHeadersSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

namespace Patchlevel\EventSourcing\Message\Serializer;

use Patchlevel\EventSourcing\Attribute\Header;
use Patchlevel\EventSourcing\Message\MissingHeaders;
use Patchlevel\EventSourcing\Metadata\ClassLocator;
use Patchlevel\EventSourcing\Metadata\FilesystemClassLocator;
use Patchlevel\EventSourcing\Metadata\InMemoryClassLocator;
use Patchlevel\EventSourcing\Metadata\Message\AttributeMessageHeaderRegistryFactory;
use Patchlevel\EventSourcing\Metadata\Message\HeaderNameNotRegistered;
use Patchlevel\EventSourcing\Metadata\Message\MessageHeaderRegistry;
Expand Down Expand Up @@ -98,9 +102,22 @@ public static function createFromPaths(
array $paths,
array $gracefulMissingHeaders = [],
Hydrator $hydrator = new StackHydrator(),
): static {
return self::createFromLocator(
new FilesystemClassLocator($paths, Header::class),
$gracefulMissingHeaders,
$hydrator,
);
}

/** @param list<string> $gracefulMissingHeaders */
public static function createFromLocator(
ClassLocator $locator,
array $gracefulMissingHeaders = [],
Hydrator $hydrator = new StackHydrator(),
): static {
return new self(
(new AttributeMessageHeaderRegistryFactory())->create($paths),
(new AttributeMessageHeaderRegistryFactory())->create($locator),
$hydrator,
new JsonEncoder(),
$gracefulMissingHeaders,
Expand All @@ -109,11 +126,6 @@ public static function createFromPaths(

public static function createDefault(Hydrator $hydrator = new StackHydrator()): static
{
return new self(
MessageHeaderRegistry::createWithInternalHeaders(),
$hydrator,
new JsonEncoder(),
[],
);
return self::createFromLocator(new InMemoryClassLocator([]), [], $hydrator);
}
}
30 changes: 30 additions & 0 deletions src/Metadata/ChainClassLocator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Metadata;

use function array_merge;
use function array_unique;
use function array_values;

final class ChainClassLocator implements ClassLocator
{
/** @param iterable<ClassLocator> $locators */
public function __construct(
private readonly iterable $locators,
) {
}

/** @return list<class-string> */
public function locate(): array
{
$classes = [];

foreach ($this->locators as $locator) {
$classes[] = $locator->locate();
}

return array_values(array_unique(array_merge(...$classes)));
}
}
11 changes: 11 additions & 0 deletions src/Metadata/ClassLocator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Metadata;

interface ClassLocator
{
/** @return list<class-string> */
public function locate(): array;
}
41 changes: 41 additions & 0 deletions src/Metadata/FilesystemClassLocator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Metadata;

use ReflectionClass;

use function array_filter;
use function array_values;

final class FilesystemClassLocator implements ClassLocator
{
/**
* @param list<string> $paths
* @param class-string|null $attribute only classes with this attribute are returned
*/
public function __construct(
private readonly array $paths,
private readonly string|null $attribute = null,
) {
}

/** @return list<class-string> */
public function locate(): array
{
$classes = (new ClassFinder())->findClassNames($this->paths);
$attribute = $this->attribute;

if ($attribute === null) {
return $classes;

Check warning on line 31 in src/Metadata/FilesystemClassLocator.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.5, ubuntu-latest)

Escaped Mutant for Mutator "ReturnRemoval": @@ @@ $attribute = $this->attribute; if ($attribute === null) { - return $classes; + } return array_values(
}

return array_values(
array_filter(
$classes,
static fn (string $class): bool => (new ReflectionClass($class))->getAttributes($attribute) !== [],
),
);
}
}
20 changes: 20 additions & 0 deletions src/Metadata/InMemoryClassLocator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Metadata;

final class InMemoryClassLocator implements ClassLocator
{
/** @param list<class-string> $classes */
public function __construct(
private readonly array $classes,
) {
}

/** @return list<class-string> */
public function locate(): array
{
return $this->classes;
}
}
Loading
Loading