diff --git a/.gitignore b/.gitignore index 896e9068..7e3a05ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ build composer.lock -docs vendor coverage .phpunit.result.cache diff --git a/docs/_index.md b/docs/_index.md new file mode 100644 index 00000000..fd96128b --- /dev/null +++ b/docs/_index.md @@ -0,0 +1,6 @@ +--- +title: v2 +slogan: Event Sourcing for Artisans +githubUrl: https://github.com/spatie/laravel-event-projector +branch: master +--- diff --git a/docs/about-us.md b/docs/about-us.md new file mode 100644 index 00000000..5c3713de --- /dev/null +++ b/docs/about-us.md @@ -0,0 +1,11 @@ +--- +title: About us +weight: 9 +--- + +[Spatie](https://spatie.be) is a webdesign agency based in Antwerp, Belgium. + +Open source software is used in all projects we deliver. Laravel, Nginx, Ubuntu are just a few +of the free pieces of software we use every single day. For this, we are very grateful. +When we feel we have solved a problem in a way that can help other developers, +we release our code as open source software [on GitHub](https://spatie.be/opensource). \ No newline at end of file diff --git a/docs/advanced-usage/_index.md b/docs/advanced-usage/_index.md new file mode 100644 index 00000000..12ccdad9 --- /dev/null +++ b/docs/advanced-usage/_index.md @@ -0,0 +1,4 @@ +--- +title: Advanced usage +weight: 5 +--- diff --git a/docs/advanced-usage/discovering-projectors-and-reactors.md b/docs/advanced-usage/discovering-projectors-and-reactors.md new file mode 100644 index 00000000..2d811753 --- /dev/null +++ b/docs/advanced-usage/discovering-projectors-and-reactors.md @@ -0,0 +1,18 @@ +--- +title: Discovering projectors and reactors +weight: 4 +--- + +By default the package will automatically discover all projectors and reactors and will register them at the projectionist. + +If you want to see a list of the discovered projectors and reactors perform the `event-projector:list` Artisan command. Here's how the output could look like: + + + +## Caching discovered projectors and reactors + +In production, you likely do not want the package to scan all of your classes on every request. Therefore, during your deployment process, you should run the `event-projector:cache-event-handlers` Artisan command to cache a manifest of all of your application's projectors and reactors. This manifest will be used by the package to speed up the registration process. The `event-projector:clear-event-handlers` command may be used to destroy the cache. + +## Disabling disovery + +If you want to turn off autodiscovery and want to enforce manualy registration of projectors and reactors, just set the `auto_discover_projectors_and_reactors` key in the `event-projector` config file to an empty array. diff --git a/docs/advanced-usage/handling-exceptions.md b/docs/advanced-usage/handling-exceptions.md new file mode 100644 index 00000000..9dca67e5 --- /dev/null +++ b/docs/advanced-usage/handling-exceptions.md @@ -0,0 +1,14 @@ +--- +title: Handling exceptions +weight: 3 +--- + +The `event-projector` config file has a key, `catch_exceptions`, that determines what will happen should a projector or reactor throw an exception. If this setting is set to `false`, exceptions will not be caught and your app will come to a grinding halt. + +If `catch_exceptions` is set to `true`, and an projector or reactor throws an exception, all other projectors and reactors will still get called. The `Projectionist` will catch all exceptions and fire the `EventHandlerFailedHandlingEvent`. That event contains these public properties: + +- `eventHandler`: The projector or reactor that could not handle the event. +- `storedEvent`: The instance of `Spatie\EventProjector\Models\StoredEvent` that could not be handled. +- `exception`: The exception thrown by the `EventHandler`. + +It will also call the `handleException` method on the projector or reactor that threw the exception. It will receive the thrown error as the first argument. If you throw an exception in `handleException`, the `Projectionist` will not catch it and your php process will fail. diff --git a/docs/advanced-usage/preparing-events.md b/docs/advanced-usage/preparing-events.md new file mode 100644 index 00000000..d8585290 --- /dev/null +++ b/docs/advanced-usage/preparing-events.md @@ -0,0 +1,59 @@ +--- +title: Preparing events +weight: 1 +--- + +The package will listen for events that implement the `\Spatie\EventProjector\ShouldBeStored` interface. This is an empty interface that simply signals to the package that the event should be stored. + +You can quickly create an event that implements `ShouldBeStored` by running this artisan command: + +```bash +php artisan make:storable-event NameOfYourEvent +``` + +Here's an example of such event: + +```php +namespace App\Events; + +use Spatie\EventProjector\ShouldBeStored; + +class MoneyAdded implements ShouldBeStored +{ + /** @var string */ + public $accountUuid; + + /** @var int */ + public $amount; + + public function __construct(string $accountUuid, int $amount) + { + $this->accountUuid = $accountUuid; + + $this->amount = $amount; + } +} +``` + +Whenever an event that implements `ShouldBeStored` is fired it will be serialized and written in the `stored_events` table. Immediately after that, the event will be passed to all projectors and reactors. + +If your event has an eloquent model, it should also use the `Illuminate\Queue\SerializesModels` trait so we are able to serialize these models correctly. + +## Specifying a queue + +When a `StoredEvent` is created, we'll dispatch a job on the queue defined in the `queue` key of the `event-projector` config file. Queued projectors and reactors will get called when the job is executed on the queue. + +On an event you can override the queue that should be used by adding a `queue` property. + +```php +namespace App\Events; + +use Spatie\EventProjector\ShouldBeStored; + +class MyEvent implements ShouldBeStored +{ + public $queue = 'alternativeQueue'; + + ... +} +``` diff --git a/docs/advanced-usage/replaying-events.md b/docs/advanced-usage/replaying-events.md new file mode 100644 index 00000000..bf59c6b2 --- /dev/null +++ b/docs/advanced-usage/replaying-events.md @@ -0,0 +1,57 @@ +--- +title: Replaying events +--- + +All [events](/laravel-event-projector/v2/handling-events/preparing-events) that implement `Spatie\EventProjector\ShouldBeStored` will be [serialized](https://docs.spatie.be/laravel-event-projector/v2/advanced-usage/using-your-own-event-serializer) and stored in the `stored_events` table. After your app has been doing its work for a while the `stored_events` table will probably contain some events. + + When creating a new [projector](/laravel-event-projector/v2/handling-events/using-projectors) or [reactor](/laravel-event-projector/v2/handling-events/using-reactors) you'll want to feed all stored events to that new projector or reactor. We call this process replaying events. + + Events can be replayed to [all projectors that were added to the projectionist](/laravel-event-projector/v2/handling-events/using-reactors) with this artisan command: + + ```bash + php artisan event-projector:replay + ``` + + You can also projectors by using the `--projector` option. All stored events will be passed only to that projector. + + ```bash + php artisan event-projector:replay --projector=App\\Projectors\\AccountBalanceProjector + ``` + + You can use the projector option multiple times: + + ```bash + php artisan event-projector:replay --projector=App\\Projectors\\AccountBalanceProjector --projector=App\\Projectors\\AnotherProjector + ``` + +If your projector has a `resetState` method it will get called before replaying events. You can use that method to reset the state of your projector. + +If you want to replay events starting from a certain event you can use the `--from` option when executing `event-projector:replay`. If you use this option the `resetState` on projectors will not get called. This package does not track which events have already been processed by which projectors. Be sure not to replay events to projectors that already have handled them. + +## Detecting event replays + +If your projector contains an `onStartingEventReplay` method, we'll call it right before the first event is replayed. + +If it contains an `onFinishedEventReplay` method, we'll call it right after all events have been replayed. + +You can also detect the start and end of event replay by listening for the `Spatie\EventProjector\Events\StartingEventReplay` and `Spatie\EventProjector\Events\FinishedEventReplay` events. + +Though, under normal circumstances, you don't need to know this, you can detect if events are currently being replayed like this: + +```php +Spatie\EventProjector\Facades\Projectionist::isReplayingEvents(); // returns a boolean +``` + +## Models with timestamps + +When using models with timestamps, it is important to keep in mind that the projector will create or update these models when replaying and the timestamps will not correspond to the event's original timestamps. This will probably not be behavior you intended. To work around this you can use the stored event's timestamps: + +```php +public function onAccountCreated(StoredEvent $storedEvent, AccountCreated $event) { + Account::create(array_merge($event->accountAttributes, ['created_at' => $storedEvent->created_at, 'updated_at' => $storedEvent->created_at])); +} +``` + +## What about reactors? + +Reactors are used to handle side effects, like sending mails and such. You'll only want reactors to do their work when an event is originally fired. You don't want to send out mails when replaying events. That's why reactors will never get called when replaying events. diff --git a/docs/advanced-usage/storing-metadata.md b/docs/advanced-usage/storing-metadata.md new file mode 100644 index 00000000..667a7127 --- /dev/null +++ b/docs/advanced-usage/storing-metadata.md @@ -0,0 +1,68 @@ +--- +title: Storing metadata +weight: 2 +--- + +You can add metadata, such as the `id` of the logged in user, to a stored event. + +## Storing metadata on all events + +If you need to store metadata on all events you can leverage Laravel's native models events. + +You must configure the package [use your own event storage model](/laravel-event-projector/v2/advanced-usage/using-your-own-event-storage-model). On that model you can hook into the model lifecycle hooks. + +```php +use Spatie\EventProjector\Models\StoredEvent; + +class CustomStoredEvent extends StoredEvent +{ + public static function boot() + { + parent::boot(); + + static::creating(function(CustomStoredEvent $storedEvent) { + $storedEvent->meta_data['user_id'] = auth()->user()->id; + }); + } +} +``` + +## Storing metadata via a projector + +The `StoredEvent` instance will be passed on to any projector method that has a variable named `$storedEvent`. On that `StoredEvent` instance there is a property, `meta_data`, that returns an instance of `Spatie\SchemalessAttributes\SchemalessAttributes`. + +Here's an example: + +```php +namespace App\Projectors; + +use Spatie\EventProjector\Projectors\Projector; +use Spatie\EventProjector\Projectors\ProjectsEvents; +use Spatie\EventProjector\Models\StoredEvent; +use Spatie\EventProjector\Facades\Projectionist; +use App\Events\MoneyAdded; + +class MetaDataProjector implements Projector +{ + use ProjectsEvents; + + /* + * Here you can specify which event should trigger which method. + */ + public $handlesEvents = [ + MoneyAdded::class => 'onMoneyAdded', + ]; + + public function onMoneyAdded(StoredEvent $storedEvent) + { + + if (Projectionist::isReplaying()) { + $storedEvent->meta_data['user_id'] = auth()->user()->id; + + $storedEvent->save(); + } + + // ... + } +} +``` diff --git a/docs/advanced-usage/using-your-own-event-serializer.md b/docs/advanced-usage/using-your-own-event-serializer.md new file mode 100644 index 00000000..eabf402c --- /dev/null +++ b/docs/advanced-usage/using-your-own-event-serializer.md @@ -0,0 +1,23 @@ +--- +title: Using your own event serializer +weight: 6 +--- + +Events will be serialized by the `Spatie\EventProjector\EventSerializers\JsonEventSerializer`. Like the name implies, this class can serialize an event to json so it can be easily stored in a `json` column in the database. + +You can specify your own serializer by creating a class that implements `Spatie\EventProjector\EventSerializers\EventSerializer` and specifying the class in the `event_serializer` key of the `event-projector.php` config file. + +This is the content of the `EventSerializer` interface: + +``` +namespace Spatie\EventProjector\EventSerializers; + +use Spatie\EventProjector\ShouldBeStored; + +interface EventSerializer +{ + public function serialize(ShouldBeStored $event): string; + + public function deserialize(string $eventClass, string $json): ShouldBeStored; +} +``` diff --git a/docs/advanced-usage/using-your-own-event-storage-model.md b/docs/advanced-usage/using-your-own-event-storage-model.md new file mode 100644 index 00000000..433c4a3c --- /dev/null +++ b/docs/advanced-usage/using-your-own-event-storage-model.md @@ -0,0 +1,6 @@ +--- +title: Using your own event storage model +weight: 5 +--- + +The default model responsible for storing events is `\Spatie\EventProjector\Models\StoredEvent`. If you want to add behaviour to that model you can create a class of your own that extends the `StoredEvent` model. You should put the class name of your model in the `stored_event_model` in the `event-projector.php` config file. diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..cf4068da --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,6 @@ +--- +title: Changelog +weight: 6 +--- + +All notable changes to laravel-event-projector are documented [on GitHub](https://github.com/spatie/laravel-event-projector/blob/master/CHANGELOG.md) diff --git a/docs/getting-familiar-with-event-sourcing/_index.md b/docs/getting-familiar-with-event-sourcing/_index.md new file mode 100644 index 00000000..2fc2555d --- /dev/null +++ b/docs/getting-familiar-with-event-sourcing/_index.md @@ -0,0 +1,4 @@ +--- +title: Getting familiar with event sourcing +weight: 1 +--- diff --git a/docs/getting-familiar-with-event-sourcing/introduction.md b/docs/getting-familiar-with-event-sourcing/introduction.md new file mode 100644 index 00000000..bd2ea596 --- /dev/null +++ b/docs/getting-familiar-with-event-sourcing/introduction.md @@ -0,0 +1,18 @@ +--- +title: Introduction +weight: 1 +--- + +Event sourcing is to data what Git is to code. Most applications only have their current state stored in a database. A lot of useful information gets lost: you don't know _how_ the application got to this state. + +Event sourcing tries to solve this problem by storing all events that happen in your app. The state of your application is built by listening to those events. An aggregate is used to validate if a new event is allowed to get written and to make decisions based on the past. Projectors are used to transform newly written events into a format useful for consumption in your app. + +Here's a concrete example to make it more clear. Imagine you're a bank. Your clients have accounts. Storing the balance of the accounts wouldn't be enough; all the transactions should be remembered too. With event sourcing, the balance isn't a standalone database field, but a value calculated from the stored transactions. + +After taking a look at [an example of traditional application](/laravel-event-projector/v2/getting-familiar-with-event-sourcing/the-traditional-application), we're going to discuss the two concepts that make up this package: [projectors](/laravel-event-projector/v2/getting-familiar-with-event-sourcing/using-projectors-to-transform-events) and [aggregates](/laravel-event-projector/v2/getting-familiar-with-event-sourcing/using-aggregates-to-make-decisions-based-on-the-past). + +If you want to skip to reading code immediately, here are the Larabank example apps used in this section. In all of them, you can create accounts and deposit or withdraw money. + +- [Larabank built traditionally without event sourcing](https://github.com/spatie/larabank-traditional) +- [Larabank built with projectors](https://github.com/spatie/larabank-event-projector) +- [Larabank built with aggregates and projectors](https://github.com/spatie/larabank-event-projector-aggregates) diff --git a/docs/getting-familiar-with-event-sourcing/the-traditional-application.md b/docs/getting-familiar-with-event-sourcing/the-traditional-application.md new file mode 100644 index 00000000..8ddbd4a5 --- /dev/null +++ b/docs/getting-familiar-with-event-sourcing/the-traditional-application.md @@ -0,0 +1,24 @@ +--- +title: The traditional application +weight: 2 +--- + +In a traditional application, you're probably going to use a database to hold the state of your application. Whenever you want to update a state, you're simply going to overwrite the old value. That old value isn't accessible anymore. Your application only holds the current state. + +You might think that you still have the old state inside your backups. But they don't count. Your app probably can't, nor should it, make decisions on data inside those backups. + +
+
+ First, we write value X +
+ +
+ +
+
+ Next, we overwrite X by Y. X cannot be accessed anymore. +
+ +
+ +Here's a demo application that uses a traditional architecture. Inside the [`AccountsController`](https://github.com/spatie/larabank-traditional/blob/6ceb08f4700a9be72f0ebfe49b997d5871d64c6b/app/Http/Controllers/AccountsController.php) we are just going to [create new accounts](https://github.com/spatie/larabank-traditional/blob/6ceb08f4700a9be72f0ebfe49b997d5871d64c6b/app/Http/Controllers/AccountsController.php#L19-L27) and [update the balance](https://github.com/spatie/larabank-traditional/blob/6ceb08f4700a9be72f0ebfe49b997d5871d64c6b/app/Http/Controllers/AccountsController.php#L19-L27). We're using an eloquent model to update the database. Whenever we change the balance of the account, the old value is lost. diff --git a/docs/getting-familiar-with-event-sourcing/using-aggregates-to-make-decisions-based-on-the-past.md b/docs/getting-familiar-with-event-sourcing/using-aggregates-to-make-decisions-based-on-the-past.md new file mode 100644 index 00000000..2d314e96 --- /dev/null +++ b/docs/getting-familiar-with-event-sourcing/using-aggregates-to-make-decisions-based-on-the-past.md @@ -0,0 +1,56 @@ +--- +title: Using aggregates to make decisions based on the past +weight: 4 +--- + +Now that you know what projections are, let's take it one step further with aggregates. In the previous examples whenever we wanted to fire off an event, we simply did so. When using aggregates, our main code is not going to fire events anymore. Instead, an aggregate will do that. An aggregate is a class that helps you to make decisions based on events that happened in the past. + +Before firing off an event, an aggregate will first check if it is allowed to fire off that particular event. Using our Larabank example again, imagine you have to implement the rule that an account's balance is not allowed to go below -$5000. When trying to subtract money for a particular account, the aggregate will first loop through all previous events of that account and calculate the current balance. If the balance minus the amount we subtract is not less than -$5000, it will record that `MoneySubtracted` event. After that, the `MoneySubtracted` event will be passed to all projectors and reactors. + +Let's go through this step by step. + +Step 1: our app wants to subtract $1000. We create a new aggregate root instance and will feed it all events. There are no events yet to retrieve in this pass. The aggregate will conclude that it's allowed to subtract $1000 and will record that `Subtract` event. This recording is just in memory, and nothing will be written to the DB yet. + +
+ +
+ +Step 2: We are going to persist the aggregate. When persisting an aggregate, all of the newly recorded events that aggregate will be written in the database. Also, if you have projectors set up, they will receive the newly persisted events as well. + +
+ +
+ +Step 3: Let's hit that account limit and try to subtract $4800 now. First, the aggregate will be reconstituted from all previous events. Because it gets the earlier events it can calculate the current balance in memory (which is of course -$1000). The aggregate root can conclude that if we were to subtract $4800 we would cross our limit of -$5000. So it is not going to record that event. Instead, we could record that fact the account limit was hit. + +
+ +
+ +Step 4: The aggregate gets persisted, and the account limit hit event gets written into the database. + +
+ +
+ +So now we've protected our account from going below -\$5000. Let's take it one step further and send our customer a loan proposal mail when he or she hits the account limit three times in a row. Using an aggregate this is easy! + +Step 5: Let's again try to subtract a lot of money to hit that account limit of \$5000. We hit our account limit the second time. + +
+ +
+ +Step 6: This time it gets interesting. We are going to try to subtract money and will hit our limit for the third time. Our aggregate gets reconstituted from all events. Those events get fed to the aggregate one by one. The aggregate in memory holds a counter of how many limit hit events it receives. That counter is now on 2. Because the amount we subtract will take us over the account limit, the aggregate will not record a subtract event, but a new limit hit event. It will update the limit hit counter from 2 to 3. Because the counter is now at 3. It can also record a new event called loan proposed. When storing the aggregate, the new events will get persisted in the database. All projectors and reactor will get called with these events. The `LoanProposalReactor` hears that `LoanProposed` event and send the mail. + +
+ +
+ +All of the above is a lot to wrap your mind around. To help you understand this better, here's our Larabank app again, but this time built [using aggregates](https://github.com/spatie/larabank-event-projector-aggregates). In the controller, you see that we don't fire events, but we are [using an aggregate](https://github.com/spatie/larabank-event-projector-aggregates/blob/c9f2ff240f4634ee2e241e3087ff60587a176ae0/app/Http/Controllers/AccountsController.php#L21-L52). Inside the aggregate we are going to [record events](https://github.com/spatie/larabank-event-projector-aggregates/blob/c9f2ff240f4634ee2e241e3087ff60587a176ae0/app/Domain/Account/AccountAggregateRoot.php#L46) that will get written to the database as soon as we [persist](https://github.com/spatie/larabank-event-projector-aggregates/blob/c9f2ff240f4634ee2e241e3087ff60587a176ae0/app/Http/Controllers/AccountsController.php#L40) the aggregate. + +Whenever we retrieve an aggregate, all of the previously stored events will be fed to the aggregate one by one to it's `apply*` methods. We can use [those apply methods](https://github.com/spatie/larabank-event-projector-aggregates/blob/c9f2ff240f4634ee2e241e3087ff60587a176ae0/app/Domain/Account/AccountAggregateRoot.php#L77-L82) to recalculate things like the balance, or the times the account limit was hit [as instance variables](https://github.com/spatie/larabank-event-projector-aggregates/blob/c9f2ff240f4634ee2e241e3087ff60587a176ae0/app/Domain/Account/AccountAggregateRoot.php#L28-L35). When we want to try to subtract money we can use those instances variables to decide whether we are going to [record the `MoneySubtracted` event or record other events](https://github.com/spatie/larabank-event-projector-aggregates/blob/c9f2ff240f4634ee2e241e3087ff60587a176ae0/app/Domain/Account/AccountAggregateRoot.php#L60-L75). + +In summary, aggregates are used to make decisions based on past events. + +If you want to know how to create and use aggregates, head over to [the `using-aggregates` section](https://docs.spatie.be/laravel-event-projector/v2/using-aggregates/writing-your-first-aggregate). diff --git a/docs/getting-familiar-with-event-sourcing/using-projectors-to-transform-events.md b/docs/getting-familiar-with-event-sourcing/using-projectors-to-transform-events.md new file mode 100644 index 00000000..ee941b76 --- /dev/null +++ b/docs/getting-familiar-with-event-sourcing/using-projectors-to-transform-events.md @@ -0,0 +1,40 @@ +--- +title: Using projectors to transform events +weight: 3 +--- + +Let's build a bit further on the [Larabank example](https://github.com/spatie/larabank-traditional) mentioned in [the previous section](https://docs.spatie.be/laravel-event-projector/v2/getting-familiar-with-event-sourcing/the-traditional-application). The main drawback highlighted that example is the fact that when updating a value, we lose the old value. Let's solve that problem. + +Instead of directly updating the value in the database, we could write every change we want to make as an event in our database. + +
+
+ Here we write our first event in the database +
+ +
+ +
+
+ When new events come in, we'll write them to the events table as well +
+ +
+ +All events get passed to a class we call a projector. The projector transforms the events to a format that is handy to use in our app. In our Larabank example, the events table hold the info of the individual transactions like `MoneyAdded` and `MoneySubtracted`. A projector could build an `Accounts` table based on those transactions. + +
+ +
+ +Imagine that you've already stored some events, and your first projector is doing its job creating that `Accounts` table. The bank directory now wants to know on which accounts the most transactions were performed. No problem, we could create another projector that reads all previous events and acts the `MoneyAdded` and `MoneySubtracted` to make projections. + +
+ +
+ +This package can help you store native Laravel events in a `stored_events` table and create projectors that transform those events. + +Here's our example app [Larabank rebuild with projectors](https://github.com/spatie/larabank-event-projector). In [the `AccountsController`](https://github.com/spatie/larabank-event-projector/blob/d02fd1de7f31f4b915c05df79d9ba61440f9e6b5/app/Http/Controllers/AccountsController.php#L20-L36) we're not going to directly modify the database anymore. Instead, the controller will call methods which will in [their turn fire off events](https://github.com/spatie/larabank-event-projector/blob/master/app/Account.php#L15-L42). Our package will listen for those events (which implement the empty `ShouldBeStored` interface) and store them in the `stored_events` table. Those events will also get passed to [all registered projectors](https://github.com/spatie/larabank-event-projector/blob/d02fd1de7f31f4b915c05df79d9ba61440f9e6b5/config/event-projector.php#L14). The [`AccountsProjector`](https://github.com/spatie/larabank-event-projector/blob/d02fd1de7f31f4b915c05df79d9ba61440f9e6b5/app/Projectors/AccountsProjector.php) will build the `Accounts` table using [a couple of events it listens for](https://github.com/spatie/larabank-event-projector/blob/d02fd1de7f31f4b915c05df79d9ba61440f9e6b5/app/Projectors/AccountsProjector.php#L17-L22). + +If you want to know more about projectors and how to use them, head over to [the `using-projectors` section](https://docs.spatie.be/laravel-event-projector/v2/using-projectors/writing-your-first-projector). diff --git a/docs/images/aggregate-01.svg b/docs/images/aggregate-01.svg new file mode 100644 index 00000000..bca473fd --- /dev/null +++ b/docs/images/aggregate-01.svg @@ -0,0 +1,493 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/aggregate-02.svg b/docs/images/aggregate-02.svg new file mode 100644 index 00000000..99566b04 --- /dev/null +++ b/docs/images/aggregate-02.svg @@ -0,0 +1,728 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/aggregate-03.svg b/docs/images/aggregate-03.svg new file mode 100644 index 00000000..5076d827 --- /dev/null +++ b/docs/images/aggregate-03.svg @@ -0,0 +1,731 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/aggregate-04.svg b/docs/images/aggregate-04.svg new file mode 100644 index 00000000..c04221d0 --- /dev/null +++ b/docs/images/aggregate-04.svg @@ -0,0 +1,787 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/aggregate-05.svg b/docs/images/aggregate-05.svg new file mode 100644 index 00000000..fcab2de3 --- /dev/null +++ b/docs/images/aggregate-05.svg @@ -0,0 +1,857 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/aggregate-06.svg b/docs/images/aggregate-06.svg new file mode 100644 index 00000000..90be8e97 --- /dev/null +++ b/docs/images/aggregate-06.svg @@ -0,0 +1,1173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/db-01.svg b/docs/images/db-01.svg new file mode 100644 index 00000000..c39a1e76 --- /dev/null +++ b/docs/images/db-01.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/db-02.svg b/docs/images/db-02.svg new file mode 100644 index 00000000..387292c9 --- /dev/null +++ b/docs/images/db-02.svg @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/header.jpg b/docs/images/header.jpg new file mode 100644 index 00000000..406977df Binary files /dev/null and b/docs/images/header.jpg differ diff --git a/docs/images/list-command.png b/docs/images/list-command.png new file mode 100644 index 00000000..f7eccbbb Binary files /dev/null and b/docs/images/list-command.png differ diff --git a/docs/images/list.png b/docs/images/list.png new file mode 100644 index 00000000..2ee9a7a5 Binary files /dev/null and b/docs/images/list.png differ diff --git a/docs/images/transform-01.svg b/docs/images/transform-01.svg new file mode 100644 index 00000000..12d1bf45 --- /dev/null +++ b/docs/images/transform-01.svg @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/transform-02.svg b/docs/images/transform-02.svg new file mode 100644 index 00000000..aa657dcd --- /dev/null +++ b/docs/images/transform-02.svg @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/transform-03.svg b/docs/images/transform-03.svg new file mode 100644 index 00000000..0ed8ab45 --- /dev/null +++ b/docs/images/transform-03.svg @@ -0,0 +1,355 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/transform-04.svg b/docs/images/transform-04.svg new file mode 100644 index 00000000..3122fb1b --- /dev/null +++ b/docs/images/transform-04.svg @@ -0,0 +1,514 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/installation-setup.md b/docs/installation-setup.md new file mode 100644 index 00000000..814d453a --- /dev/null +++ b/docs/installation-setup.md @@ -0,0 +1,110 @@ +--- +title: Installation & setup +weight: 4 +--- + +laravel-event-projector can be installed via composer: + +```bash +composer require spatie/laravel-event-projector:^2.0.0 +``` + +You need to publish and run the migrations to create the `stored_events` table: + +```bash +php artisan vendor:publish --provider="Spatie\EventProjector\EventProjectorServiceProvider" --tag="migrations" +php artisan migrate +``` + +You must publish the config file with this command: + +```bash +php artisan vendor:publish --provider="Spatie\EventProjector\EventProjectorServiceProvider" --tag="config" +``` + +This is the default content of the config file that will be published at `config/event-projector.php`: + +```php +return [ + + /* + * These directories will be scanned for projectors and reactors. They + * will be automatically registered to projectionist automatically. + */ + 'auto_discover_projectors_and_reactors' => [ + app_path(), + ], + + /* + * Projectors are classes that build up projections. You can create them by performing + * `php artisan event-projector:create-projector`. When not using autodiscovery + * Projectors can be registered in this array or a service provider. + */ + 'projectors' => [ + // App\Projectors\YourProjector::class + ], + + /* + * Reactors are classes that handle side effects. You can create them by performing + * `php artisan event-projector:create-reactor`. When not using autodiscovery + * Reactors can be registered in this array or a service provider. + */ + 'reactors' => [ + // App\Reactors\YourReactor::class + ], + + /* + * A queue is used to guarantee that all events get passed to the projectors in + * the right order. Here you can set of the name of the queue. + */ + 'queue' => env('EVENT_PROJECTOR_QUEUE_NAME', null), + + /* + * When a projector or reactor throws an exception the event projectionist can catch it + * so all other projectors and reactors can still do their work. The exception will + * be passed to the `handleException` method on that projector or reactor. + */ + 'catch_exceptions' => env('EVENT_PROJECTOR_CATCH_EXCEPTIONS', false), + + /* + * This class is responsible for storing events. To add extra behaviour you + * can change this to a class of your own. The only restriction is that + * it should extend \Spatie\EventProjector\Models\StoredEvent. + */ + 'stored_event_model' => \Spatie\EventProjector\Models\StoredEvent::class, + + /* + * This class is responsible for handle stored events. To add extra behaviour you + * can change this to a class of your own. The only restriction is that + * it should extend \Spatie\EventProjector\HandleDomainEventJob. + */ + 'stored_event_job' => \Spatie\EventProjector\HandleStoredEventJob::class, + + /* + * This class is responsible for serializing events. By default an event will be serialized + * and stored as json. You can customize the class name. A valid serializer + * should implement Spatie\EventProjector\EventSerializers\Serializer. + */ + 'event_serializer' => \Spatie\EventProjector\EventSerializers\JsonEventSerializer::class, + + /* + * When replaying events potentially a lot of events will have to be retrieved. + * In order to avoid memory problems events will be retrieved in + * a chunked way. You can specify the chunk size here. + */ + 'replay_chunk_size' => 1000, + + /* + * In production, you likely don't want the package to auto discover the event handlers + * on every request. The package can cache all registered event handlers. + * More info: https://docs.spatie.be/laravel-event-projector/v2/advanced-usage/discovering-projectors-and-reactors + * + * Here you can specify where the cache should be stored. + */ + 'cache_path' => storage_path('app/event-projector'), +]; +``` + +The package will scan all classes of your project to [automatically discover projectors and reactors](/laravel-event-projector/v2/advanced-usage/discovering-projectors-and-reactors#discovering-projectors-and-reactors). In a production production environment you problably should [cache auto discovered projectors and reactors](/laravel-event-projector/v2/advanced-usage/discovering-projectors-and-reactors#caching-discovered-projectors-and-reactors). + +It's recommended that should set up a queue. Specify the connection name in the `queue` key of the `event-projector` config file. This queue will be used to guarantee that the events will be processed by all projectors in the right order. You should make sure that the queue will process only one job at a time. In a local environment, where events have a very low chance of getting fired concurrently, it's probably ok to just use the `sync` driver. diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 00000000..bf5c502f --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,28 @@ +--- +title: Introduction +weight: 1 +--- + +This package aims to be the entry point to get started with event sourcing in Laravel. It can help you setting up aggregates, projectors and reactors. + +If you've never worked with event sourcing, or are uncertain about what projectors, reactors and aggregates are, head over to [the getting familiar with event sourcing section](https://docs.spatie.be/laravel-event-projector/v2/getting-familiar-with-event-sourcing/introduction). + +Are you visual learner? Then start by watching this video. It explains event sourcing in general and how you can use projectors, reactors and aggregates. + + + +Event sourcing might be a good choice for your project if: + +- your app needs to make decisions based on the past +- your app has auditing requirements: the reason why your app is in a certain state is equally as important as the state itself +- you foresee that there will be a reporting need in the future, but you don't know yet which data you need to collect for those reports + +## We have badges! + +
+ Latest Version + Software License + Build Status + Quality Score + Total Downloads +
diff --git a/docs/postcardware.md b/docs/postcardware.md new file mode 100644 index 00000000..348a79f0 --- /dev/null +++ b/docs/postcardware.md @@ -0,0 +1,10 @@ +--- +title: Postcardware +weight: 2 +--- + +You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. + +Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium. + +The best postcards will get published on the [open source section](https://spatie.be/en/opensource/postcards) on our website. diff --git a/docs/questions-issues.md b/docs/questions-issues.md new file mode 100644 index 00000000..0505dcdf --- /dev/null +++ b/docs/questions-issues.md @@ -0,0 +1,8 @@ +--- +title: Questions and issues +weight: 5 +--- + +Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving Laravel Event Projector? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-event-projector/issues), we'll try to address it as soon as possible. + +If you've found a bug regarding security please mail [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker. diff --git a/docs/requirements.md b/docs/requirements.md new file mode 100644 index 00000000..9ea447bf --- /dev/null +++ b/docs/requirements.md @@ -0,0 +1,10 @@ +--- +title: Requirements +weight: 3 +--- + +This package requires: +- PHP 7.3 or higher +- Laravel 5.8 or higher +- A database that supports `json` columns, like MySQL 5.7 or higher +- A queue backend that is [supported by Laravel](https://laravel.com/docs/5.6/queues#driver-prerequisites) diff --git a/docs/resources-and-alternatives.md b/docs/resources-and-alternatives.md new file mode 100644 index 00000000..6c6fa7bf --- /dev/null +++ b/docs/resources-and-alternatives.md @@ -0,0 +1,14 @@ +--- +title: Resources and alternatives +weight: 8 +--- + +If you want to know more about event sourcing in general check out these links: + +- [Event Sourcing made Simple](https://kickstarter.engineering/event-sourcing-made-simple-4a2625113224): the blogpost that showed us that event sourcing can be used very pragmatically as well +- [The Many Meanings of Event-Driven Architecture](https://www.youtube.com/watch?v=STKCRSUsyP0): A recording of a cool talk by [Martin Fowler](https://martinfowler.com/) + +There are many other event sourcing packages out there: + +- [EventSauce](https://eventsauce.io/): A pragmatic, feature rich event sourcing library for PHP made by [Frank de Jonge](https://frankdejonge.nl) +- [prooph](https://github.com/prooph): A full blown CQRS and event sourcing solution in PHP diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 00000000..63d8c13d --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,6 @@ +--- +title: Upgrading +weight: 7 +--- + +Instructions on how to upgrade from an earlier major version of `laravel-event-projector` are available [on GitHub](https://github.com/spatie/laravel-event-projector/blob/master/UPGRADING.md) diff --git a/docs/using-aggregates/_index.md b/docs/using-aggregates/_index.md new file mode 100644 index 00000000..97d6e52a --- /dev/null +++ b/docs/using-aggregates/_index.md @@ -0,0 +1,4 @@ +--- +title: Using aggregates +weight: 4 +--- diff --git a/docs/using-aggregates/creating-and-configuring-aggregates.md b/docs/using-aggregates/creating-and-configuring-aggregates.md new file mode 100644 index 00000000..aa9dbe75 --- /dev/null +++ b/docs/using-aggregates/creating-and-configuring-aggregates.md @@ -0,0 +1,84 @@ +--- +title: Creating and configuring aggregates +weight: 2 +--- + +An aggregate is a class that decides to record events based on past events. + +## Creating an aggregate + +The easiest way to create an aggregate root would be to use the `make:aggregate` command: + +```php +php artisan make:aggregate MyAggregate +``` + +This will create a class like this: + +```php +namespace App\Aggregates; + +use Spatie\EventProjector\AggregateRoot; + + +final class MyAggregate extends AggregateRoot +{ +} +``` + +## Retrieving an aggregate + +An aggregate can be retrieved like this: + +```php +MyAggregate::retrieve($uuid) +``` + +This will cause all events with the given `uuid` to be retrieved and fed to the aggregate. For example, an event `MoneyAdded` will be passed to the `applyMoneyAdded` method on the aggregate if such a method exists. + +## Recording events + +Inside an aggregate you can record new events using the `recordThat` function. All events being passed to that function should implement `Spatie\EventProjector\ShouldBeStored`. + +Here's an example event + +```php +use Spatie\EventProjector\ShouldBeStored; + +class MoneyAdded extends ShouldBeStored +{ + /** @var int */ + private $amount + + public function __construct(int $amount) + { + $this->amount = $amount; + } +} +``` + +Inside an aggregate root you can pass the event to `recordThat`: + +```php +// somehwere inside your aggregate +public function addMoney(int $amount) +{ + $this->recordThat(new MoneyAdded($amount)); +} +``` + +Calling `recordThat` will persist the event to the DB, that will happen when the aggregate itself gets persisted. However, recording an event will cause it getting applied to the aggregate immediately. For example, when you record the event `MoneyAdded`, we'll immediately call `applyMoneyAdded` on the aggregate. + +Notice that your event isn't required to contain the `$uuid`. Your aggregate is built up for a specific `$uuid` and under the hood, the package will save that `$uuid` along with the event when the aggregate gets persisted. + +## Persisting aggregates + +To persist an aggregate call `persist` on it. Here's an example: + +```php +MyAggregate::retrieve($uuid) // will cause all events for this uuid to be fed to the `apply*` methods + // call methods that record events + ->persist(); // +``` + +Persisting an aggregate root will write all newly recorded events to the database. The newly persisted events will get passed to all projectors and reactors. diff --git a/docs/using-aggregates/writing-your-first-aggregate.md b/docs/using-aggregates/writing-your-first-aggregate.md new file mode 100644 index 00000000..faa364ce --- /dev/null +++ b/docs/using-aggregates/writing-your-first-aggregate.md @@ -0,0 +1,184 @@ +--- +title: Writing your first aggregate +weight: 1 +--- + +An aggregate is a class that decides to record events based on past events. To know more about their general purpose and the idea behind them, read this section on [using aggregates to make decisions-based-on-the-past](https://docs.spatie.be/laravel-event-projector/v2/getting-familiar-with-event-sourcing/using-aggregates-to-make-decisions-based-on-the-past). + +## Creating an aggregate + +The easiest way to create an aggregate root would be to use the `make:aggregate` command: + +```php +php artisan make:aggregate AccountAggregate +``` + +This will create a class like this: + +```php +namespace App\Aggregates; + +use Spatie\EventProjector\AggregateRoot; + + +final class AccountAggregate extends AggregateRoot +{ +} +``` + +## Recording events + +You can add any methods or variables you need on the aggregate. To get you familiar with event modeling using aggregates let's implement a small piece of [the Larabank example app](https://github.com/spatie/larabank-event-projector-aggregates). We are going to add methods to record the [`AccountCreated`](https://github.com/spatie/larabank-event-projector-aggregates/blob/c9f2ff240f4634ee2e241e3087ff60587a176ae0/app/Domain/Account/ShouldBeStoreds/AccountCreated.php), [`MoneyAdded`](https://github.com/spatie/larabank-event-projector-aggregates/blob/c9f2ff240f4634ee2e241e3087ff60587a176ae0/app/Domain/Account/ShouldBeStoreds/MoneyAdded.php) and the [`MoneySubtracted`](https://github.com/spatie/larabank-event-projector-aggregates/blob/c9f2ff240f4634ee2e241e3087ff60587a176ae0/app/Domain/Account/ShouldBeStoreds/MoneySubtracted.php) events. + +First, let's add a `createAccount` methods to our aggregate that will record the `AccountCreated` event. + +```php +namespace App\Aggregates; + +use Spatie\EventProjector\AggregateRoot; + + +final class AccountAggregate extends AggregateRoot +{ + public function createAccount(string $name, string $userId) + { + $this->recordThat(new AccountCreated($name, $userId)); + } + + public function addMoney(int $amount) + { + $this->recordThat(new MoneyAdded($amount)); + } + + public function subtractAmount(int $amount) + { + $this->recordThat(new MoneySubtracted($amount)); + } +} +``` + +The `recordThat` function will not persist the events to the database. It will simply hold them in memory. The events will get written to the database when the aggregate itself is persisted. + +There are two things to notice. First, the method name is written in the present tense, not the past tense. We're trying to do something, and for the rest of our application is hasn't happened yet until the actual `AccountCreated` is saved. This will only happen when the `AccountAggregate` gets persisted. + +The second thing to note is that nor the method and the event contain an uuid. The aggregate itself is aware of the uuid to use because it is passed to the retrieve method (`AccountAggregate::retrieve($uuid)`, we'll get to this in a bit). When persisting the aggregateroot, it will save the recorded events along with the uuid. + +With this in place you can use the aggregate like this: + +```php +AccountAggregate::retrieve($uuid) + ->createAccount('my account', auth()->user()->id) + ->persist(); +``` + +```php +AccountAggregate::retrieve($uuid) + ->addMoney(123) + ->persist(); +``` + +```php +AccountAggregate::retrieve($uuid) + ->subtractMoney(456) + ->persist(); +``` + +When persisting an aggregate all newly recorded events inside aggregate root will be saved to the database. The newly recorded events will also get passed to all projectors and reactors that listen for them. + +In our demo app we retrieve and persist the aggregate [in the `AccountsController`](https://github.com/spatie/larabank-event-projector-aggregates/blob/c9f2ff240f4634ee2e241e3087ff60587a176ae0/app/Http/Controllers/AccountsController.php). The package has no opinion on where you should interact with aggregates. Do whatever you wish. + +## Implementing our first business rule + +Let's now implement the rule that an account cannot go below -$5000. Here's the thing to keep in mind: when retrieving an aggregate all events for the given uuid will be retrieved and will be passed to methods named `apply` on the aggregate. + +So for our aggregate to receive all past `MoneyAdded` and `MoneySubtracted` events we need to add `applyMoneySubtracted` and`applyMoneySubtracted` methods to our aggregate. Because those events are all fed to the same instance of the aggregate, we can simply add an instance variable to hold the calculated balance. + +```php +// in our aggregate + +private $balance = 0; + +//... + +public function applyMoneyAdded(MoneyAdded $event) +{ + $this->balance += $event->amount; +} + +public function applyMoneySubtracted(MoneySubtracted $event) +{ + $this->balance -= $event->amount; +} +``` + +Now that we have the balance of the account in memory, we can add a simple check to `subtractAmount` to prevents an event from being recorded. + +```php +public function subtractAmount(int $amount) +{ + if (! $this->hasSufficientFundsToSubtractAmount($amount) { + throw CouldNotSubtractMoney::notEnoughFunds($amount); + } + + $this->recordThat(new MoneySubtracted($amount)); +} + +private function hasSufficientFundsToSubtractAmount(int $amount): bool +{ + return $this->balance - $amount >= $this->accountLimit; +} +``` + +## Implementing another business rule + +We can take this one step further. You could also record the event that the account limit was hit. + +```php +public function subtractAmount(int $amount) +{ + if (! $this->hasSufficientFundsToSubtractAmount($amount) { + $this->recordThat(new AccountLimitHit($amount)); + + // persist the aggregate so the record event gets persisted + $this->persist(); + + throw CouldNotSubtractMoney::notEnoughFunds($amount); + } + + $this->recordThat(new MoneySubtracted($amount)); +} +``` + +Let's now add a new business rule. Whenever somebody hits the limit three times a loan proposal should be sent. We can implement that as such. + +```php +private $accountLimitHitCount = 0; + +// we need to add this method to count the amount of this the limit was hit +public function applyAccountLimitHit() +{ + $this->accountLimitHitCount++; +} + +public function subtractAmount(int $amount) +{ + if (! $this->hasSufficientFundsToSubtractAmount($amount) { + $this->recordThat(new AccountLimitHit($amount)); + + if ($this->accountLimitHitCount === 3) { + $this->recordThat(new LoanProposed()); + } + + // persist the aggregate so the record events gets persisted + $this->persist(); + + throw CouldNotSubtractMoney::notEnoughFunds($amount); + } + + $this->recordThat(new MoneySubtracted($amount)); +} +``` + +When the limit is hit three times, we record another event `LoanProposed`. We could set up a reactor that listens for that event and sends the actual mail. + +If you want to toy around with this example, clone the [Larabank with aggregates](https://github.com/spatie/larabank-event-projector-aggregates) example. diff --git a/docs/using-projectors/_index.md b/docs/using-projectors/_index.md new file mode 100644 index 00000000..70c996f5 --- /dev/null +++ b/docs/using-projectors/_index.md @@ -0,0 +1,4 @@ +--- +title: Using projectors +weight: 2 +--- diff --git a/docs/using-projectors/creating-and-configuring-projectors.md b/docs/using-projectors/creating-and-configuring-projectors.md new file mode 100644 index 00000000..28493e3f --- /dev/null +++ b/docs/using-projectors/creating-and-configuring-projectors.md @@ -0,0 +1,198 @@ +--- +title: Creating and registering projectors +weight: 2 +--- + +A projector is a class that listens for events that were stored. When it hears an event that it is interested in, it can perform some work. + +## Creating projectors + +Let's create a projector. You can perform this artisan command to create a projector in `app\Projectors`: + +```php +php artisan make:projector AccountBalanceProjector +``` + +## Registering projectors + +By default, the package will automatically find an register all projectors found in your application. + +Alternatively, you can manually register projectors in the `projectors` key of the `event-projectors` config file. + +You can also add them to the `Projectionist`. This can be done anywhere, but typically you would do this in a ServiceProvider of your own. + +```php +namespace App\Providers; + +use App\Projectors\AccountBalanceProjector; +use Illuminate\Support\ServiceProvider; +use Spatie\EventProjector\Facades\Projectionist; + +class EventProjectorServiceProvider extends ServiceProvider +{ + public function register() + { + // adding a single projector + Projectionist::addProjector(AccountBalanceProjector::class); + + // you can also add multiple projectors in one go + Projectionist::addProjectors([ + AnotherProjector::class, + YetAnotherProjector::class, + ]); + } +} +``` + +## Using projectors + +This is the contents of a class created by the artisan command mentioned in the section above. + +```php +namespace App\Projectors; + +use Spatie\EventProjector\Projectors\Projector; +use Spatie\EventProjector\Projectors\ProjectsEvents; + +class MyProjector implements Projector +{ + use ProjectsEvents; + + public function onEventHappened(EventHappended $event) + { + // do some work + } +} +``` + +Just by adding a typehint of the event you want to handle makes our package call that method when the typehinted event occurs. All methods specified in your projector can also make use of method injection, so you can resolve any dependencies you need in those methods as well. + +## Getting the uuid of an event + +In most cases you want to have access to the event that was fired. When [using aggregates](/laravel-event-projector/v2/using-aggregates/writing-your-first-aggregate) your events probably won't contain the uuid associated with that event. To get to the uuid of an event simply add a parameter called `$aggregateUuid` that typehinted as a string. + +```php +// ... + +public function onMoneyAdded(MoneyAdded $event, string $aggregateUuid) +{ + $account = Account::findByUuid($aggregateUuid); + + $account->balance += $event->amount; + + $account->save(); +} +``` + +The order of the parameters giving to an event handling method like `onMoneyAdded`. We'll simply pass the uuid to any arguments named `$aggregateUuid`. + +## Manually registering event handling methods + +The `$handlesEvents` property is an array which has event class names as keys and method names as values. Whenever an event is fired that matches one of the keys in `$handlesEvents` the corresponding method will be fired. You can name your methods however you like. + +Here's an example where we listen for a `MoneyAdded` event: + +```php +namespace App\Projectors; + +use App\Account; +use App\Events\MoneyAdded; +use Spatie\EventProjector\Projectors\Projector; +use Spatie\EventProjector\Projectors\ProjectsEvents; + +class AccountBalanceProjector implements Projector +{ + use ProjectsEvents; + + /* + * Here you can specify which event should trigger which method. + */ + protected $handlesEvents = [ + MoneyAdded::class => 'onMoneyAdded', + ]; + + public function onMoneyAdded(MoneyAdded $event) + { + // do some work + } +} +``` + +When the package needs to call the projector, it will use the container to create that projector so you may inject any dependencies in the constructor. In fact, all methods specified in `$handlesEvent` can make use of method injection, so you can resolve any dependencies you need in those methods as well. Any variable in the method signature with the name `$event` will receive the event you're listening for. + +## Using a class as an event handler + +Instead of letting a method on a projector handle an event you can use a dedicated class. + +```php +// in a projector + +// ... + +protected $handlesEvents = [ + /* + * If this event is passed to the projector, the `AddMoneyToAccount` class will be called. + */ + MoneyAdded::class => AddMoneyToAccount::class, +]; +``` + +Here's an example implementation of `AddMoneyToAccount`: + +```php +use App\Events\MoneyAdded; + +class AddMoneyToAccount +{ + public function __invoke(MoneyAdded $event) + { + $event->account->addMoney($event->amount); + } +} +``` + +## Using default event handling method names + +In the example above the events are mapped to methods on the projector using the `$handlesEvents` property. + +```php +// in a projector + +// ... + +protected $handlesEvents = [ + MoneyAdded::class => 'onMoneyAdded', +]; +``` + +You can write this a little shorter. Just put the class name of an event in that array. The package will infer the method name to be called. It will assume that there is a method called `on` followed by the name of the event. Here's an example: + +```php +// in a projector + +// ... + +protected $handlesEvents = [ + /* + * If this event is passed to the projector, the `onMoneyAdded` method will be called. + */ + MoneyAdded::class, +]; +``` + +## Handling a single event + +You can `$handleEvent` to the class name of an event. When such an event comes in we'll call the `__invoke` method. + +```php +// in a projector + +// ... + +protected $handleEvent = MoneyAdded::class, + +public function __invoke(MoneyAdded $event) +{ +} +``` + diff --git a/docs/using-projectors/making-sure-events-get-handled-in-the-right-order.md b/docs/using-projectors/making-sure-events-get-handled-in-the-right-order.md new file mode 100644 index 00000000..45e30e5b --- /dev/null +++ b/docs/using-projectors/making-sure-events-get-handled-in-the-right-order.md @@ -0,0 +1,16 @@ +--- +title: Making sure events get handled in the right order +weight: 4 +--- + +By default all events are handled in a synchronous manner. This means that if you fire off an event in a request, all projectors will get called in the same request. + +## Handling events in a queue + +A queue can be used to guarantee that all events get passed to projectors in the right order. If you want a projector to handle events in a queue, you should let your projector implement the `Spatie\EventProjector\Projectors\QueuedProjector` interface instead of the the normal `Spatie\EventProjector\Projectors\Projector`. This interface merely hints to the `Projectionist` that the event handling should happen in a queued manner. + +A useful rule of thumb is that if your projectors aren't producing data that is consumed in the same request as the events are fired, you should let your projector implement `QueuedProjector`. + +You can set the name of the queue connection in the `queue` key of the `event-projector` config file. You should make sure that the queue will process only one job at a time. + +In a local environment, where events have a very low chance of getting fired concurrently, it's probably ok to just use the `sync` driver. diff --git a/docs/using-projectors/replaying-events.md b/docs/using-projectors/replaying-events.md new file mode 100644 index 00000000..f2998c34 --- /dev/null +++ b/docs/using-projectors/replaying-events.md @@ -0,0 +1,84 @@ +--- +title: Replaying events +weight: 3 +--- + +All [events](/laravel-event-projector/v2/handling-events/preparing-events) that implement `Spatie\EventProjector\ShouldBeStored` will be [serialized](https://docs.spatie.be/laravel-event-projector/v2/advanced-usage/using-your-own-event-serializer) and stored in the `stored_events` table. After your app has been doing its work for a while the `stored_events` table will probably contain some events. + + When creating a new [projector](/laravel-event-projector/v2/handling-events/using-projectors) or [reactor](/laravel-event-projector/v2/handling-events/using-reactors) you'll want to feed all stored events to that new projector or reactor. We call this process replaying events. + + Events can be replayed to [all projectors that were added to the projectionist](/laravel-event-projector/v2/handling-events/using-reactors) with this artisan command: + + ```bash + php artisan event-projector:replay + ``` + + You can also specify projectors by using the `--projector` option. All stored events will be passed only to that projector. + + ```bash + php artisan event-projector:replay --projector=App\\Projectors\\AccountBalanceProjector + ``` + + You can use the projector option multiple times: + + ```bash + php artisan event-projector:replay --projector=App\\Projectors\\AccountBalanceProjector --projector=App\\Projectors\\AnotherProjector + ``` + +If your projector has a `resetState` method it will get called before replaying events. You can use that method to reset the state of your projector. + +If you want to replay events starting from a certain event you can use the `--from` option when executing `event-projector:replay`. If you use this option the `resetState` on projectors will not get called. This package does not track which events have already been processed by which projectors. Be sure not to replay events to projectors that already have handled them. + +## Detecting event replays + +If your projector contains an `onStartingEventReplay` method, we'll call it right before the first event is replayed. + +If it contains an `onFinishedEventReplay` method, we'll call it right after all events have been replayed. + +You can also detect the start and end of event replay by listening for the `Spatie\EventProjector\Events\StartingEventReplay` and `Spatie\EventProjector\Events\FinishedEventReplay` events. + +Though, under normal circumstances, you don't need to know this, you can detect if events are currently being replayed like this: + +```php +Spatie\EventProjector\Facades\Projectionist::isReplayingEvents(); // returns a boolean +``` + +## Performing some work before and after replaying events + +If your projector has a `onStartingEventReplay` method, it will get called right before the first event will be replayed. This can be handy to clean up any data your projector writes to. Here's an example where we truncate the `accounts` table before replaying events: + +```php +namespace App\Projectors; + +use App\Account; + +// ... + +class AccountBalanceProjector implements Projector +{ + use ProjectsEvents; + + // ... + + public function onStartingEventReplay() + { + Account::truncate(); + } +} +``` + +After all events are replayed, the `onFinishedEventReplay` method will be called, should your projector have one. + +## Models with timestamps + +When using models with timestamps, it is important to keep in mind that the projector will create or update these models when replaying and the timestamps will not correspond to the event's original timestamps. This will probably not be behavior you intended. To work around this you can use the stored event's timestamps: + +```php +public function onAccountCreated(StoredEvent $storedEvent, AccountCreated $event) { + Account::create(array_merge($event->accountAttributes, ['created_at' => $storedEvent->created_at, 'updated_at' => $storedEvent->created_at])); +} +``` + +## What about reactors? + +Reactors are used to handle side effects, like sending mails and such. You'll only want reactors to do their work when an event is originally fired. You don't want to send out mails when replaying events. That's why reactors will never get called when replaying events. diff --git a/docs/using-projectors/thinking-in-events.md b/docs/using-projectors/thinking-in-events.md new file mode 100644 index 00000000..517c5562 --- /dev/null +++ b/docs/using-projectors/thinking-in-events.md @@ -0,0 +1,131 @@ +--- +title: Thinking in events +weight: 5 +--- + +In this example we're going to try to send a mail whenever an account is broke (balance below zero). You can do this with projectors and reactors alone, but aggregates might be a better fit for this. Aggregates make it easy to make decisions based on past events. Check out the section on [how to use aggreates](https://docs.spatie.be/laravel-event-projector/v2/introduction) to learn more about them, or keep reading on this page if you don't want to use aggragetes. + +Let's build upon the examples shown in the [writing your first projector](/laravel-event-projector/v2/using-projectors/writing-your-first-projector) and [handling side effects with reactors](https://docs.spatie.be/laravel-event-projector/v2/using-reactors/writing-your-first-reactor)' sections. + +Imagine you are tasked with sending a mail to an account holder whenever he or she is broke. You might think, that's easy, let's just check in a new reactor if the account balance is less than zero. + +Let's first add a little helper method to the `Account` model to check if an account is broke. + +```php +// ... + +class Account extends Model + + // ... + + public function isBroke(): bool + { + return $this->balance < 0; + } +} +``` + +Now create a new reactor called `BrokeReactor`: + +```php +namespace App\Reactors; + +// ... + +class BrokeReactor implements EventHandler +{ + use HandlesEvents; + + public function onMoneySubtracted(MoneySubtracted $event) + { + $account = Account::uuid($event->accountUuid); + + if ($account->isBroke()) { + Mail::to($account->email)->send(new BrokeMail($account)); + + event(new BrokeMailSent($account->uuid)); + } + } +} +``` + +A mail will get sent when an account is broke. The problem with this approach is that mails will also get sent for accounts that were already broke before. If you want to only sent mail when an account went from a positive balance to a negative balance we need to do some more work. + +You might be tempted to add some kind of flag here that determines if the mail was already sent. + +But you should never let reactors write to models (or whatever storage mechanism you use) you've built up using projectors. If you were to do that, all changes would get lost when replaying events: events won't get passed to reactors when replaying them. Keep in mind that reactors are meant for side effects, not for building up state. + +If you are tempted to modify state in a reactor, just fire off a new event and let a projector modify the state. Let's modify the `BrokeReactor` to do just that. If you're following along don't forget to create migration that adds the `broke_mail_sent` field to the `accounts` table. + +```php +// ... + +class BrokeReactor implements EventHandler +{ + use HandlesEvents; + + public function onMoneySubtracted(MoneySubtracted $event) + { + $account = Account::uuid($event->accountUuid); + + /* + * Don't send a mail if an account isn't broke + */ + if (! $account->isBroke()) { + return; + } + + /* + * Don't send a mail if it was sent already + */ + if ($account->broke_mail_sent) { + return; + } + + Mail::to($account->email)->send(new BrokeMail($account)); + + /* + * Send out an event so the projector can modify the state. + */ + event(new BrokeMailSent($account->uuid)); + } +} +``` + +Let's leverage that new event in the `AccountBalanceProjector`. + +```php +// ... + +class AccountBalanceProjector implements Projector +{ + public function onBrokeMailSent(BrokeMailSent $event) + { + $account = Account::uuid($event->accountUuid); + + $account->broke_mail_sent = true; + + $account->save(); + } + + public function onMoneyAdded(MoneyAdded $event) + { + $account = Account::uuid($event->accountUuid); + + $account->balance += $event->amount; + + /* + * If the balance is above zero again, set the broke_mail_sent + * flag to false again, so we can send another mail + * when the balance goes below zero again. + */ + if ($account->balance >= 0) { + $account->broke_mail_sent = false; + } + + $account->save(); + } +} +``` + +The `BrokeReactor` will only send out a mail when an account goes broke. No mails will be sent if the account was already broke. When the account goes above zero and goes broke again a new mail will be sent. When replaying all events, no mail will get sent, but all account state will be correct. diff --git a/docs/using-projectors/writing-your-first-projector.md b/docs/using-projectors/writing-your-first-projector.md new file mode 100644 index 00000000..fea13fe5 --- /dev/null +++ b/docs/using-projectors/writing-your-first-projector.md @@ -0,0 +1,383 @@ +--- +title: Writing your first projector +weight: 1 +--- + +This section is a perfect entry point to get yourself aquinted with projectors. Most examples in these docs are also available in the Laravel app you'll find in [this repo on GitHub](https://github.com/spatie/larabank-event-projector). Clone that repo to toy around with the package. + +A projector is a class that gets triggered when new events come in. It typically writes data (to the database or to a file on disk). We call that written data a projection. + +Imagine you are a bank with customers that have accounts. All these accounts have a balance. When money gets added or subtracted we could modify the balance. If we do that however, we would never know why the balance got to that number. If we were to store all the transactions as events we could calculate the balance. + +## Creating a model + +Here's a small migration to create a table that stores accounts. Using a `uuid` is not strictly required, but it will make your life much easier when using this package. In all examples we'll assume that you'll use them. + +```php +use Illuminate\Support\Facades\Schema; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class CreateAccountsTable extends Migration +{ + public function up() + { + Schema::create('accounts', function (Blueprint $table) { + $table->increments('id'); + $table->string('uuid'); + $table->string('name'); + $table->integer('balance')->default(0); + $table->timestamps(); + }); + } +} +``` + +The `Account` model itself could look like this: + +```php +namespace App; + +use App\Events\AccountCreated; +use App\Events\AccountDeleted; +use App\Events\MoneyAdded; +use App\Events\MoneySubtracted; +use Illuminate\Database\Eloquent\Model; +use Ramsey\Uuid\Uuid; + +class Account extends Model +{ + protected $guarded = []; + + protected $casts = [ + 'broke_mail_send' => 'bool', + ]; + + public static function createWithAttributes(array $attributes): Account + { + /* + * Let's generate a uuid. + */ + $attributes['uuid'] = (string) Uuid::uuid4(); + + /* + * The account will be created inside this event using the generated uuid. + */ + event(new AccountCreated($attributes)); + + /* + * The uuid will be used the retrieve the created account. + */ + return static::uuid($attributes['uuid']); + } + + public function addMoney(int $amount) + { + event(new MoneyAdded($this->uuid, $amount)); + } + + public function subtractMoney(int $amount) + { + event(new MoneySubtracted($this->uuid, $amount)); + } + + public function remove() + { + event(new AccountDeleted($this->uuid)); + } + + /* + * A helper method to quickly retrieve an account by uuid. + */ + public static function uuid(string $uuid): ?Account + { + return static::where('uuid', $uuid)->first(); + } +} +``` + +## Defining events + +Instead of creating, updating and deleting accounts, we're simply firing off events. All these events should implement `\Spatie\EventProjector\ShouldBeStored`. This is an empty interface that signifies to our package that the event should be stored. + +Let's take a look at all events used in the `Account` model. + +```php +namespace App\Events; + +use Spatie\EventProjector\ShouldBeStored; + +class AccountCreated implements ShouldBeStored +{ + /** @var array */ + public $accountAttributes; + + public function __construct(array $accountAttributes) + { + $this->accountAttributes = $accountAttributes; + } +} +``` + +```php +namespace App\Events; + +use Spatie\EventProjector\ShouldBeStored; + +class MoneyAdded implements ShouldBeStored +{ + /** @var string */ + public $accountUuid; + + /** @var int */ + public $amount; + + public function __construct(string $accountUuid, int $amount) + { + $this->accountUuid = $accountUuid; + + $this->amount = $amount; + } +} +``` + +```php +namespace App\Events; + +use Spatie\EventProjector\ShouldBeStored; + +class MoneySubtracted implements ShouldBeStored +{ + /** @var string */ + public $accountUuid; + + /** @var int */ + public $amount; + + public function __construct(string $accountUuid, int $amount) + { + $this->accountUuid = $accountUuid; + + $this->amount = $amount; + } +} +``` + +```php +namespace App\Events; + +use Spatie\EventProjector\ShouldBeStored; + +class AccountDeleted implements ShouldBeStored +{ + /** @var string */ + public $accountUuid; + + public function __construct(string $accountUuid) + { + $this->accountUuid = $accountUuid; + } +} +``` + +## Creating your first projector + +A projector is a class that listens for events that were stored. When it hears an event that it is interested in, it can perform some work. + +Let's create your first projector. You can perform `php artisan make:projector AccountBalanceProjector` to create a projector in `app\Projectors`. + +Here's an example projector that handles all the events mentioned above: + +```php +namespace App\Projectors; + +use App\Account; +use App\Events\AccountCreated; +use App\Events\AccountDeleted; +use App\Events\MoneyAdded; +use App\Events\MoneySubtracted; +use Spatie\EventProjector\Models\StoredEvent; +use Spatie\EventProjector\Projectors\Projector; +use Spatie\EventProjector\Projectors\ProjectsEvents; + +class AccountBalanceProjector implements Projector +{ + use ProjectsEvents; + + public function onAccountCreated(AccountCreated $event) + { + Account::create($event->accountAttributes); + } + + public function onMoneyAdded(MoneyAdded $event) + { + $account = Account::uuid($event->accountUuid); + + $account->balance += $event->amount; + + $account->save(); + } + + public function onMoneySubtracted(MoneySubtracted $event) + { + $account = Account::uuid($event->accountUuid); + + $account->balance -= $event->amount; + + $account->save(); + } + + public function onAccountDeleted(AccountDeleted $event) + { + Account::uuid($event->accountUuid)->delete(); + } +} +``` + +Just by typehinting an event in a method will make the package call that method when the event occurs. By default the package will automatically discover and registering projectors. + +## Let's fire off some events + +With all this out of the way we can fire off some events. + +Let's try adding an account with: + +```php +Account::createWithAttributes(['name' => 'Luke']); +Account::createWithAttributes(['name' => 'Leia']); +``` + +And let's make some transactions on that account: + +```php +$account = Account::where(['name' => 'Luke'])->first(); +$anotherAccount = Account::where(['name' => 'Leia'])->first(); + +$account->addMoney(1000); +$anotherAccount->addMoney(500); +$account->subtractMoney(50); +``` + +If you take a look at the contents of the `accounts` table you should see some accounts together with their calculated balance. Sweet! In the `stored_events` table you should see an entry per event that we fired. + +## Your second projector + +Imagine that, after a while, someone at the bank wants to know which accounts have processed the most transactions. Because we stored all changes to the accounts in the events table we can easily get that info by creating another projector. + +We are going to create another projector that stores the transaction count per account in a model. Bear in mind that you can easily use any other storage mechanism instead of a model. The projector doesn't care what you use. + +Here's the migration and the model class that the projector is going to use: + +```php +use Illuminate\Support\Facades\Schema; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class CreateTransactionCountsTable extends Migration +{ + public function up() + { + Schema::create('transaction_counts', function (Blueprint $table) { + $table->increments('id'); + $table->string('account_uuid'); + $table->integer('count')->default(0); + $table->timestamps(); + }); + } +} +``` + +If you're following along don't forget to run this new migration. + +```php +php artisan migrate +``` + +```php +namespace App; + +use Illuminate\Database\Eloquent\Model; + +class TransactionCount extends Model +{ + protected $table = 'transactions_count'; + protected $guarded = []; +} +``` + +Here's the projector that is going to listen to the `MoneyAdded` and `MoneySubtracted` events. Typehinting `MoneyAdded` and `MoneySubtracted` will make our package call `onMoneyAdded` and ``MoneySubtracted`` when these events occur. + + +```php +namespace App\Projectors; + +use App\Events\MoneyAdded; +use App\Events\MoneySubtracted; +use App\TransactionCount; +use Spatie\EventProjector\Models\StoredEvent; +use Spatie\EventProjector\Projectors\Projector; +use Spatie\EventProjector\Projectors\ProjectsEvents; + +class TransactionCountProjector implements Projector +{ + use ProjectsEvents; + + public function onMoneyAdded(MoneyAdded $event) + { + $transactionCounter = TransactionCount::firstOrCreate(['account_uuid' => $event->accountUuid]); + + $transactionCounter->count += 1; + + $transactionCounter->save(); + } + + public function onMoneySubtracted(MoneySubtracted $event) + { + $transactionCounter = TransactionCount::firstOrCreate(['account_uuid' => $event->accountUuid]); + + $transactionCounter->count += 1; + + $transactionCounter->save(); + } +} +``` + +Let's not forget to register this projector: + +```php +// in a service provider of your own +Projectionist::addProjector(TransactionCountProjector::class); +``` + +If you've followed along, you've already created some accounts and some events. To feed those past events to the projector we can simply perform this artisan command: + +```php +php artisan event-projector:replay +``` + +This command will take all events stored in the `stored_events` table and pass them to `TransactionCountProjector`. After the command completes you should see the transaction counts in the `transaction_counts` table. + +## Welcoming new events + +Now that both of your projections have handled all events, try firing off some new events. + +``` +Account::createWithAttributes(['name' => 'Yoda']); +``` + +And let's add some transactions to that account: + +```php +$yetAnotherAccount = Account::where(['name' => 'Yoda'])->first(); + +$yetAnotherAccount->addMoney(1000); +$yetAnotherAccount->subtractMoney(50); +``` + +You'll notice that both projectors are immediately handling these new events. The balance of the `Account` model is up to date and the data in the `transaction_counts` table gets updated. + +## Benefits of projectors and projections + +The cool thing about projectors is that you can write them after events have happened. Imagine that someone at the bank wants to have a report of the average balance of each account. You would be able to write a new projector, replay all events, and have that data. + +Projections are very fast to query. Imagine that our application has processed millions of events. If you want to create a screen where you display the accounts with the most transactions you can easily query the `transaction_counts` table. This way you don't need to fire off some expensive query. The projector will keep the projections (the `transaction_counts` table) up to date. diff --git a/docs/using-reactors/_index.md b/docs/using-reactors/_index.md new file mode 100644 index 00000000..8e06264e --- /dev/null +++ b/docs/using-reactors/_index.md @@ -0,0 +1,4 @@ +--- +title: Using reactors +weight: 3 +--- diff --git a/docs/using-reactors/creating-and-configuring-reactors.md b/docs/using-reactors/creating-and-configuring-reactors.md new file mode 100644 index 00000000..f2308c6a --- /dev/null +++ b/docs/using-reactors/creating-and-configuring-reactors.md @@ -0,0 +1,185 @@ +--- +title: Creating and configuring reactors +weight: 2 +--- + +A reactor is a class, that much like a projector, listens for incoming events. Unlike projectors however, reactors will not get called when events are replayed. Reactors will only get called when the original event fires. + +## Creating reactors + +Let's create a reactor. You can perform this artisan command to create a projector in `app\Reactors`: + +```php +php artisan make:reactor BigAmountAddedReactor +``` + +## Registering reactors + +By default, the package will automatically find an register all reactors found in your application. + +Alternatively, you can also manually register them in the `reactors` key of the `event-projectors` config file. + +They can also be added to the `Projectionist`. This can be done anywhere, but typically you would do this in a ServiceProvider of your own. + +```php +namespace App\Providers; + +use App\Projectors\AccountBalanceProjector; +use Illuminate\Support\ServiceProvider; +use Spatie\EventProjector\Facades\Projectionist; + +class EventProjectorServiceProvider extends ServiceProvider +{ + public function register() + { + // adding a single reactor + Projectionist::addReactor(BigAmountAddedReactor::class); + + // you can also add multiple reactors in one go + Projectionist::addReactors([ + AnotherReactor::class, + YetAnotherReactor::class, + ]); + } +} +``` + +## Using reactors + +This is the contents of a class created by the artisan command mentioned in the section above: + +```php +namespace App\Reactors; + +class MyReactor +{ + public function onEventHappened(EventHappended $event) + { + + } +} +``` + +Just by adding a typehint of the event you want to handle makes our package call that method when the typehinted event occurs. All methods specified in your projector can also make use of method injection, so you can resolve any dependencies you need in those methods as well. + +## Getting the uuid of an event + +In most cases you want to have access to the event that was fired. When [using aggregates]() your events probably won't contain the uuid associated with that event. To get to the uuid of an event simply add a parameter called `$aggregateUuid` that typehinted as a string. + +```php +// ... + +public function onMoneyAdded(MoneyAdded $event, string $aggregateUuid) +{ + $account = Account::findByUuid($aggregateUuid); + + Mail::to($account->user)->send(new MoreMoneyAddedMailable()); +} +``` + +The order of the parameters giving to an event handling method like `onMoneyAdded`. We'll simply pass the uuid to any arguments named `$uuid`. + +## Manually register event handling methods + +The `$handlesEvents` property is an array which has event class names as keys and method names as values. Whenever an event is fired that matches one of the keys in `$handlesEvents` the corresponding method will be fired. You can name your methods however you like. + +Here's an example where we listen for a `MoneyAdded` event: + +```php +namespace App\Reactors; + +use App\Events\MoneyAdded; + +class BigAmountAddedReactor +{ + /* + * Here you can specify which event should trigger which method. + */ + protected $handlesEvents = [ + MoneyAdded::class => 'onMoneyAdded', + + ]; + + public function onMoneyAdded(MoneyAdded $event) + { + // do some work + } +} +``` + +This reactor will be created using the container so you may inject any dependency you'd like. In fact, all methods present in `$handlesEvent` can make use of method injection, so you can resolve any dependencies you need in those methods as well. Any variable in the method signature with the name `$event` will receive the event you're listening for. + +## Using default event handling method names + +In the example above the events are mapped to methods on the reactor using the `$handlesEvents` property. + +```php +// in a reactor + +// ... + +protected $handlesEvents = [ + MoneyAdded::class => 'onMoneyAdded', +]; +``` + +You can write this a little shorter. Just put the class name of an event in that array. The package will infer the method name to be called. It will assume that there is a method called `on` followed by the name of the event. Here's an example: + +```php +// in a reactor + +// ... + +protected $handlesEvents = [ + /* + * If this event is passed to the reactor, the `onMoneyAdded` method will be called. + */ + MoneyAdded::class, +]; +``` + +## Handling a single event + +You can `$handleEvent` to the class name of an event. When such an event comes in we'll call the `__invoke` method. + +```php +// in a reactor + +// ... + +protected $handleEvent = MoneyAdded::class, + +public function __invoke(MoneyAdded $event) +{ +} +``` + +## Using a class as an event handler + +Instead of letting a method on a reactor handle an event you can use a dedicated class. + +```php +// in a projector + +// ... + +protected $handlesEvents = [ + /* + * If this event is passed to the projector, the `AddMoneyToAccount` class will be called. + */ + MoneyAdded::class => SendMoneyAddedMail::class, +]; +``` + +Here's an example implementation of `SendMoneyAddedMail`: + +```php +use App\Events\MoneyAdded; + +class SendMoneyAddedMail +{ + public function __invoke(MoneyAdded $event) + { + // do work to send a mail here + } +} diff --git a/docs/using-reactors/writing-your-first-reactor.md b/docs/using-reactors/writing-your-first-reactor.md new file mode 100644 index 00000000..56336dcf --- /dev/null +++ b/docs/using-reactors/writing-your-first-reactor.md @@ -0,0 +1,62 @@ +--- +title: Writing your first reactor +weight: 1 +--- + +## What is a reactor + +Now that you've [written your first projector](/laravel-event-projector/v2/using-projectors/writing-your-first-projector), let's learn how to handle side effects. With side effects we mean things like sending a mail, sending a notification, ... You only want to perform these actions when the original event happens. You don't want to do this work when replaying events. + +A reactor is a class, that much like a projector, listens for incoming events. Unlike projectors however, reactors will not get called when events are replayed. Reactors only will get called when the original event fires. + +## Creating your first reactor + +Let's create your first reactor. You can perform `php artisan make:reactor BigAmountAddedReactor` to create a reactor in `app\Reactors`. We will make this reactor send a mail to the director of the bank whenever a big amount of money is added to an account. Typehinting `MoneyAdded` will make our package call `onMoneyAdded` when the event occurs. + +```php +namespace App\Reactors; + +use App\Account; +use App\Events\MoneyAdded; +use App\Mail\BigAmountAddedMail; +use Illuminate\Support\Facades\Mail; +use Spatie\EventProjector\EventHandlers\EventHandler; +use Spatie\EventProjector\EventHandlers\HandlesEvents; + +final class BigAmountAddedReactor implements EventHandler +{ + use HandlesEvents; + + public function onMoneyAdded(MoneyAdded $event) + { + if ($event->amount < 900) { + return; + } + + $account = Account::uuid($event->accountUuid); + + Mail::to('director@bank.com')->send(new BigAmountAddedMail($account, $event->amount)); + } +} +``` + +By default the package will automatically find and use your reactor. + +## Using the reactor + +The reactor above will send an email to the director of the bank whenever an amount of 900 or more gets added to an account. Let's put the reactor to work. + +```php +$account = Account::createWithAttributes(['name' => 'Rey']); +$account->addMoney(1000); +``` + +A mail will be sent to the director. + +If you truncate the `accounts` table and rebuild the contents with + +```php +php artisan event-projector:rebuild +``` + +no mail will be sent.