This package integrates Greg Young's eventstore
into Laravel's event system. By simply implementing ShouldBeStored
on your events, they will be sent to eventstore. In the same fashion you can also setup listeners that can respond to events that are received from the eventstore.
Example implementation: https://github.com/digitalrisks/laravel-eventstore-example
You can install the package via composer:
composer require digitalrisks/laravel-eventstore
Add the base service provider for the package.
<?php
namespace App\Providers;
use Illuminate\Support\Str;
use DigitalRisks\LaravelEventStore\EventStore;
use DigitalRisks\LaravelEventStore\ServiceProvider as EventStoreApplicationServiceProvider;
class EventStoreServiceProvider extends EventStoreApplicationServiceProvider
{
/**
* Bootstrap the application services.
*/
public function boot()
{
parent::boot();
}
/**
* Set the eventToClass method.
*
* @return void
*/
public function eventClasses()
{
// This will set your events to be the following '\\App\Events\\' . $event->getType();.
EventStore::eventToClass();
// You can customise this by doing the following.
EventStore::eventToClass(function ($event) {
return 'App\Events\\' . Str::studly($event->getType());
});
}
/**
* Handle logging when event is triggered.
*
* @return void
*/
public function logger()
{
// This will setup the logger for when an event happens.
EventStore::logger();
// You can customise this by doing the following.
EventStore::logger(function ($event, $type) {
Log::info($event->getType());
});
}
/**
* Register the application services.
*/
public function register()
{
parent::register();
}
}
In your config/app.php
file, add the following to the providers
array.
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [
...
App\Providers\EventStoreServiceProvider::class,
],
use DigitalRisks\LaravelEventStore\Contracts\CouldBeReceived;
use DigitalRisks\LaravelEventStore\Contracts\ShouldBeStored;
use DigitalRisks\LaravelEventStore\Traits\ReceivedFromEventStore;
use DigitalRisks\LaravelEventStore\Traits\SendsToEventStore;
class QuoteStarted implements ShouldBeStored, CouldBeReceived
{
use SendsToEventStore, ReceivedFromEventStore;
public $email;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($email = null)
{
$this->email = $email;
}
}
The package will automatically send events dispatched in Laravel that implement the ShouldBeStored
interface.
interface ShouldBeStored
{
public function getEventStream(): string;
public function getEventType(): string;
public function getEventId(): string;
public function getData(): array;
public function getMetadata(): array;
}
To assist in implementing the interface, the package comes with a SendsToEventStore
trait which meets the requirements of the interface in a basic fashion:
- Event Type: the event's class name
- Event ID: A UUID v4 will be generated
- Data: all of the events public properties are automatically serialized
- Metadata: data from all of the methods marked with
@metadata
will be collected and serialized
use DigitalRisks\LaravelEventStore\Contracts\CouldBeReceived;
use DigitalRisks\LaravelEventStore\Contracts\ShouldBeStored;
use DigitalRisks\LaravelEventStore\Traits\SendsToEventStore;
class AccountCreated implements ShouldBeStored, CouldBeReceived
{
use SendsToEventStore;
public function getEventStream(): string
{
return 'accounts';
}
}
Then raising an event is done in the normal Laravel way:
event(new AccountCreated('[email protected]'));
Metadata can help trace events around your system. You can include any of the following traits on your event to attach metadata automatically
AddsHerokuMetadata
AddsLaravelMetadata
AddsUserMetaData
Or you can define your own methods to collect metadata. Any method with the @metadata
annotation will be called:
class AccountCreated implements ShouldBeStored
{
use DigitalRisks\LaravelEventStore\Traits\AddsLaravelMetadata;
/** @metadata */
public function collectIpMetadata()
{
return [
'ip' => $_SERVER['REMOTE_ADDR'],
];
}
}
If you would like to test that your events are being fired correctly, you can use the Laravel Event::mock
method, or the package comes with helpers that interact with an eventstore to confirm they have been stored correctly.
class AccountCreatedTest extends TestCase
{
use DigitalRisks\LaravelEventStore\Tests\Traits\InteractsWithEventStore;
public function test_it_creates_an_event_when_an_account_is_created()
{
// Act.
$this->json('POST', '/api/accounts', ['email' => '[email protected]']);
// Assert.
$this->assertEventStoreEventRaised('AccountCreated', 'accounts', ['email' => '[email protected]']);
}
}
You must first run the worker which will listen for events.
None of the options are required. By default it will run the persistance subscription with a timeout of 10 seconds and 1 parallel event at a time.
$ php artisan eventstore:worker {--parallel= : How many events to run in parallel.} {--timeout= : How long the event should time out for.}
$ php artisan eventstore:worker --parallel=10 --timeout=5
When an event is received, it will be mapped to the Laravel event and the original EventRecord
can be accessed via getEventRecord()
.
You can react to these events in the normal Laravel fashion.
class EventServiceProvider extends ServiceProvider
{
protected $listen = [
AccountCreated::class => [SendAccountCreatedEmail::class],
];
}
class SendAccountCreatedEmail
{
public function handle(AccountCreated $event)
{
Mail::to($event->email)->send('Here is your account');
}
}
If you are listening to the same stream where you are firing events, your events WILL BE fired twice - once by Laravel and once received from the event store. You may choose react synchronously if $event->getEventRecord()
is false and asynchronously if $event->getEventRecord()
returns the eventstore record.
class SendAccountCreatedEmail
{
public function handle(AccountCreated $event)
{
// Side effect, let's only send an email when we've triggered this event and not when replaying events
if (! $event->getEventRecord()) return;
Mail::to($event->email)->send('Here is your account');
}
}
class SaveAccountToDatabase
{
public function handle(AccountCreated $event)
{
// State change, let's ensure we update our database with this event.
if ($event->getEventRecord()) return;
Account::create(['email' => $event->email]);
}
}
In addition, if you would like to test that are events are created AND how the application reacts to those events
you may set eventstore.connection
to sync
. This will trick the event listeners into thinking that the
event has been received from the eventstore.
If you would like to test your listeners, the package comes with several helper methods to mimic events being received from the worker.
class QuoteStartedTest extends TestCase
{
use \DigitalRisks\LaravelEventStore\Tests\MakesEventRecords;
public function test_it_sends_an_email_when_an_account_is_created()
{
// Arrange.
$event = $this->makeEventRecord('AccountCreated', ['email' => '[email protected]');
// Act.
event($event->getType(), $event);
// Assert.
Mail::assertSentTo('[email protected]');
}
}
In addition you may set set eventstore.connection
to sync
, which will trick your listeners
You can replay events by using the replay command
php artisan eventstore:replay <stream> <event>
<event>
can be a single event number or a range like 390-396
The defaults are set in config/eventstore.php
. Copy this file to your own config directory to modify the values:
php artisan vendor:publish --provider="DigitalRisks\LaravelEventStore\ServiceProvider"
return [
'tcp_url' => 'tls://admin:changeit@localhost:1113',
'http_url' => 'http://admin:changeit@localhost:2113',
'group' => 'account-email-subscription',
'volatile_streams' => ['quotes', 'accounts'],
'subscription_streams' => ['quotes', 'accounts'],
];
composer test
Please see CHANGELOG for more information what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security related issues, please email [email protected] instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.