diff --git a/docs/aggregate-id.md b/docs/aggregate-id.md index bc2a3471..5b220c57 100644 --- a/docs/aggregate-id.md +++ b/docs/aggregate-id.md @@ -41,7 +41,6 @@ use Patchlevel\EventSourcing\Aggregate\Uuid; $uuid = Uuid::generate(); $uuid = Uuid::fromString('d6e8d7a0-4b0b-4e6a-8a9a-3a0b2d9d0e4e'); ``` - :::note We implemented the version 7 of the uuid, because it is most suitable for event sourcing. More information about uuid versions can be found [here](https://uuid.ramsey.dev/en/stable/rfc4122.html). @@ -65,9 +64,8 @@ final class Profile extends BasicAggregateRoot private CustomId $id; } ``` - :::warning -If you want to use a custom id that is not an uuid, +If you want to use a custom id that is not an uuid, you need to change the `aggregate_id_type` to `string` in the store configuration. More information can be found [here](store.md). ::: diff --git a/docs/aggregate.md b/docs/aggregate.md index 6c91ee83..49f7eda4 100644 --- a/docs/aggregate.md +++ b/docs/aggregate.md @@ -5,7 +5,7 @@ One main difference is that we don't save the current state, but only the indivi This means it is always possible to build the current state again from the events. :::note -The term aggregate itself comes from DDD and has nothing to do with event sourcing and can be used independently as a pattern. +The term aggregate itself comes from DDD and has nothing to do with event sourcing and can be used independently as a pattern. You can find out more about Aggregates [here](https://martinfowler.com/bliki/DDD_Aggregate.html). ::: @@ -44,7 +44,6 @@ final class Profile extends BasicAggregateRoot } } ``` - :::warning The aggregate is not yet finished and has only been built to the point that you can instantiate the object. ::: @@ -77,7 +76,6 @@ final class CreateProfileHandler } } ``` - :::warning If you look in the database now, you would see that nothing has been saved. This is because only events are stored in the database and as long as no events exist, @@ -109,7 +107,6 @@ final class ProfileRegistered } } ``` - :::note You can find out more about events [here](events.md). ::: @@ -151,7 +148,6 @@ final class Profile extends BasicAggregateRoot } } ``` - :::tip Prefixing the apply methods with "apply" improves readability. ::: @@ -185,7 +181,6 @@ final class NameChanged } } ``` - :::note Events should best be written in the past, as they describe a state that has happened. ::: @@ -262,7 +257,6 @@ final class ChangeNameHandler } } ``` - :::success Our aggregate can now be changed and saved. ::: @@ -309,9 +303,8 @@ final class Profile extends BasicAggregateRoot } } ``` - :::tip -You don't necessarily need to define multiple `Apply` attributes with the event class +You don't necessarily need to define multiple `Apply` attributes with the event class if you define the event types in the method using a union type. ::: @@ -366,7 +359,6 @@ final class Profile extends BasicAggregateRoot } } ``` - :::warning When all events are suppressed, debugging becomes more difficult if you forget an apply method. ::: @@ -399,7 +391,6 @@ final class PersonalInformation extends BasicAggregateRoot { } ``` - :::warning You need to define the `SharedApplyContext` attribute on all aggregates that share the apply context. ::: @@ -443,7 +434,6 @@ final class GuestList extends BasicAggregateRoot // ... } ``` - :::tip You can find more about splitting aggregates [here](aggregate.md#splitting-aggregates). ::: @@ -486,7 +476,6 @@ final class Profile extends BasicAggregateRoot } } ``` - :::danger Validations during "apply" should not happen, they will break the rebuilding of the aggregate! Instead validate the data *before* the event will be recorded. @@ -573,7 +562,6 @@ final class NameChanged } } ``` - :::warning You need to create a normalizer for the `Name` value object. So the payload must be serializable and unserializable as json. @@ -793,7 +781,6 @@ final class Shipping extends BasicAggregateRoot } } ``` - :::tip With the [SharedApplyContext](aggregate.md#shared-apply-context) attribute, you can suppress missing applies for events that are handled by other aggregates. @@ -842,7 +829,6 @@ final class Shipping extends BasicChildAggregate } } ``` - :::warning The apply method must be public, otherwise the root aggregate cannot call it. ::: @@ -890,7 +876,6 @@ final class Order extends BasicAggregateRoot } } ``` - ## Auto Initialize :::experimental @@ -898,8 +883,8 @@ This feature is still experimental and may change in the future. Use it with caution. ::: -Sometimes you want to be able to access an aggregate even if it has not yet been created in the system. -In this case, the aggregate should be automatically initialized if it cannot be found in the store. +Sometimes you want to be able to access an aggregate even if it has not yet been created in the system. +In this case, the aggregate should be automatically initialized if it cannot be found in the store. To achieve this, the aggregate must mark the initialization method with the `AutoInitialize` attribute. The method must be static, receives the aggregate ID as an argument and must return an instance of the aggregate. @@ -933,7 +918,6 @@ final class Profile extends BasicAggregateRoot } } ``` - :::note Recording events in the `initialize` method is optional but recommended. ::: diff --git a/docs/cli.md b/docs/cli.md index 09850ce9..8c6dff40 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -141,9 +141,8 @@ $cli->addCommands([ new Command\VersionCommand($dependencyFactory, 'event-sourcing:migrations:version'), ]); ``` - :::note -Here you can find more information on how to +Here you can find more information on how to [configure doctrine migration](https://www.doctrine-project.org/projects/doctrine-migrations/en/3.3/reference/custom-configuration.html). ::: diff --git a/docs/clock.md b/docs/clock.md index 25cb26b1..ee99fc14 100644 --- a/docs/clock.md +++ b/docs/clock.md @@ -61,7 +61,6 @@ $clock = new FrozenClock($firstDate); $clock->sleep(10); // sleep 10 seconds ``` - :::note The instance of the frozen datetime will be cloned internally, so the it's not the same instance but equals. ::: diff --git a/docs/command-bus.md b/docs/command-bus.md index 75940c9e..9560cbc1 100644 --- a/docs/command-bus.md +++ b/docs/command-bus.md @@ -38,7 +38,6 @@ final class CreateProfileHandler } } ``` - :::note To use Service Handler you need to register the handler in the `ServiceHandlerProvider`. ::: @@ -65,7 +64,6 @@ final class CreateProfileHandler } } ``` - ### Union Types You can also use union types to handle multiple commands and the library will automatically detect the commands. @@ -82,7 +80,6 @@ final class CreateProfileHandler } } ``` - ### Inheritance The handler will also be invoked if the command implements an interface or extends a class that the handler expects. @@ -99,15 +96,14 @@ final class CreateProfileHandler } } ``` - ### Aggregate Handler Another way to handle commands is to use the aggregates themselves. To do this, you need to mark the method that handles the command with the `#[Handle]` attribute. :::note -The aggregates themselves are of course not a service. -The AggregateHandlerProvider uses the aggregates to create the handlers for you. +The aggregates themselves are of course not a service. +The AggregateHandlerProvider uses the aggregates to create the handlers for you. You can find out more about this in the [providers](command-bus.md#provider) section. ::: @@ -140,7 +136,6 @@ final class Profile extends BasicAggregateRoot // ... apply methods } ``` - :::tip You can find more information about aggregates [here](aggregate.md). ::: @@ -193,9 +188,8 @@ final class Profile extends BasicAggregateRoot // ... apply methods } ``` - :::tip -If you want to automatically initialize an aggregate if it cannot be found in the store, +If you want to automatically initialize an aggregate if it cannot be found in the store, you can use the [Auto Initialize](aggregate.md#auto-initialize) feature. ::: @@ -233,7 +227,6 @@ final class Profile extends BasicAggregateRoot // ... apply methods } ``` - :::note The service must be registered in the service locator. ::: @@ -274,7 +267,6 @@ final class Profile extends BasicAggregateRoot // ... apply methods } ``` - :::note Injection in handler methods is only possible with the `AggregateHandlerProvider`. ::: @@ -327,7 +319,6 @@ final class CreateProfile } } ``` - :::tip You can override the default values for the maximum number of retries and the conditions by passing them to the `InstantRetry` attribute. @@ -405,7 +396,6 @@ $provider = new AggregateHandlerProvider( ]), // or other psr-11 compatible container ); ``` - :::tip You can find suitable implementations of psr-11 containers on [packagist](https://packagist.org/search/?tags=PSR-11). ::: diff --git a/docs/event-bus.md b/docs/event-bus.md index d31310e1..e27af201 100644 --- a/docs/event-bus.md +++ b/docs/event-bus.md @@ -20,7 +20,6 @@ use Patchlevel\EventSourcing\EventBus\DefaultEventBus; $eventBus = DefaultEventBus::create([$mailListener]); ``` - :::note The order in which the listeners are executed is determined by the order in which they are passed to the factory. ::: @@ -56,7 +55,6 @@ $eventBus = new DefaultEventBus( new DefaultConsumer($listenerProvider), ); ``` - :::tip The `DefaultEventBus::create` method uses the `DefaultConsumer` and `AttributeListenerProvider` by default. ::: @@ -80,7 +78,6 @@ $listenerProvider = new class implements ListenerProvider { } }; ``` - :::tip You can use `$listenerDiscriptor->name()` to get the name of the listener. ::: @@ -103,7 +100,6 @@ final class WelcomeSubscriber } } ``` - :::tip If you use psalm, you can use the [event sourcing plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin) for better type support. ::: @@ -136,7 +132,6 @@ use Patchlevel\EventSourcing\EventBus\Psr14EventBus; $eventBus = new Psr14EventBus($psr14EventDispatcher); ``` - :::warning You can't use the `Subscribe` attribute with the psr-14 event bus. ::: diff --git a/docs/events.md b/docs/events.md index fc7af7e1..41e6b334 100644 --- a/docs/events.md +++ b/docs/events.md @@ -27,7 +27,6 @@ final class ProfileCreated } } ``` - :::warning The payload must be serializable and unserializable as json. ::: @@ -41,7 +40,7 @@ Here are some examples: * `profile.created` * `profile.name_changed` * `hotel.guest_checked_out` -::: + ::: ## Alias @@ -109,14 +108,13 @@ final class ProfileCreated } } ``` - :::tip -Built-in normalizers like `IdNormalizer` and `DateTimeImmutableNormalizer` can be inferred from the type hint +Built-in normalizers like `IdNormalizer` and `DateTimeImmutableNormalizer` can be inferred from the type hint and so you don't have to specify them. If you want to configure the Normalizer, you still have to do it. ::: :::note -You can find out more about normalizer [here](normalizer.md). +You can find out more about normalizer [here](normalizer.md). ::: ## Event Registry diff --git a/docs/getting-started.md b/docs/getting-started.md index 16ca49a2..f152e2b6 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -55,9 +55,8 @@ final class GuestIsCheckedOut } } ``` - :::note -You can find out more about events [here](events.md). +You can find out more about events [here](events.md). ::: ## Define aggregates @@ -148,7 +147,6 @@ final class Hotel extends BasicAggregateRoot } } ``` - :::note You can find out more about aggregates [here](aggregate.md). ::: @@ -227,7 +225,6 @@ final class HotelProjector } } ``` - :::note You can find out more about projector [here](subscription.md). ::: @@ -259,7 +256,6 @@ final class SendCheckInEmailProcessor } } ``` - :::note You can find out more about processor [here](subscription.md). ::: @@ -280,6 +276,7 @@ use Patchlevel\EventSourcing\Repository\DefaultRepositoryManager; use Patchlevel\EventSourcing\Serializer\DefaultEventSerializer; use Patchlevel\EventSourcing\Store\DoctrineDbalStore; use Patchlevel\EventSourcing\Subscription\Engine\DefaultSubscriptionEngine; +use Patchlevel\EventSourcing\Subscription\Engine\StoreMessageLoader; use Patchlevel\EventSourcing\Subscription\Repository\RunSubscriptionEngineRepositoryManager; use Patchlevel\EventSourcing\Subscription\Store\DoctrineSubscriptionStore; use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository; @@ -313,7 +310,7 @@ $subscriberRepository = new MetadataSubscriberAccessorRepository([ $subscriptionStore = new DoctrineSubscriptionStore($connection); $engine = new DefaultSubscriptionEngine( - $eventStore, + new StoreMessageLoader($eventStore), $subscriptionStore, $subscriberRepository, ); @@ -328,14 +325,13 @@ $repositoryManager = new RunSubscriptionEngineRepositoryManager( $hotelRepository = $repositoryManager->get(Hotel::class); ``` - :::note You can find out more about stores [here](store.md). ::: :::note -The `RunSubscriptionEngineRepositoryManager` is a decorator that triggers the -Subscription Engine when an Aggregate is saved. Normally, you'd use the +The `RunSubscriptionEngineRepositoryManager` is a decorator that triggers the +Subscription Engine when an Aggregate is saved. Normally, you'd use the `DefaultRepositoryManager` and a worker to run the Subscription Engine. Learn more [here](subscription.md). @@ -372,7 +368,6 @@ $schemaDirector->create(); /** @var SubscriptionEngine $engine */ $engine->setup(skipBooting: true); ``` - :::note you can use the predefined [cli commands](cli.md) for this. ::: @@ -399,9 +394,8 @@ $hotelRepository->save($hotel2); $hotels = $hotelProjection->getHotels(); ``` - :::note -You can also use other forms of IDs such as uuid version 6 or a custom format. +You can also use other forms of IDs such as uuid version 6 or a custom format. You can find more about this [here](aggregate-id.md). ::: @@ -410,7 +404,7 @@ You can find more about this [here](aggregate-id.md). :::success We have successfully implemented and used event sourcing. -Feel free to browse further in the documentation for more detailed information. +Feel free to browse further in the documentation for more detailed information. If there are still open questions, create a ticket on Github and we will try to help you. ::: diff --git a/docs/index.md b/docs/index.md index b68c6fe1..582e9d96 100644 --- a/docs/index.md +++ b/docs/index.md @@ -29,4 +29,4 @@ composer require patchlevel/event-sourcing :::tip Start with the [quickstart](getting-started.md) to get a feeling for the library. -::: \ No newline at end of file +::: diff --git a/docs/message-decorator.md b/docs/message-decorator.md index c0709d6a..52b5c56f 100644 --- a/docs/message-decorator.md +++ b/docs/message-decorator.md @@ -66,7 +66,6 @@ $repositoryManager = new DefaultRepositoryManager( $repository = $repositoryManager->get(Profile::class); ``` - :::note You can find out more about repository [here](repository.md). ::: @@ -98,7 +97,6 @@ final class OnSystemRecordedDecorator implements MessageDecorator } } ``` - :::note The Message is immutable, for more information look up [here](message.md). ::: diff --git a/docs/message.md b/docs/message.md index 288ee168..b93f457a 100644 --- a/docs/message.md +++ b/docs/message.md @@ -11,7 +11,6 @@ use Patchlevel\EventSourcing\Message\Message; $message = Message::create(new NameChanged('foo')); ``` - :::note You don't have to create the message yourself, it is automatically created, saved and dispatched in the [repository](repository.md). @@ -33,7 +32,6 @@ $message = Message::create(new NameChanged('foo')) recordedOn: $clock->now(), )); ``` - :::note The message object is immutable. It creates a new instance with the new data. ::: @@ -82,7 +80,6 @@ use Patchlevel\EventSourcing\Message\Message; $message = Message::create(new NameChanged('foo')) ->withHeader(new ApplicationHeader('app')); ``` - :::warning The header needs to be serializable. The library uses the hydrator to serialize and deserialize the headers. So you can add normalize attributes to the properties if needed. @@ -214,7 +211,6 @@ use Patchlevel\EventSourcing\Message\Translator\RecalculatePlayheadTranslator; $translator = new RecalculatePlayheadTranslator(); ``` - :::warning The `RecalculatePlayheadTranslator` is and need to be stateful. You can't reuse the translator for multiple streams. @@ -281,13 +277,12 @@ final class SplitProfileCreatedTranslator implements Translator } } ``` - :::warning Since we changed the number of messages, we have to recalculate the playhead. ::: :::tip -You don't have to migrate the store directly for every change, +You don't have to migrate the store directly for every change, but you can also use the [upcasting](upcasting.md) feature. ::: diff --git a/docs/normalizer.md b/docs/normalizer.md index ca6cb47c..1d7b6f17 100644 --- a/docs/normalizer.md +++ b/docs/normalizer.md @@ -5,7 +5,7 @@ For example DateTime, enums or value objects. Here you can use the normalizer to define how the data should be saved and loaded. :::note -The underlying system called hydrator exists as a library. +The underlying system called hydrator exists as a library. You can find out more details [here](https://github.com/patchlevel/hydrator). ::: @@ -75,7 +75,6 @@ final class Item } } ``` - :::note With the `ObjectNormalizer`, you can seraialize and deserialize recursively. ::: @@ -100,7 +99,6 @@ final class CreateHotel } } ``` - :::note If you have personal data, you can use [crypto-shredding](personal-data.md). ::: @@ -127,7 +125,6 @@ final class Hotel extends BasicAggregateRoot // ... } ``` - :::note You can learn more about snapshots [here](snapshots.md). ::: @@ -153,7 +150,6 @@ final class DTO public array $dates; } ``` - :::note The keys from the arrays are taken over here. ::: @@ -172,7 +168,6 @@ final class DTO public DateTimeImmutable $date; } ``` - :::tip You can let the hydrator guess the normalizer from the type hint. ::: @@ -189,7 +184,6 @@ final class DTO public DateTimeImmutable $date; } ``` - :::note You can read about how the format is structured in the [php docs](https://www.php.net/manual/de/datetime.format.php). ::: @@ -207,7 +201,6 @@ final class DTO public DateTime $date; } ``` - :::tip You can let the hydrator guess the normalizer from the type hint. ::: @@ -223,9 +216,8 @@ final class DTO public DateTime $date; } ``` - :::warning -It is highly recommended to only ever use DateTimeImmutable objects and the DateTimeImmutableNormalizer. +It is highly recommended to only ever use DateTimeImmutable objects and the DateTimeImmutableNormalizer. This prevents you from accidentally changing the state of the DateTime and thereby causing bugs. ::: @@ -246,7 +238,6 @@ final class DTO public DateTimeZone $timeZone; } ``` - :::tip You can let the hydrator guess the normalizer from the type hint. ::: @@ -264,7 +255,6 @@ final class DTO public Status $status; } ``` - :::tip You can let the hydrator guess the normalizer from the type hint. ::: @@ -294,7 +284,6 @@ final class DTO public Uuid $id; } ``` - :::tip You can let the hydrator guess the normalizer from the type hint. ::: @@ -394,7 +383,6 @@ class NameNormalizer implements Normalizer } } ``` - :::warning The important thing is that the result of Normalize is serializable! ::: @@ -408,7 +396,6 @@ final class DTO public Name $name; } ``` - :::tip Every normalizer, including the custom normalizer, can be used both for the events and for the snapshots. ::: @@ -443,14 +430,13 @@ The whole thing looks like this "profile_name": "David" } ``` - :::tip You can also rename properties to events without having a backwards compatibility break by keeping the serialized name. ::: :::note -NormalizedName also works for snapshots. -But since a snapshot is just a cache, you can also just invalidate it, +NormalizedName also works for snapshots. +But since a snapshot is just a cache, you can also just invalidate it, if you have backwards compatibility break in the property name ::: diff --git a/docs/our-backward-compatibility-promise.md b/docs/our-backward-compatibility-promise.md index 35f90d3c..ddb9ad7e 100644 --- a/docs/our-backward-compatibility-promise.md +++ b/docs/our-backward-compatibility-promise.md @@ -43,4 +43,4 @@ In our docs the features are marked like this: :::experimental This feature is still experimental and may change in the future. Use it with caution. -::: \ No newline at end of file +::: diff --git a/docs/personal-data.md b/docs/personal-data.md index 7fd4d2b8..4f5c5dc1 100644 --- a/docs/personal-data.md +++ b/docs/personal-data.md @@ -39,7 +39,6 @@ final class EmailChanged } } ``` - :::tip You can use the `DataSubjectId` in aggregates for snapshots too. ::: @@ -64,7 +63,6 @@ final class EmailChanged } } ``` - :::tip You can use the `PersonalData` in aggregates for snapshots too. ::: @@ -94,7 +92,6 @@ final class ProfileChanged } } ``` - :::danger You have to deal with this case in your business logic such as aggregates and subscriptions. ::: @@ -152,7 +149,6 @@ use Patchlevel\Hydrator\Cryptography\PersonalDataPayloadCryptographer; /** @var CipherKeyStore $cipherKeyStore */ $cryptographer = PersonalDataPayloadCryptographer::createWithDefaultSettings($cipherKeyStore); ``` - :::tip You can specify the cipher method with the second parameter. ::: @@ -171,7 +167,6 @@ DefaultEventSerializer::createFromPaths( cryptographer: $cryptographer, ); ``` - :::note More information about the events can be found [here](events.md). ::: @@ -192,7 +187,6 @@ $snapshotStore = DefaultSnapshotStore::createDefault( $cryptographer, ); ``` - :::note More information about the snapshot store can be found [here](snapshots.md). ::: diff --git a/docs/query-bus.md b/docs/query-bus.md index 817d3f88..9f61c537 100644 --- a/docs/query-bus.md +++ b/docs/query-bus.md @@ -35,7 +35,6 @@ final class QueryProfileHandler } } ``` - :::warning A query can only be answered by one method. ::: @@ -68,9 +67,8 @@ final class ProfileProjector // projector related methods to maintain the state of profiles } ``` - :::tip -Using small dedicated projections for each usecase is best practice. Using them directly as query handlers are +Using small dedicated projections for each usecase is best practice. Using them directly as query handlers are endoresed and can reduce fragmentation of the system. ::: diff --git a/docs/repository.md b/docs/repository.md index 75c02c98..9cbd8a2b 100644 --- a/docs/repository.md +++ b/docs/repository.md @@ -31,7 +31,6 @@ $repositoryManager = new DefaultRepositoryManager( $repository = $repositoryManager->get(Profile::class); ``` - :::note The same repository instance is always returned for a specific aggregate. ::: @@ -61,7 +60,6 @@ $repositoryManager = new DefaultRepositoryManager( $repository = $repositoryManager->get(Profile::class); ``` - :::warning If you use the event bus, you should be aware that the events are dispatched synchronously. You may encounter [at least once](https://softwaremill.com/message-delivery-and-deduplication-strategies/) problems. @@ -72,7 +70,7 @@ You can find out more about event bus [here](event-bus.md). ::: :::tip -In most cases it is better to react to events asynchronously, +In most cases it is better to react to events asynchronously, that's why we recommend the subscription engine. More information can be found [here](subscription.md). ::: @@ -107,7 +105,6 @@ $repositoryManager = new DefaultRepositoryManager( $repository = $repositoryManager->get(Profile::class); ``` - :::note You can find out more about snapshots [here](snapshots.md). ::: @@ -137,7 +134,6 @@ $repositoryManager = new DefaultRepositoryManager( $repository = $repositoryManager->get(Profile::class); ``` - :::note You can find out more about message decorator [here](message-decorator.md). ::: @@ -167,16 +163,15 @@ $profile = Profile::create($id, 'david.badura@patchlevel.de'); /** @var Repository $repository */ $repository->save($profile); ``` - :::warning All events are written to the database with one transaction in order to ensure data consistency. -If an exception occurs during the save process, +If an exception occurs during the save process, the transaction is rolled back and the aggregate is not valid anymore. You can not save the aggregate again and you need to load it again. ::: :::note -Due to the nature of the aggregate having a playhead, +Due to the nature of the aggregate having a playhead, we have a unique constraint that ensures that no race condition happens here. An `AggregateOutdated` exception is thrown if a conflict occurs. ::: @@ -200,18 +195,17 @@ $id = Uuid::fromString('229286ff-6f95-4df6-bc72-0a239fe7b284'); /** @var Repository $repository */ $profile = $repository->load($id); ``` - :::warning When the method is called, the aggregate is always reloaded and rebuilt from the database. ::: :::note -You can only fetch one aggregate at a time and don't do any complex queries either. +You can only fetch one aggregate at a time and don't do any complex queries either. Projections are used for this purpose. ::: :::tip -If you want to automatically initialize an aggregate if it cannot be found in the store, +If you want to automatically initialize an aggregate if it cannot be found in the store, you can use the [Auto Initialize](aggregate.md#auto-initialize) feature. ::: @@ -231,9 +225,8 @@ if ($repository->has($id)) { // ... } ``` - :::note -The query is fast and does not load any event. +The query is fast and does not load any event. This means that the state of the aggregate is not rebuild either. ::: diff --git a/docs/snapshots.md b/docs/snapshots.md index 42f1fe89..3fd1d25b 100644 --- a/docs/snapshots.md +++ b/docs/snapshots.md @@ -55,7 +55,6 @@ $repositoryManager = new DefaultRepositoryManager( $snapshotStore, ); ``` - :::note You can read more about Repository [here](repository.md). ::: @@ -100,7 +99,6 @@ final class Profile extends BasicAggregateRoot // ... } ``` - :::danger If anything changes in the properties of the aggregate, then the cache must be cleared. Or the snapshot version needs to be changed so that the previous snapshot is invalid. @@ -156,16 +154,15 @@ final class Profile extends BasicAggregateRoot // ... } ``` - :::warning If the snapshots are discarded, a load peak can occur since the aggregates have to be rebuilt. You should update the snapshot version only when necessary. ::: :::tip -If you have aggregates with a lot of events, +If you have aggregates with a lot of events, you should consider using [split streams](split-stream.md) if it make sense in your domain. -Then the load peak is not so high anymore, +Then the load peak is not so high anymore, because only the events from new stream start are loaded to rebuild the aggregate. ::: @@ -232,9 +229,8 @@ use Patchlevel\EventSourcing\Snapshot\SnapshotStore; */ $snapshotStore->save($aggregate); ``` - :::danger -If the state of an aggregate is saved as a snapshot without being saved to the event store (database), +If the state of an aggregate is saved as a snapshot without being saved to the event store (database), it can lead to data loss or broken aggregates! ::: @@ -256,7 +252,7 @@ If the aggregate was not found, then a `SnapshotNotFound` is thrown. And if the version is no longer correct and the snapshot is therefore invalid, then a `SnapshotVersionInvalid` is thrown. :::warning -The aggregate may be in an old state as the snapshot may lag behind. +The aggregate may be in an old state as the snapshot may lag behind. You still have to bring the aggregate up to date by loading the missing events from the event store. ::: diff --git a/docs/split-stream.md b/docs/split-stream.md index 57c8ef31..3254cd22 100644 --- a/docs/split-stream.md +++ b/docs/split-stream.md @@ -40,7 +40,6 @@ $repositoryManager = new DefaultRepositoryManager( new SplitStreamDecorator($eventMetadataFactory), ); ``` - :::note You can find out more about decorator [here](message-decorator.md). ::: @@ -70,9 +69,8 @@ final class BalanceReported } } ``` - :::warning -The event needs all data which is relevant the aggregate to be used since all past event will not be loaded! +The event needs all data which is relevant the aggregate to be used since all past event will not be loaded! Keep this in mind if you want to use this feature. ::: diff --git a/docs/store.md b/docs/store.md index cc4569b1..d09288cd 100644 --- a/docs/store.md +++ b/docs/store.md @@ -36,9 +36,8 @@ $store = new DoctrineDbalStore( DefaultEventSerializer::createFromPaths(['src/Event']), ); ``` - :::note -You can find out more about how to create a connection +You can find out more about how to create a connection [here](https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html) ::: @@ -97,9 +96,8 @@ $store = new StreamDoctrineDbalStore( DefaultEventSerializer::createFromPaths(['src/Event']), ); ``` - :::note -You can find out more about how to create a connection +You can find out more about how to create a connection [here](https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html) ::: @@ -137,7 +135,6 @@ use Patchlevel\EventSourcing\Store\InMemoryStore; $store = new InMemoryStore(); ``` - :::tip You can pass messages to the constructor to initialize the store with some events. ::: @@ -189,7 +186,6 @@ $schemaDirector = new DoctrineSchemaDirector( $store, ); ``` - :::note How to setup cli commands for schema director can be found [here](cli.md). ::: @@ -294,9 +290,8 @@ $dependencyFactory->setService( $schemaProvider, ); ``` - :::note -Here you can find more information on how to +Here you can find more information on how to [configure doctrine migration](https://www.doctrine-project.org/projects/doctrine-migrations/en/3.3/reference/custom-configuration.html). ::: @@ -387,7 +382,6 @@ foreach ($stream as $message) { $message->event(); // get the event } ``` - :::note You can find more information about the `Message` object [here](message.md). ::: @@ -438,9 +432,8 @@ $store->save($message); $store->save($message1, $message2, $message3); $store->save(...$messages); ``` - :::note -The saving happens in a transaction, so all messages are saved or none. +The saving happens in a transaction, so all messages are saved or none. The store lock the table for writing during each save by default. ::: @@ -463,7 +456,6 @@ use Patchlevel\EventSourcing\Store\StreamStore; /** @var StreamStore $store */ $store->remove('profile-*'); ``` - :::note The method is only available in the `StreamStore` like `StreamDoctrineDbalStore`. ::: @@ -478,7 +470,6 @@ use Patchlevel\EventSourcing\Store\StreamStore; /** @var StreamStore $store */ $streams = $store->streams(); // ['profile-1', 'profile-2', 'profile-3'] ``` - :::note The method is only available in the `StreamStore` like `StreamDoctrineDbalStore`. ::: @@ -503,7 +494,6 @@ $store->transactional(static function () use ($command, $bankAccountRepository): $bankAccountRepository->save($accountTo); }); ``` - :::note The store lock the table for writing during the transaction by default. ::: diff --git a/docs/subscription.md b/docs/subscription.md index bd46b6e8..829b1e10 100644 --- a/docs/subscription.md +++ b/docs/subscription.md @@ -26,7 +26,6 @@ final class DoStuffSubscriber { } ``` - :::note For each subsciber ID, the engine will create a subscription. If the subscriber ID changes, a new subscription will be created. @@ -74,7 +73,6 @@ final class ProfileProjector } } ``` - :::warning PostgreSQL, MySQL and MariaDB don't support transactions for DDL statements. So you must use a different database connection for your subscriptions. @@ -149,9 +147,8 @@ final class DoStuffSubscriber } } ``` - :::tip -If you are using psalm then you can install the event sourcing [plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin) +If you are using psalm then you can install the event sourcing [plugin](https://github.com/patchlevel/event-sourcing-psalm-plugin) to make the event method return the correct type. ::: @@ -272,7 +269,6 @@ final class PublicProfileProjection // ... setup, teardown, ... } ``` - :::note More about reducers you can find [here](message.md#reducer) ::: @@ -329,10 +325,9 @@ final class ProfileProjector } } ``` - :::danger PostgreSQL, MySQL and MariaDB don't support transactions for DDL statements. -So you must use a different database connection in your projectors, +So you must use a different database connection in your projectors, otherwise you will get an error when the subscription tries to create the table. ::: @@ -371,10 +366,9 @@ final class ProfileProjector } } ``` - :::danger PostgreSQL, MySQL and MariaDB don't support transactions for DDL statements. -So you must use a different database connection in your projectors, +So you must use a different database connection in your projectors, otherwise you will get an error when the subscription tries to create the table. ::: @@ -415,7 +409,6 @@ final class ProfileProjector } } ``` - :::note You can not mix the `cleanup` method with the `teardown` method. ::: @@ -470,7 +463,6 @@ final class InvoiceProcessor } } ``` - :::warning Currently, the `OnFailed` method is only available for non-batchable subscribers. ::: @@ -494,7 +486,6 @@ final class ProfileSubscriber // ... } ``` - :::warning If you change the `subscriberID`, you must also change the table/collection name. Otherwise the table/collection will conflict with the old subscription. @@ -521,14 +512,13 @@ final class ProfileSubscriber // ... } ``` - :::note The different attributes has different default group. * `Subscriber` - `default` * `Projector` - `projector` * `Processor` - `processor` -::: + ::: ### Run Mode @@ -550,7 +540,6 @@ final class WelcomeEmailSubscriber // ... } ``` - :::tip If you want create projections and run from the beginning, you can use the `Projector` attribute. ::: @@ -571,7 +560,6 @@ final class WelcomeEmailSubscriber // ... } ``` - :::tip If you want process events from now, you can use the `Processor` attribute. ::: @@ -723,7 +711,7 @@ The Subscription Engine was inspired by the following two blog posts: * [Projection Building Blocks: What you'll need to build projections](https://barryosull.com/blog/projection-building-blocks-what-you-ll-need-to-build-projections/) * [Managing projectors is harder than you think](https://barryosull.com/blog/managing-projectors-is-harder-than-you-think/) -::: + ::: ## Subscription ID @@ -850,7 +838,7 @@ In order for the subscription engine to be able to do its work, you have to asse ### Message Loader The subscription engine needs a message loader to load the messages. -We provide two implementations by default. +We provide three implementations by default. Which one has a better performance depends on the use case. :::tip @@ -953,9 +941,8 @@ $schemaDirector = new DoctrineSchemaDirector( ]), ); ``` - :::note -You can find more about schema configurator [here](store.md) +You can find more about schema configurator [here](store.md) ::: ### Retry Strategy @@ -1012,7 +999,6 @@ $retryStrategyRepository = new RetryStrategyRepository([ 'no_retry' => new NoRetryStrategy(), ]); ``` - :::note This is what our default configuration looks like if you do not define the retry strategy. ::: @@ -1036,7 +1022,6 @@ final class DropCollection } } ``` - :::warning The task class must be serializable. It will be stored in the subscription store. ::: @@ -1080,7 +1065,6 @@ $cleaner = new DefaultCleaner([ new MongodbCleanupTaskHandler($mongodbDatabase), ]); ``` - :::warning You need to pass the Cleaner to the Subscription Engine. ::: @@ -1182,11 +1166,14 @@ use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngine; /** @var SubscriptionEngine $subscriptionEngine */ $catchupSubscriptionEngine = new CatchUpSubscriptionEngine($subscriptionEngine); ``` - :::tip You can use the `CatchUpSubscriptionEngine` in your tests to process the events immediately. ::: +:::note +Learn more about the worker [here](./cli#subscription-commands). +::: + ### Throw on error Subscription Engine This is another decorator for the subscription engine. It throws an exception if a subscription is in error state. @@ -1199,7 +1186,6 @@ use Patchlevel\EventSourcing\Subscription\Engine\ThrowOnErrorSubscriptionEngine; /** @var SubscriptionEngine $subscriptionEngine */ $throwOnErrorSubscriptionEngine = new ThrowOnErrorSubscriptionEngine($subscriptionEngine); ``` - :::warning This is only for testing or development. Don't use it in production. The subscription engine has an build in retry strategy to retry subscriptions that have failed. @@ -1227,7 +1213,6 @@ $eventBus = new RunSubscriptionEngineRepositoryManager( 100, // limit the number of messages ); ``` - :::danger By using this, you can't wrap the repository in a transaction. A rollback is not supported and can break the subscription engine. @@ -1256,7 +1241,6 @@ $criteria = new SubscriptionEngineCriteria( groups: ['default'], ); ``` - :::note An `OR` check is made for the respective criteria and all criteria are checked with an `AND`. ::: @@ -1274,7 +1258,6 @@ use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria; /** @var SubscriptionEngine $subscriptionEngine */ $subscriptionEngine->setup(new SubscriptionEngineCriteria()); ``` - :::tip You can skip the booting step with the second boolean parameter named `skipBooting`. ::: @@ -1380,6 +1363,14 @@ use Patchlevel\EventSourcing\Subscription\Engine\SubscriptionEngineCriteria; /** @var SubscriptionEngine $subscriptionEngine */ $subscriptionEngine->refresh(new SubscriptionEngineCriteria()); ``` +## Basic workflow for the worker + +Use `event-sourcing:subscription:boot --setup` to first run the setup of any new subscriptions and immediately boot +them. + +The `event-sourcing:subscription:run` command will continue to run and process new events until the process is killed. +After adding a new subscriber and booting it, you should restart the `run` command. + ## Learn more * [How to use CLI commands](cli.md) diff --git a/docs/testing.md b/docs/testing.md index 39a8fc86..d332df5e 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -209,7 +209,6 @@ final class ProfileTest extends AggregateRootTestCase } } ``` - :::note You can find out more about the clock [here](clock.md). ::: @@ -242,8 +241,7 @@ final class ProfileTest extends TestCase } } ``` - :::warning The `IncrementalRamseyUuidFactory` is only for testing purposes and supports only the version 7 what is used by the library. -::: \ No newline at end of file +::: diff --git a/docs/upcasting.md b/docs/upcasting.md index 9042996d..09a308f4 100644 --- a/docs/upcasting.md +++ b/docs/upcasting.md @@ -34,7 +34,6 @@ final class ProfileCreatedEmailLowerCastUpcaster implements Upcaster } } ``` - :::warning You need to consider that other events are passed to the Upcaster. So and early out is here endorsed. ::: @@ -66,7 +65,6 @@ final class EventNameRenameUpcaster implements Upcaster } } ``` - :::tip Events can also have [aliases](events.md#alias). This is usually sufficient. :::