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
8 changes: 4 additions & 4 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 48 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,14 +178,22 @@ patchlevel_event_sourcing:
Following store types are available:

- `dbal_stream` *default*
- `dbal_taggable` *required for DCB*
- `dbal_taggable`
- `in_memory`
- `custom`

:::note
If you use `custom` store type, you need to set the service id under `patchlevel_event_sourcing.store.service`.
:::

:::tip
With `dbal_taggable` the bundle registers the `PostgreSQLPlatformMiddleware` on the event store connection,
so a GIN index on the `tags` column is created on PostgreSQL.
This works automatically if you configure the connection via `url` or use a doctrine bundle connection
like `doctrine.dbal.eventstore_connection`.
For any other connection service you have to register the middleware yourself.
:::

### Change table Name

You can change the table name of the event store.
Expand Down Expand Up @@ -550,6 +558,23 @@ final class SubscriptionErrorListener
You can find all available events in the [library documentation](/docs/event-sourcing/latest/subscription).
:::

### Event Emitter

Subscribers can emit new events into their own `subscription_<id>` stream with the `EventEmitter`.
This is disabled by default and can be enabled like this:

```yaml
patchlevel_event_sourcing:
subscription:
event_emitter: true
```
:::warning
If a subscription is removed, its `subscription_<id>` stream is removed from the event store as well.
:::

:::note
The event emitter does not work with a read only store.
:::
## Command Bus

You can enable the command bus integration to use your aggregates as command handlers.
Expand Down Expand Up @@ -869,4 +894,25 @@ You can then specify this service here:
patchlevel_event_sourcing:
clock:
service: 'my_own_clock_service'
```
```

## Dynamic Consistency Boundary

You can enable the experimental dynamic consistency boundary (DCB).
This registers the `DecisionModelBuilder` and the `EventAppender` services.

```yaml
patchlevel_event_sourcing:
store:
type: 'dbal_taggable'
dcb: true
```
:::note
DCB needs a store that supports appending, like `dbal_taggable` or `in_memory` for tests.
:::

:::tip
We recommend PostgreSQL for DCB, because only there the tag queries can use an index.
:::

If you want to learn more about DCB, read the [library documentation](/docs/event-sourcing/latest/dynamic-consistency-boundary).
7 changes: 6 additions & 1 deletion src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
* enabled: bool,
* retries_in_ms: list<int>,
* detection_window: string
* }
* },
* event_emitter: array{enabled: bool}
* },
* connection: ?array{
* service: ?string,
Expand Down Expand Up @@ -184,7 +185,7 @@
->addDefaultsIfNotSet()
->children()
->enumNode('type')
->values(['dbal', 'in_memory', 'static_in_memory', 'custom'])

Check warning on line 188 in src/DependencyInjection/Configuration.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ ->addDefaultsIfNotSet() ->children() ->enumNode('type') - ->values(['dbal', 'in_memory', 'static_in_memory', 'custom']) + ->values(['in_memory', 'static_in_memory', 'custom']) ->defaultValue('dbal') ->end() ->scalarNode('service')->defaultNull()->end()
->defaultValue('dbal')
->end()
->scalarNode('service')->defaultNull()->end()
Expand All @@ -206,13 +207,13 @@
->arrayNode('options')->variablePrototype()->end()->end()
->end()
->end()
->defaultValue([

Check warning on line 210 in src/DependencyInjection/Configuration.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ ->end() ->end() ->defaultValue([ - 'default' => [ - 'type' => 'clock_based', - 'options' => [ - 'base_delay' => 5, - 'delay_factor' => 2, - 'max_attempts' => 5, - ], - ], 'no_retry' => [ 'type' => 'no_retry', ],
'default' => [
'type' => 'clock_based',
'options' => [

Check warning on line 213 in src/DependencyInjection/Configuration.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ 'default' => [ 'type' => 'clock_based', 'options' => [ - 'base_delay' => 5, 'delay_factor' => 2, 'max_attempts' => 5, ],
'base_delay' => 5,

Check warning on line 214 in src/DependencyInjection/Configuration.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "IncrementInteger": @@ @@ 'default' => [ 'type' => 'clock_based', 'options' => [ - 'base_delay' => 5, + 'base_delay' => 6, 'delay_factor' => 2, 'max_attempts' => 5, ],

Check warning on line 214 in src/DependencyInjection/Configuration.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "DecrementInteger": @@ @@ 'default' => [ 'type' => 'clock_based', 'options' => [ - 'base_delay' => 5, + 'base_delay' => 4, 'delay_factor' => 2, 'max_attempts' => 5, ],
'delay_factor' => 2,

Check warning on line 215 in src/DependencyInjection/Configuration.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "IncrementInteger": @@ @@ 'type' => 'clock_based', 'options' => [ 'base_delay' => 5, - 'delay_factor' => 2, + 'delay_factor' => 3, 'max_attempts' => 5, ], ],

Check warning on line 215 in src/DependencyInjection/Configuration.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "DecrementInteger": @@ @@ 'type' => 'clock_based', 'options' => [ 'base_delay' => 5, - 'delay_factor' => 2, + 'delay_factor' => 1, 'max_attempts' => 5, ], ],
'max_attempts' => 5,

Check warning on line 216 in src/DependencyInjection/Configuration.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "IncrementInteger": @@ @@ 'options' => [ 'base_delay' => 5, 'delay_factor' => 2, - 'max_attempts' => 5, + 'max_attempts' => 6, ], ], 'no_retry' => [

Check warning on line 216 in src/DependencyInjection/Configuration.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "DecrementInteger": @@ @@ 'options' => [ 'base_delay' => 5, 'delay_factor' => 2, - 'max_attempts' => 5, + 'max_attempts' => 4, ], ], 'no_retry' => [
],
],
'no_retry' => [
Expand Down Expand Up @@ -264,6 +265,10 @@
->scalarNode('detection_window')->defaultValue('PT5M')->end()
->end()
->end()

->arrayNode('event_emitter')
->canBeEnabled()
->end()
->end()
->end()

Expand Down
69 changes: 65 additions & 4 deletions src/DependencyInjection/PatchlevelEventSourcingExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
use Patchlevel\EventSourcing\Snapshot\Adapter\Psr6SnapshotAdapter;
use Patchlevel\EventSourcing\Snapshot\DefaultSnapshotStore;
use Patchlevel\EventSourcing\Snapshot\SnapshotStore;
use Patchlevel\EventSourcing\Store\Dbal\PostgreSQLPlatformMiddleware;
use Patchlevel\EventSourcing\Store\InMemoryStore;
use Patchlevel\EventSourcing\Store\ReadOnlyStore;
use Patchlevel\EventSourcing\Store\Store;
Expand All @@ -102,7 +103,9 @@
use Patchlevel\EventSourcing\Subscription\Cleanup\DefaultCleaner;
use Patchlevel\EventSourcing\Subscription\Engine\CatchUpSubscriptionEngine;
use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine;
use Patchlevel\EventSourcing\Subscription\Engine\Event\OnSubscriptionRemoved;
use Patchlevel\EventSourcing\Subscription\Engine\GapResolverStoreMessageLoader;
use Patchlevel\EventSourcing\Subscription\Engine\Listener\RemoveSubscriptionStreamListener;
use Patchlevel\EventSourcing\Subscription\Engine\MessageLoader;
use Patchlevel\EventSourcing\Subscription\Engine\StoreMessageLoader;
use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine;
Expand All @@ -115,6 +118,7 @@
use Patchlevel\EventSourcing\Subscription\Store\InMemorySubscriptionStore;
use Patchlevel\EventSourcing\Subscription\Store\SubscriptionStore;
use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\ArgumentResolver;
use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\EventEmitterResolver;
use Patchlevel\EventSourcing\Subscription\Subscriber\ArgumentResolver\LookupResolver;
use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository;
use Patchlevel\EventSourcing\Subscription\Subscriber\SubscriberAccessorRepository;
Expand Down Expand Up @@ -156,6 +160,7 @@
use Symfony\Component\EventDispatcher\EventDispatcher;

use function class_exists;
use function preg_match;
use function sprintf;

/** @psalm-import-type Config from Configuration */
Expand Down Expand Up @@ -533,6 +538,7 @@
]);

$this->configureSyncSubscription($config, $container);
$this->configureEventEmitter($config, $container);

if ($config['subscription']['auto_setup']['enabled']) {
$container->register(AutoSetupListener::class)
Expand Down Expand Up @@ -599,6 +605,33 @@
]);
}

/** @param Config $config */
private function configureEventEmitter(array $config, ContainerBuilder $container): void
{
if (!$config['subscription']['event_emitter']['enabled']) {
return;
}

if ($config['store']['read_only']) {
throw new InvalidArgumentException('Event emitter does not support a read only store');
}

$container->register(EventEmitterResolver::class)
->setArguments([new Reference(Store::class)])

Check warning on line 620 in src/DependencyInjection/PatchlevelEventSourcingExtension.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ } $container->register(EventEmitterResolver::class) - ->setArguments([new Reference(Store::class)]) + ->setArguments([]) ->addTag('event_sourcing.argument_resolver'); $container->register(RemoveSubscriptionStreamListener::class)
->addTag('event_sourcing.argument_resolver');

$container->register(RemoveSubscriptionStreamListener::class)
->setArguments([

Check warning on line 624 in src/DependencyInjection/PatchlevelEventSourcingExtension.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ $container->register(RemoveSubscriptionStreamListener::class) ->setArguments([ - new Reference(Store::class), new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE), ]) ->addTag('kernel.event_listener', [
new Reference(Store::class),
new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE),
])
->addTag('kernel.event_listener', [
'event' => OnSubscriptionRemoved::class,
'dispatcher' => 'event_sourcing.subscription.event_dispatcher',
])
->addTag('monolog.logger', ['channel' => 'event_sourcing']);

Check warning on line 632 in src/DependencyInjection/PatchlevelEventSourcingExtension.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "ArrayItemRemoval": @@ @@ 'event' => OnSubscriptionRemoved::class, 'dispatcher' => 'event_sourcing.subscription.event_dispatcher', ]) - ->addTag('monolog.logger', ['channel' => 'event_sourcing']); + ->addTag('monolog.logger', []); } /** @PARAM Config $config */
}

/** @param Config $config */
private function configureHydrator(array $config, ContainerBuilder $container): void
{
Expand Down Expand Up @@ -681,11 +714,19 @@
return;
}

$middlewares = [];

if ($this->usesTaggableStore($config)) {
$container->register(PostgreSQLPlatformMiddleware::class);
$middlewares[] = new Reference(PostgreSQLPlatformMiddleware::class);
}

if ($config['connection']['url'] !== null) {
$container->register('event_sourcing.dbal_connection', Connection::class)
->setFactory([DbalConnectionFactory::class, 'createConnection'])
->setArguments([
$config['connection']['url'],
$middlewares,
]);

if ($config['connection']['provide_dedicated_connection']) {
Expand All @@ -710,6 +751,26 @@
}

$container->setAlias('event_sourcing.dbal_connection', $config['connection']['service']);

if (
$middlewares === []
|| !preg_match('/^doctrine\.dbal\.(.+)_connection$/', $config['connection']['service'], $matches)

Check warning on line 757 in src/DependencyInjection/PatchlevelEventSourcingExtension.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PregMatchRemoveDollar": @@ @@ if ( $middlewares === [] - || !preg_match('/^doctrine\.dbal\.(.+)_connection$/', $config['connection']['service'], $matches) + || !preg_match('/^doctrine\.dbal\.(.+)_connection/', $config['connection']['service'], $matches) ) { return; }

Check warning on line 757 in src/DependencyInjection/PatchlevelEventSourcingExtension.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PregMatchRemoveCaret": @@ @@ if ( $middlewares === [] - || !preg_match('/^doctrine\.dbal\.(.+)_connection$/', $config['connection']['service'], $matches) + || !preg_match('/doctrine\.dbal\.(.+)_connection$/', $config['connection']['service'], $matches) ) { return; }
) {
return;
}

$container->getDefinition(PostgreSQLPlatformMiddleware::class)
->addTag('doctrine.middleware', ['connection' => $matches[1]]);
}

/** @param Config $config */
private function usesTaggableStore(array $config): bool
{
return $config['store']['type'] === 'dbal_taggable'
|| (
$config['store']['migrate_to_new_store']['enabled']
&& $config['store']['migrate_to_new_store']['type'] === 'dbal_taggable'

Check warning on line 772 in src/DependencyInjection/PatchlevelEventSourcingExtension.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "Identical": @@ @@ return $config['store']['type'] === 'dbal_taggable' || ( $config['store']['migrate_to_new_store']['enabled'] - && $config['store']['migrate_to_new_store']['type'] === 'dbal_taggable' + && $config['store']['migrate_to_new_store']['type'] !== 'dbal_taggable' ); }
);
}

/** @param Config $config */
Expand Down Expand Up @@ -1196,19 +1257,19 @@
return;
}

if ($config['store']['type'] !== 'dbal_taggable') {
if ($config['store']['type'] === 'dbal_stream') {
throw new InvalidArgumentException(
'DCB requires a taggable store, please use "dbal_taggable" as store type.',
'DCB requires a store that supports appending, please use "dbal_taggable", "in_memory" or a custom store.',
);
}

$container->register(StoreDecisionModelBuilder::class)
->setArguments([new Reference(TaggableDoctrineDbalStore::class)]);
->setArguments([new Reference(Store::class)]);
$container->setAlias(DecisionModelBuilder::class, StoreDecisionModelBuilder::class);

$container->register(StoreEventAppender::class)
->setArguments([
new Reference(TaggableDoctrineDbalStore::class),
new Reference(Store::class),
new Reference(EventTagExtractor::class),
]);
$container->setAlias(EventAppender::class, StoreEventAppender::class);
Expand Down
6 changes: 5 additions & 1 deletion src/Doctrine/DbalConnectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

namespace Patchlevel\EventSourcingBundle\Doctrine;

use Doctrine\DBAL\Configuration;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\Middleware;
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Tools\DsnParser;

Expand All @@ -27,10 +29,12 @@ final class DbalConnectionFactory
'sqlite3' => 'pdo_sqlite',
];

public static function createConnection(string $url): Connection
/** @param list<Middleware> $middlewares */
public static function createConnection(string $url, array $middlewares = []): Connection
{
return DriverManager::getConnection(
(new DsnParser(self::DEFAULT_SCHEME_MAP))->parse($url),
(new Configuration())->setMiddlewares($middlewares),
);
}
}
Loading
Loading