Skip to content
Merged
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
32 changes: 17 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,14 @@ powered by the reliable Doctrine ecosystem and focused on developer experience.
* 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
* Automatic [snapshot](https://patchlevel.github.io/event-sourcing-docs/latest/snapshots/)-system to boost your
performance
* [Split](https://patchlevel.github.io/event-sourcing-docs/latest/split_stream/) big aggregates into multiple streams
* Versioned and managed lifecycle
of [subscriptions](https://patchlevel.github.io/event-sourcing-docs/latest/subscription/) like projections and
processors
* Safe usage of [Personal Data](https://patchlevel.github.io/event-sourcing-docs/latest/personal_data/) with
crypto-shredding
* Smooth [upcasting](https://patchlevel.github.io/event-sourcing-docs/latest/upcasting/) of old events
* Simple setup with [scheme management](https://patchlevel.github.io/event-sourcing-docs/latest/store/)
and [doctrine migration](https://patchlevel.github.io/event-sourcing-docs/latest/store/)
* Built in [cli commands](https://patchlevel.github.io/event-sourcing-docs/latest/cli/)
with [symfony](https://symfony.com/)
* Automatic [snapshot](https://patchlevel.dev/docs/event-sourcing/latest/snapshots)-system to boost your performance
* [Split](https://patchlevel.dev/docs/event-sourcing/latest/split-stream) big aggregates into multiple streams
* Versioned and managed lifecycle of [subscriptions](https://patchlevel.dev/docs/event-sourcing/latest/subscription) like projections and processors
* Safe usage of [personal data](https://patchlevel.dev/docs/event-sourcing/latest/personal-data) with crypto-shredding
* Smooth [upcasting](https://patchlevel.dev/docs/event-sourcing/latest/upcasting) of old events
* Simple setup with [schema management](https://patchlevel.dev/docs/event-sourcing/latest/store) and [doctrine migration](https://patchlevel.dev/docs/event-sourcing/latest/store)
* Built in [cli commands](https://patchlevel.dev/docs/event-sourcing/latest/cli) with [symfony](https://symfony.com/)
* Decisions across streams with a [dynamic consistency boundary](https://patchlevel.dev/docs/event-sourcing/latest/dynamic-consistency-boundary)
* and much more...

## Installation
Expand All @@ -36,14 +31,21 @@ composer require patchlevel/event-sourcing

## Documentation

* Latest [Docs](https://event-sourcing.patchlevel.io/latest/getting_started/)
* Related [Blog](https://patchlevel.de/blog)
* Latest [Docs](https://patchlevel.dev/docs/event-sourcing/latest)
* Related [Blog](https://patchlevel.dev/blog)

## Integration

* [Symfony](https://github.com/patchlevel/event-sourcing-bundle)
* [Psalm](https://github.com/patchlevel/event-sourcing-psalm-plugin)

## Contributing

We are open to contributions as long as they are in line with
our [BC-Policy](https://patchlevel.dev/our-backward-compatibility-promise).

Also note that the `composer.lock` is always generated with the newest supported PHP version as this is the version our tools run in the CI.

## Supported databases

We officially only support the databases and versions listed in the table, as these are tested in the CI.
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"dynamic consistency boundary",
"patchlevel"
],
"homepage": "https://event-sourcing.patchlevel.io",
"homepage": "https://patchlevel.dev/docs/event-sourcing/latest",
"authors": [
{
"name": "Daniel Badura",
Expand Down
1 change: 1 addition & 0 deletions docs/command-bus.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,4 @@ $provider = new ChainHandlerProvider([
* [How to use clock](clock.md)
* [How to use aggregate id](identifier.md)
* [How to use query bus](query-bus.md)
* [How to decide across streams with a dynamic consistency boundary](dynamic-consistency-boundary.md)
8 changes: 4 additions & 4 deletions docs/dynamic-consistency-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ final class GuestIsCheckedOut
```

:::note
You can find out more about events [here](events.md).
You can find out more about [events](events.md).
:::

## Define Commands
Expand Down Expand Up @@ -456,6 +456,6 @@ You can find this [Getting Started](./getting-started.md) section.

## Learn more

* [Events](./events.md)
* [Command Bus](./command-bus.md)
* [Store](./store.md)
* [How to define events](events.md)
* [How to dispatch commands](command-bus.md)
* [How to store events](store.md)
4 changes: 2 additions & 2 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ You can also listen on events to react and perform different actions.
An event has a name and additional information called payload.
Such an event can be represented as any class.
It is important that the payload can be serialized as JSON at the end.
Later it will be explained how to ensure it for all values.
How to ensure this for complex values is shown in the [normalizer](#normalizer) section below.

To register an event you have to set the `Event` attribute over the class,
otherwise it will not be recognized as an event.
Expand Down Expand Up @@ -80,7 +80,7 @@ $serializer = DefaultEventSerializer::createFromPaths(['src/Domain']);
```
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.
Internally, an EventRegistry is used, which is described in the [Event Registry](#event-registry) section below.

## Normalizer

Expand Down
8 changes: 4 additions & 4 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ You can find out more about [processors](subscription.md).
After we have defined everything, we still have to plug the whole thing together:

:::tip
If you use symfony, you can use our [symfony bundle](https://event-sourcing-bundle.patchlevel.io/latest/installation/) to skip this step.
If you use symfony, you can use our [symfony bundle](https://patchlevel.dev/docs/event-sourcing-bundle/latest) to skip this step.
:::

```php
Expand Down Expand Up @@ -326,7 +326,7 @@ $hotelRepository = $repositoryManager->get(Hotel::class);
```

:::note
You can find out more about stores [here](store.md).
You can find out more about the [store](store.md).
:::

## Database setup
Expand Down Expand Up @@ -386,12 +386,12 @@ $hotel2 = $hotelRepository->load(Uuid::fromString('d0d0d0d0-d0d0-d0d0-d0d0-d0d0d
$hotel2->checkIn('David');
$hotelRepository->save($hotel2);

$hotels = $hotelProjection->getHotels();
$hotels = $hotelProjector->getHotels();
```

:::note
You can also use other forms of IDs such as uuid version 6 or a custom format.
You can find more about this [here](identifier.md).
You can find more about [identifiers](identifier.md).
:::

## Result
Expand Down
File renamed without changes.
28 changes: 16 additions & 12 deletions docs/message.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,17 @@ the [repository](repository.md).
You can add a header using `withHeader`:

```php
use Patchlevel\EventSourcing\Aggregate\AggregateHeader;
use Patchlevel\EventSourcing\Clock\SystemClock;
use Patchlevel\EventSourcing\Message\Message;
use Patchlevel\EventSourcing\Store\Header\PlayheadHeader;
use Patchlevel\EventSourcing\Store\Header\RecordedOnHeader;
use Patchlevel\EventSourcing\Store\Header\StreamNameHeader;

$clock = new SystemClock();
$message = Message::create(new NameChanged('foo'))
->withHeader(new AggregateHeader(
aggregateName: 'profile',
aggregateId: 'bca7576c-536f-4428-b694-7b1f00c714b7',
playhead: 2,
recordedOn: $clock->now(),
));
->withHeader(new StreamNameHeader('profile-bca7576c-536f-4428-b694-7b1f00c714b7'))
->withHeader(new PlayheadHeader(2))
->withHeader(new RecordedOnHeader($clock->now()));
```
:::note
The message object is immutable. It creates a new instance with the new data.
Expand All @@ -39,19 +38,24 @@ The message object is immutable. It creates a new instance with the new data.
You can also access the headers:

```php
use Patchlevel\EventSourcing\Aggregate\AggregateHeader;
use Patchlevel\EventSourcing\Message\Message;
use Patchlevel\EventSourcing\Store\Header\PlayheadHeader;

/** @var Message $message */
$message->header(AggregateHeader::class); // AggregateHeader object
$message->hasHeader(AggregateHeader::class); // true
$message->headers(); // [AggregateHeader object]
$message->header(PlayheadHeader::class); // PlayheadHeader object
$message->hasHeader(PlayheadHeader::class); // true
$message->headers(); // [StreamNameHeader object, PlayheadHeader object, ...]
```
## Built-in headers

The message object has some built-in headers which are used internally.

* `AggregateHeader` - Contains the aggregate name, aggregate id, playhead and recorded on.
* `StreamNameHeader` - The name of the stream the message belongs to, in the format `[aggregateName]-[aggregateId]`.
* `PlayheadHeader` - The position of the message within its stream.
* `RecordedOnHeader` - The date and time when the message was recorded.
* `EventIdHeader` - The unique id of the event.
* `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.

Expand Down
6 changes: 5 additions & 1 deletion docs/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"navigation": [
{
"title": "Introduction",
"file": "index.md"
"file": "introduction.md"
},
{
"title": "Getting Started",
Expand Down Expand Up @@ -80,6 +80,10 @@
"title": "Split Stream",
"file": "split-stream.md"
},
{
"title": "Dynamic Consistency Boundary",
"file": "dynamic-consistency-boundary.md"
},
{
"title": "Time / Clock",
"file": "clock.md"
Expand Down
4 changes: 2 additions & 2 deletions docs/repository.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ An `AggregateOutdated` exception is thrown if a conflict occurs.
:::

:::tip
If you use the Command Bus, you can use the [RetryOutdatedAggregateCommandBus](command-bus.md#retry-outdated-aggregate-command-bus)
to retry the command when an `AggregateOutdated` exception occurs automatically.
If you use the Command Bus, you can use the [instant retry](command-bus.md#instant-retry) decorator
to retry the command automatically when an `AggregateOutdated` exception occurs.
:::

### Load an aggregate
Expand Down
2 changes: 1 addition & 1 deletion docs/split-stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ $repositoryManager = new DefaultRepositoryManager(
```

:::note
You can find out more about decorator [here](./message-decorator.md).
You can find out more about the [message decorator](message-decorator.md).
:::

:::tip
Expand Down
46 changes: 25 additions & 21 deletions docs/store.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,17 @@ $store = new InMemoryStore();
You can pass messages to the constructor to initialize the store with some events.
:::

### StreamReadOnlyStore
### ReadOnlyStore

Last but not least, we offer a read-only store named `StreamReadOnlyStore`.
It passes all methods to the underlying store, but throws an `StoreIsReadOnly` exception when trying to execute write
Last but not least, we offer a read-only store named `ReadOnlyStore`.
It passes all methods to the underlying store, but throws a `StoreIsReadOnly` exception when trying to execute write
operations.

```php
use Patchlevel\EventSourcing\Store\ReadOnlyStore;
use Patchlevel\EventSourcing\Store\StreamStore;
use Patchlevel\EventSourcing\Store\Store;

/** @var StreamStore $store */
/** @var Store $store */
$readOnlyStore = new ReadOnlyStore($store);
```
## Schema
Expand Down Expand Up @@ -121,7 +121,7 @@ $schemaDirector = new DoctrineSchemaDirector(
```

:::note
How to setup cli commands for schema director can be found [here](cli.md).
How to setup [cli commands](cli.md) for the schema director is described in the CLI documentation.
:::

#### Create schema
Expand Down Expand Up @@ -231,7 +231,7 @@ Here you can find more information on how to
:::

:::note
How to setup cli commands for doctrine migration can be found [here](cli.md).
How to setup [cli commands](cli.md) for doctrine migrations is described in the CLI documentation.
:::

## Usage
Expand Down Expand Up @@ -268,16 +268,15 @@ $stream = $store->load(
The `Criteria` object is used to filter the events.

```php
use Patchlevel\EventSourcing\Store\Criteria\AggregateIdCriterion;
use Patchlevel\EventSourcing\Store\Criteria\AggregateNameCriterion;
use Patchlevel\EventSourcing\Store\Criteria\ArchivedCriterion;
use Patchlevel\EventSourcing\Store\Criteria\Criteria;
use Patchlevel\EventSourcing\Store\Criteria\EventsCriterion;
use Patchlevel\EventSourcing\Store\Criteria\FromIndexCriterion;
use Patchlevel\EventSourcing\Store\Criteria\FromPlayheadCriterion;
use Patchlevel\EventSourcing\Store\Criteria\StreamCriterion;

$criteria = new Criteria(
new AggregateNameCriterion('profile'),
new AggregateIdCriterion('e3e3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e'),
new StreamCriterion('profile-e3e3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e'),
new FromPlayheadCriterion(2),
new FromIndexCriterion(100),
new ArchivedCriterion(true),
Expand All @@ -290,21 +289,24 @@ Or you can the criteria builder to create the criteria.
use Patchlevel\EventSourcing\Store\Criteria\CriteriaBuilder;

$criteria = (new CriteriaBuilder())
->aggregateName('profile')
->aggregateId('e3e3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e')
->streamName('profile-e3e3e3e3-3e3e-3e3e-3e3e-3e3e3e3e3e3e')
->fromPlayhead(2)
->fromIndex(100)
->archived(true)
->events(['profile.created', 'profile.name_changed'])
->build();
```
:::tip
A stream name has the format `[aggregateName]-[aggregateId]`. To match every stream of an aggregate,
use a wildcard with `StreamCriterion::startWith('profile-')`.
:::
#### Stream

The load method returns a `Stream` object and is a generator.
This means that the messages are only loaded when they are needed.

```php
use Patchlevel\EventSourcing\Store\Stream;
use Patchlevel\EventSourcing\Message\Stream;

/** @var Stream $stream */
$stream->index(); // get the index of the stream
Expand All @@ -319,7 +321,7 @@ foreach ($stream as $message) {
```

:::note
You can find more information about the `Message` object [here](message.md).
You can find more information about the [`Message` object](message.md).
:::

:::warning
Expand Down Expand Up @@ -385,22 +387,24 @@ In event sourcing, the events are immutable.

### Remove

You can remove a stream with the `remove` method.
You can remove streams with the `remove` method by passing a criteria.

```php
use Patchlevel\EventSourcing\Store\StreamStore;
use Patchlevel\EventSourcing\Store\Criteria\Criteria;
use Patchlevel\EventSourcing\Store\Criteria\StreamCriterion;
use Patchlevel\EventSourcing\Store\Store;

/** @var StreamStore $store */
$store->remove('profile-*');
/** @var Store $store */
$store->remove(new Criteria(StreamCriterion::startWith('profile-')));
```
### List Streams

You can list all streams with the `streams` method.

```php
use Patchlevel\EventSourcing\Store\StreamStore;
use Patchlevel\EventSourcing\Store\Store;

/** @var StreamStore $store */
/** @var Store $store */
$streams = $store->streams(); // ['profile-1', 'profile-2', 'profile-3']
```
### Transaction
Expand Down
14 changes: 13 additions & 1 deletion docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ a [PHPUnit testing library](https://github.com/patchlevel/event-sourcing-phpunit

## Testing with patchlevel/event-sourcing-phpunit

The helpers in this section live in a separate package that you install as a dev dependency:

```bash
composer require --dev patchlevel/event-sourcing-phpunit
```

### Aggregate Unit Tests

There is a special `TestCase` for aggregate tests that you can extend. By extending `AggregateRootTestCase`, you can use
Expand Down Expand Up @@ -245,5 +251,11 @@ final class ProfileTest extends TestCase

:::warning
The `FakeRamseyUuidFactory` is only for testing purposes
and supports only the version 7 what is used by the library.
and supports only the version 7 which is used by the library.
:::

## Learn more

* [How to define aggregates](aggregate.md)
* [How to control time with the clock](clock.md)
* [How to work with identifiers](identifier.md)
6 changes: 0 additions & 6 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,6 @@ parameters:
count: 1
path: src/Attribute/Subscriber.php

-
message: '#^Patchlevel\\EventSourcing\\CommandBus\\Handler\\UpdateAggregateHandler should not depend on Patchlevel\\EventSourcing\\Identifier\\Identifier$#'
identifier: phpat.testCommandBusCanOnlyDependOnAllowedLayers
count: 2
path: src/CommandBus/Handler/UpdateAggregateHandler.php

-
message: '#^Patchlevel\\EventSourcing\\CommandBus\\InstantRetryCommandBus should not depend on Patchlevel\\EventSourcing\\Store\\AppendConditionNotMet$#'
identifier: phpat.testCommandBusCanOnlyDependOnAllowedLayers
Expand Down
Loading
Loading