Giter Site home page Giter Site logo

spatie / laravel-event-projector Goto Github PK

View Code? Open in Web Editor NEW
637.0 28.0 62.0 1.63 MB

Event sourcing for Artisans πŸ“½

Home Page: https://docs.spatie.be/laravel-event-projector

License: MIT License

PHP 100.00%
laravel event-sourcing php events eventstore

laravel-event-projector's Introduction

THIS PACKAGE HAS BEEN SUPERCEDED BY SPATIE/LARAVEL-EVENT-SOURCING

Because the package now does more than just providing projectors, we decided to change the name of the package to spatie/laravel-event-sourcing.

Upgrading from v3 of laravel-event-projector to v1 of laravel-event-sourcing is easy. Take a look at the upgrade guide for laravel-event-sourcing.

Event sourcing for Artisans πŸ“½

Latest Version on Packagist Build Status StyleCI Quality Score Total Downloads

This package aims to be the entry point to get started with event sourcing in Laravel. It can help you with setting up aggregates, projectors, and reactors.

If you've never worked with event sourcing, or are uncertain about what aggregates, projectors and reactors are head over to the getting familiar with event sourcing section in our docs.

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

If you want to skip to reading code immediately, here are some example apps. In each of them, you can create accounts and deposit or withdraw money.

Documentation

You can find installation instructions and detailed instructions on how to use this package at the dedicated documentation site.

Changelog

Please see CHANGELOG for more information what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

Postcardware

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.

We publish all received postcards on our company website.

Credits

The aggregate root functionality is heavily inspired by Frank De Jonge's excellent EventSauce package. A big thank you to Dries Vints for giving lots of valuable feedback while we were developing the package.

Support us

Spatie is a webdesign agency based in Antwerp, Belgium. You'll find an overview of all our open source projects on our website.

Does your business depend on our contributions? Reach out and support us on Patreon. All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff.

Footnotes

1 Quote taken from Event Sourcing made Simple

License

The MIT License (MIT). Please see License File for more information.

laravel-event-projector's People

Contributors

36864 avatar adrianb93 avatar azeirah avatar brendt avatar datashaman avatar dib258 avatar freekmurze avatar guitarbien avatar ianrodrigues avatar jminkler avatar jovanialferez avatar kylestev avatar maantje avatar mfullbrook avatar michael-schaefer-eu avatar mpociot avatar namoshek avatar nathangiesbrecht avatar nathanheffley avatar nunomaduro avatar pascalbaljet avatar profd2004 avatar rgehan avatar rodrigopedra avatar sebastiandedeyne avatar siebeve avatar talvbansal avatar telkins avatar unreasonablymundane avatar vtalbot avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laravel-event-projector's Issues

Set metadata when event is emitted

Currently, there is no way to add metadata to an event when you emit it. Instead, you have to either hook into the model's creating events or manipulate it after it's been saved to the database.
I think it should be possible to add metadata to an event when it is generated. Here's my concrete use case for this:

I'm working on an "Undo" feature which undoes the last event generated by the user by emitting the opposite event (Created <=> Deleted). Of course if I just fetch the last event, clicking undo twice will constantly create/delete the same model.

To get around that, I would like to add some metadata to an event which marks it as an "undo" action. Then I'd only need to fetch the latest event that isn't an undo action and that hasn't been undone (marking an event as "undone" is easy enough with the current system).

I believe a projector shouldn't have to know whether the event it's handling is undoing an earlier event or not. As far as it's concerned, a create event makes a model and a delete event deletes a model.

To solve this, I propose allowing events to define a public $meta_data property that would be used in the StoredEvent constructor. An example usage in an event would look something like this:

class TaskCreated implements ShouldBeStored
{
    use Dispatchable, Undoable;

    /** @var array */
    public $meta_data

    /** @var array */
    public $taskAttributes;

    /** @var string */
    public $undoEvent = TaskDeleted::class;

    /** @var array */
    public $undoAttributes = ['taskAttributes'];

    /**
     * Create a new event instance.
     *
     * @param array $taskAttributes
     * @return void
     */
    public function __construct(array $taskAttributes, array $meta_data = [])
    {
        $this->taskAttributes = $taskAttributes;
        $this->metadata = $meta_data;
    }
}

This would allow me to simply pass an additional parameter to an event to set metadata instead of having to pollute its event_properties with some kind of marker that doesn't directly relate to the event. An undo routine could look something like this:

public function undo()
{
    $event = StoredEvent::whereNull('meta_data->undo') // Ignore Undo Events
      ->whereNull('meta_data->undone') // Ignore Undone Events
      ->latest()
      ->firstOrFail()
      ->getEventAttribute();

    if (method_exists($event, 'undo')) { // Check if event is undoable
      $event->undo();
    }
    else {
      throw new \Exception("This action cannot be undone.", 1);
    }
}

I considered just making a PR for this, but I thought I'd explain my use case first and see if this all makes sense or if there's some other way I should be doing this, or if I've missed an obvious way to set metadata on event creation.

Using ID instead of UUID's

Hi!
This is not really an issue, more of a question.


Like some other people I was wondering why using UUID seemed necessary. And as far as I understood the (only?) issue was that some identifier to refer to the object needs to be known even before the object is created.
What if we instead checked for the next auto-generated ID and used that to generate the object and later refer to it? Of course this would be done in a transaction to avoid race conditions.

What do you think, do you see any issues related to event sourcing?
I cloned your sample repo and adjusted it to use ID's instead of UUID's, and it seems to be working so far, but maybe I missed some use case (the only interesting change is in App\Account.php, I copied the snipped below).

Would be glad for feedback / opinions!

public static function createWithAttributes(array $attributes)
    {
        $account = DB::transaction(function () use ($attributes) {

            // Fetch the next ID
            $result = DB::select('show table status where name = ?', ['accounts'])[0];
            $id = $result->Auto_increment;

            // Generate the object by using the ID instead of UUID
            $attributes['id'] = $id;
            event(new AccountCreated($attributes));

            return Account::find($id);
        });

        return $account;
    }

When a reactor fires new event with failing synchronous projector

I've come across a potential issue, hopefully clearly explained below:

  1. Let's say a SomethingHappened Event is fired, and pushed to the event-projecting queue.
  2. A Reactor listens for this event and receives it from the queue for handling
  3. The reactor produces a new SomethingElseHappenedNow event, which is fired and stored
  4. A projector named SomeEventsProjector handles the SomethingElseHappenedNow event immediately, within the same process, but throws an exception.
  5. This means the job queue perceives the handling of the SomethingHappened event as failed and will (depending on the queue configuration) retry, meaning steps 2 through 4 are repeated, but:
    1. As a result of step 3, event SomethingElseHappenedNow is already stored
    2. As a result of step 4, the SomeEventsProjector is now behind (has_received_all_events = false)
    3. Which will leave us in the situation that another SomethingElseHappenedNow event is stored, which will this time not reach the SomeEventsProjector, because it is behind.

All this kind of fails silently; the Exception is logged, but the retry of the job now has status "processed" instead of "failed". On top of that we kind of have a corrupt event log, because now there are two SomethingElseHappenedNow events stored...

I hope I make sense here... I guess this will only happen when a reactor handles an event, produces a new event as a result and a synchronous projector for that event fails somehow by throwing an exception.

I've thought of two ways to prevent this (apart from not running the projector synchronously);

  1. setting 'tries' on the event-projector queue to 1 (not very desirable), or
  2. having projectors and reactors run in a database transaction so the SomethingElseHappenedNow in the example will not be committed to the database.

I'm curious what your thoughts are on this... Maybe I'm missing something obvious ;)

laravel timestamps in models will be incorrect if left unhandled

I'm not sure if this is a feature request or a documentation bug.

If you add timestamps to your models that are updated via a projection, then the timestamps will be populated by the insertion to your database rather than when the event happened. This can be slightly off if using a queue to handle events and grossly incorrect when replaying the events.

I suggest either stating this in the documentation and warning the users to not use timestamps or add the appropriate handling

OR

maybe add a trait that when present, will copy the event->created_at timestamp to the correct (updated_at or created_at or deleted_at) timestamp.

Thoughts?

event streams fail when using mariaDB

Issue

Creating an event fails when using mariaDB and event streams - the following SQL error is returned:

...check the manual that corresponds to your MariaDB server version for the right syntax to use near '>'$."field"' = ? 

Streaming events fails on mariaDB due to the -> shorthand for JSON_EXTRACT - this exists in mysql but not in mariaDB.

Possible Solution

This may be solved by changing occurrences of

->where("event_properties->{$whereJsonClause}", $streamValue)

to

->where(\DB::raw("JSON_EXTRACT(`event_properties`,{$whereJsonClause})", $streamValue)

If you are happy with this option then I can submit a PR, however may need some guidance for adding MariaDB to the travisCI test suite.

Question: retrieving historical data

Hi,

We have been using LEP for a while now and we are really loving it. We are running in to a little challenge right now though and we are curious to hear how you guys (and girls) would solve this.

Basically, we have a few tasklists in our application. These tasklists contain individual tasks, and a tasklist is always started and ended on the same day. The tasklists are recurring, meaning they can/have to be completed every day.

In our current setup, we have a tasklists table which represents the current state of the application. However, we want to retain information of tasklists that have seen user interaction in the past. Take this example:

  1. User creates tasklist "Clean living room" on 01-01-2018
  2. User adds 3 tasks to the tasklist on 01-01-2018
  3. User renames tasklist to "Clean all living rooms" on 07-01-2018
  4. User adds another 2 tasks to the tasklist on 10-01-2018

Now, if an admin wants to see the details of a tasklist on a specific date, we obviously cannot use the current state from the tasklists table, as for instance the name of the tasklist has changed. We want to serve the user with the data that was the current state on the day they are

We are contemplating adding a tasklist_history table, projected by LEP, which we can then use to query the exact state of a model on a specific date. However, as our user base grows, this table will become huge. Furthermore, simple tasks such as retrieving basic fields from the database can get super cumbersome (imagine the code below for all fields of a table, for all your models):

public function getNameAttribute()
{
    if ($this->relationLoaded('history') {
        return $this->history->name;
    }

    return $this->name;
}

Obviously, we can reduce this duplicate code by extending the base Model class, but before doing that we wanted some more input from you guys.

Cheers!

Inconsistency error in docs

In the docs (https://docs.spatie.be/laravel-event-projector/v1/basic-usage/handling-side-effects-using-reactors) the code example for the BigAmountAddedReactor uses find() instead of the uuid() method, as the MoneyAdded only receives the uuid.

so in short: $account = Account::find($event->accountId); should be $account = Account::uuid($event->accountUuid);

I tried searching for the docs on github to create a pull request, but i couldn't find it. That's why i created this issue.

Should this work with SQLite?

I'm getting the following error when I am using SQLite and have a projector with streamEventsBy implmented:

RuntimeException: This database engine does not support JSON operations.

C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php:930
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php:916
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php:309
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php:200
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Support\Collection.php:932
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php:201
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php:184
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php:87
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\Grammar.php:62
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Grammars\SQLiteGrammar.php:49
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php:1914
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php:1963
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php:1951
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php:2435
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Query\Builder.php:1952
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php:481
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Builder.php:465
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Database\Concerns\BuildsQueries.php:77
C:\Users\srott\Source\projectpay\vendor\spatie\laravel-event-projector\src\Projectors\ProjectsEvents.php:120
C:\Users\srott\Source\projectpay\vendor\spatie\laravel-event-projector\src\Projectionist.php:138
C:\Users\srott\Source\projectpay\vendor\spatie\laravel-event-projector\src\Projectionist.php:128
C:\Users\srott\Source\projectpay\vendor\spatie\laravel-event-projector\src\Projectionist.php:101
C:\Users\srott\Source\projectpay\vendor\spatie\laravel-event-projector\src\EventSubscriber.php:36
C:\Users\srott\Source\projectpay\vendor\spatie\laravel-event-projector\src\EventSubscriber.php:31
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Events\Dispatcher.php:375
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Events\Dispatcher.php:209
C:\Users\srott\Source\projectpay\vendor\laravel\framework\src\Illuminate\Foundation\helpers.php:474
C:\Users\srott\Source\projectpay\app\Models\Accounting\Ledger.php:32

Removing the method appears to let the Projector execute without trouble. While I'm not likely to run using SQLite in a production environment it does kind of break running tests. Is there something I'm doing wrong?

What's the point of having timestamps in projected models?

Hi! Thanks for your amazing open source work, a postcard will be on the way soon ;).

I had a question regarding models that have entries that are created through a projector. In the docs, the migration comes with $table->timestamps();. I was wondering if these fields are still useful. If you do a php artisan event-projector:rebuild, these fields will contain the date and time of when the artisan command was run.

In our current setup, I dropped the timestamps from the mutation. My colleague however pointed out that it would be nice to have insights in when the entries were rebuild. Therefore, it would make sense to drop the updated_at field and rename the created_at in to projected_at or something.

Another solution would be to extrapolate the created_at and updated_at from the events. This would be the cleanest solution as the value of the date fields would make sense, but it is probably much harder to achieve.

Fake event projecting when testing

First: I love this package! I'm currently implementing it in a project I'm working on and it's worked great so far!

It would be nice to be able to "fake" event projecting when running tests, preventing all projectors and reactors from executing by default, and allow only specific Projectors/Reactors to actually handle events.

We could then run assertions on the outcome of the projectors/reactors that did execute (and we actually care about for the given test). For those that did not execute (and were faked) we can assert that they would have been triggered by a given event.

If I were to implement this (and submit a PR πŸ˜‰), I would probably go for an api like this:

// Start faking the event projecting and allow running given projector(s) and/or reactor(s).
Projectionist::fake($allowedHandlers);

// Allow faked projectionist to run given projector(s) and/or reactor(s).
Projectionist::allowHandler($allowedHandlers);

// Allow faked projectionist to run all sync projectors.
Projectionist::runAllSyncProjectors();

// Assert that a disabled projector or reactor would have been called with given event.
Projectionist::assertCalledWith($eventHandler, $event);

// Assert that a disabled projector or reactor would not have been called with given event.
Projectionist::assertNotCalledWith($eventHandler, $event);

Let me know what you think!

In-memory event replay

Hey @freekmurze - are you guys planning to support in-memory replay of the events for a given projector?

The idea is to have a projector that will take all the events for a given uuid, and it only replays those?

With this, you can versioning (or the state at a certain point in time) quite easily.

Thanks.

php version

Is ^7.2 a constraint (maybe some dependency) or is it possible to relax it to ^7.1.3 (laravel min constraint)?

Questions about rebuilding and related issues

A few quick things:

  • I like the idea of event sourcing.
  • I like the package you've developed so far.
  • I have yet to use it, even in a test or hobby project.

I do have a question about a specific scenario, however, which is more or less the same as some of the examples in the documentation: https://docs.spatie.be/laravel-event-projector/v1/basic-usage/writing-your-first-projector

During account creation, one might imagine that an initial password is generated and welcome email would be sent. During a rebuild, however, a new and different password would be generated. Reactors would fire, too, and a welcome email would be sent out...again. These kinds of things are problems, in my mind.

First, I don't think these are issues that ought to be resolved by this package...nor could they, easily, I suppose. So...unless I'm off-base somewhere, I'm wondering if anyone has any suggestions on how one might go about addressing such problems.

Any input/advice would be welcome.

Plans to support async event processing?

First, thanks for building this! Looks like it will be a great help.

Do you have any plans to support async event processing rather than how the package (I presume) works currently where events are processed during the same request as an event is created?

More generally, do you think there is a benefit to async event processing? Could it make an event-sourced application more robust?

setCircularReferenceLimit

I have the setup as the code snippets below and I'm getting a tricky to google "A circular reference has been detected when serializing the object of class "Illuminate\Database\MySqlConnection" (configured limit: 1)".

Create a Unit. When the unit is actually created in UnitCreated I want to create a random number of Augments, and then assign those augments to the unit, instantiating AugmentAdded events.

Is this broken or am I using the wrong pattern?

Unit.php:

public static function createWithAttributes(array $attributes): Unit
{

$attributes['uuid'] = (string) Uuid::uuid4();

/*
 * The Unit will be created inside this event using the generated uuid.
 */
event(new UnitCreated($attributes));

/*
 * The uuid will be used the retrieve the created Unit.
 */
return static::uuid($attributes['uuid']);

}

public function addAugment(Augment $augment)
{

event(new AugmentAdded($this->uuid, $augment));

}
UnitProjector.php:

public function onUnitCreated(UnitCreated $event)
{
$unitCreated = Unit::create($event->unitAttributes);
}

public function onAugmentAdded(AugmentAdded $event)
{
$unit = Unit::uuid($event->unitUuid);
$unit->augments()->attach($augment->id);
$unit->save();
}
class AugmentAdded implements ShouldBeStored {

/** @var string */
public $unitUuid;

/** @var augment */
public $augment;

public function __construct(string $unitUuid, Augment $augment)
{
$this->unitUuid = $unitUuid;
$this->augment = $augment;
}
}

Run projectors in a specific order

First of all, thanks for this awesome library.

I got into a scenario where one projector has to be executed before the second one. It's on update of a field that's needed in the second projector.

Or maybe i am applying it (event sourcing, etc) wrongly.

Thanks!

Can't work with hyn/multi-tenant

I want to integrate this package with hyn/multi-tenant package. When I ran test, the test always failed with there error:

Illuminate\Contracts\Container\BindingResolutionException: Unresolvable dependency resolving [Parameter #0 [ <required> array $config ]] in class Spatie\EventProjector\Projectionist

/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:948
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:886
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:827
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:795
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:646
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:601
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:734
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:900
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:828
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:795
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:646
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Container/Container.php:601
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:734
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:398
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:375
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:209
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php:182
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php:162
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:173
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:156
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php:36
/home/dim/Codes/web/portal2/newportal/vendor/hyn/multi-tenant/src/Providers/TenancyProvider.php:66
/home/dim/Codes/web/portal2/newportal/vendor/hyn/multi-tenant/src/Providers/TenancyProvider.php:43
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:573
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Foundation/ProviderRepository.php:75
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:548
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php:17
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:206
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php:297
/home/dim/Codes/web/portal2/newportal/tests/CreatesApplication.php:18
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:91
/home/dim/Codes/web/portal2/newportal/tests/TenantAwareTestCase.php:27
/home/dim/Codes/web/portal2/newportal/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestCase.php:68
/home/dim/Codes/web/portal2/newportal/tests/TenantAwareTestCase.php:19
/home/dim/Codes/web/portal2/newportal/tests/Feature/TenantCreateCommandTest.php:16

Any input/suggestion about this error would be very appreciated.

replaying events does not use custom 'stored_event_model'

When using a custom StoredEvent model (and correct setup in the config), while replaying Events, it will still use the (hard coded) Spatie\EventProjector\Models\StoredEvent class.

I was using a custom class for auto encrypting / decrypting of the payload, which fails during replay when the default StoredEvent is trying to unserialize the encrypted data.

Spatie\EventProjector\Console\Concerns\ReplaysEvents.php should check the config and use the class that is setup in there instead of using 'Spatie\EventProjector\Models\StoredEvent.php'

Subscribe only to specific events

Should be nice to add a config parameter to EventSubscriber::subscribe in order to specify a list of event to listen avoiding to listen all events (now there is the wildchar *).

The steps to replay events in tutorial are incorrect

Hi,

I'm following the tutorial on replaying the events (https://docs.spatie.be/laravel-event-projector/v1/basic-usage/handling-side-effects-using-reactors) - per the example:

If you truncate the accounts table and rebuild the contents with

php artisan event-projector:replay

no mail will be sent.

Unfortunately, afrter truncating the accounts table and running the command I'm getting following output:

Are you sure you want to replay events to all projectors? (yes/no) [no]:
 > yes

There are no events to replay.
There are no events to replay

After debugging, to properly replay the events I've had to empty projector_statuses table (which I suspect stores the status of the latest event that was handled by given projector). Afterwards running php artisan event-projector:replay correctly outputs:

Replaying all events...

 2/2 [============================] 100%

All done!

Cheers!

Handling Eloquent Relationship

Quick sanity check here if I can.

I've an app with has bars and beers.

A bar can have a beer added (which may or may not already exist) and removed, so I've been using a standard hasMany relationship.

I wanted to "track" these beers being added and removed to bars using event projectors instead.

Where before I used Eloquents sync method:

$bar->beers()->sync($arrayOfBeerIds)

I created my own version of Eloquent sync:

$bar->syncBeers($beerIds);
 public function syncBeers(Collection $beers): array
    {
        $beers = $beers->toArray();
        $current = array_keys($this->beers->toArray());

        // first lets see what needs attached
        $toBeAttached = collect(array_diff($beers, $current));

        // now lets see what needs detached
        $toBeDetached = collect(array_diff($current, $beers));

        $toBeAttached->each(function ($beerId) {
            event(new AttachBeerToBar(['beer_id' => $beerId,
                                        'bar_id' => $this->id,
                                        'uuid' => (string) Uuid::uuid4()]));
        });

        $toBeDetached->each(function ($beerId) {
            event(new RemoveBeerFromBar(['beer_id' => $beerId,
                                        'bar_id' => $this->id]));
        });

        return ['attached' => $toBeAttached,
                'detached' => $toBeDetached];
    }

Which then fires the Events\AttachBeerToBar and Events\RemoveBeerFromBar to handle the creation and deletion of the rows on the BarBeer pivot table.

Does this seem like a sensible approach or would you suggest somehow hooking into laravels own sync() methods? It's all working so I'm happy enough.

Queues vs. Streams

I'm trying to understand when to use streams, and when to use a queue to process events.
If all the incoming events are placed on a queue, and processed one at a time, is there any reason to use different streams? Maybe I'm not getting the point of streams, but from the docs, it sounds like they are for preventing race conditions with processing events concurrently. If they are all in a single queue though, is this a concern?

Event projector not receiving events

Hello, first of all thanks a lot for this awesome package! I heard about event sourcing recently and am happy that there's already something readymade for Laravel. Anyway, on to my question:

I have a very simple event class for firing events when someone logs into the app. This uses the default auth, so there isn't anything for the projector to do as such, but I wanted to just have the event logged:

<?php

namespace App\Projectors\Common;

use Spatie\EventProjector\Projectors\Projector;
use Spatie\EventProjector\Projectors\ProjectsEvents;
use Illuminate\Support\Facades\Log;

use App\Events\Common\LoggedIn;

class LoggedInProjector implements Projector
{
    use ProjectsEvents;

    protected $handlesEvents = [
        LoggedIn::class => 'onLoggedIn',
    ];

    public function onLoggedIn(LoggedIn $event)
    {
        Log::info('Damn, someone logged in!');
        echo 'Damn, someone logged in!';
    }
}

As you can see, I'm logging and echoing a string as "proof" that this projector is receiving events. I do believe I have Redis set up correctly (the name of the queue being all_events), as I can see the events stored in Redis and then disappear when I run the worker.

hrms-ait$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.10-0ubuntu0.18.04.1 β€” cli) by Justin Hileman
>>> use App\Events\Common\LoggedIn;
>>> event(new LoggedIn(['user_id' => 7, 'role' => 'a3']));
=> [
     null,
   ]

This now gets stored in the stored_events table. Then I run the worker:

hrms-ait$ php artisan queue:work --queue=all_events
[2018-11-02 17:14:14][VhgW0a5jVhA7nMaleYRy0XYQyX3fJXRf] Processing: Spatie\EventProjector\HandleStoredEventJob
[2018-11-02 17:14:14][VhgW0a5jVhA7nMaleYRy0XYQyX3fJXRf] Processed:  Spatie\EventProjector\HandleStoredEventJob

And yet, there's no logging and no echo anywhere. Looks like the projector didn't receive anything:

mysql> select * from projector_statuses \G
*************************** 1. row ***************************
                     id: 1
         projector_name: App\Projectors\Common\LoggedInProjector
                 stream: main
has_received_all_events: 0
last_processed_event_id: 0
             created_at: 2018-11-02 15:09:12
             updated_at: 2018-11-02 15:09:12
1 row in set (0.00 sec)

. . . even though I have my projector registered in config/event-projector.php:

'projectors' => [
        App\Projectors\Common\LoggedInProjector::class,
    ],

Even if I not register my projector, I get the same behavior as before, so I daresay the projector isn't receiving the events. Any idea why this might be?

Thanks so much in advance!

EventHandlerCollection is missing `use Spatie\EventProjector\Exceptions\InvalidEventHandler;`

Hi,

I've been following the tutorial (https://docs.spatie.be/laravel-event-projector/v1/basic-usage/handling-side-effects-using-reactors) and after adding the Reactor I've noticed a fatal error:

Class 'Spatie\EventProjector\EventHandlers\InvalidEventHandler' not found in EventHandlerCollection.php:29

to which, after debugging, I've found that there's no inclusion of Spatie\EventProjector\Exceptions\InvalidEventHandler, but it's being used, thus the error.

Adding the inclusion per use Spatie\EventProjector\Exceptions\InvalidEventHandler; solves the issue.


I've stumbled upon this as in tutorial class BigAmountAddedReactor doesn't implement EventHandler nor use HandlesEvents. The example files in https://github.com/spatie/laravel-event-projector-demo-app/blob/master/app/Reactors/BigAmountAddedReactor.php are correct.

Cheers!

Snapshotting feature?

Hi,

I have been looking and playing with this package and so far so good πŸ‘. I was however wondering if it had snapshotting capabilities. My conclusion now seems to be "no". But it seems it did had that feature one time. I found the fallowing info so far:

So my question now is if it's on the roadmap? What was the reason the old snapshotting capabilities where removed?

BTW, I have to say that (allot of) the commit messages in this repo are ... well, terrible :P. On a positive side! The separate documentation site is easy/nice to read, however there are some mistakes in the examples and some of the wording is not totally correct. Like for example I came across the "Image" while it should have been "Imagine".

Event Sourcing and Laravel Nova...

This may not be the best place to ask the question, but I wasn't sure where else I could ask and I figured someone from Spatie would have some good insight.... :-)

So...I've just started to build up a new project using this package, planning to take an event sourcing approach. Since the announcement of Nova, I've also planned to use that as an admin tool. Last night I realized that Laravel Nova and any event sourcing model probably won't play nice together.

Can anyone offer any insight into this? Event sourcing is something I'd like to do. Laravel Nova is something I pretty much need to use (assuming it works as advertised). Before I go much further, I'd like to know if I'm wasting time with event sourcing on this particular project. (There is another project on the horizon that will not need something like Laravel Nova and so I can apply event sourcing when I work on that.)

Thanks in advance for any feedback/discussion/whatever. :-)

Slight event handling declaration improvement...

Hi,

I'd like to make what I consider a slight improvement to the event handling declaration for a projector.

From the documentation (https://docs.spatie.be/laravel-event-projector/v1/basic-usage/writing-your-first-projector#creating-your-first-projector):

    /*
     * Here you can specify which event should trigger which method.
     */
    protected $handlesEvents = [
        AccountCreated::class => 'onAccountCreated',
        MoneyAdded::class => 'onMoneyAdded',
        MoneySubtracted::class => 'onMoneySubtracted',
        AccountDeleted::class => 'onAccountDeleted',
    ];

I would like to allow for the following:

    /*
     * Here you can specify which event should trigger which method.
     */
    protected $handlesEvents = [
        AccountCreated::class,
        MoneyAdded::class,
        MoneySubtracted::class,
        AccountDeleted::class,
    ];

To me, this seems a bit easier/nicer and less error-prone. That is, instead of writing handler method name in two places, one must only enter it once. I know it's minor, but it seems like an improvement.

So, my thought is that when the array is in this structure, then it will automatically look for on{$eventName} methods.

I've just forked the project and am happy to find and submit a solution, but I'd first like to hear back to find out if it's time worth spending.

Thx....

Going 'back in time'

Just getting my head around this package, its great!

Is it easy enough to recreate a state at a certain point in time? Eg using the bank example is there a simple way to show an account balance on xxx date?

Storing a lot of events

Great package. Very easy to get started indeed πŸ‘

Just curious, how would you deal with a lot of stored events? Would you remove the old/processed ones? πŸ€”

assuring priority to store the event?

Hello,
the package seems interesting and nice idea are present but one thing in event sourcing is very important : being sure to be able to reread the event, so be sure it is stored. So for me it's important that storing the event is coming as a first step before anything else.

Why?
Because if you can't store the event, the event shouldn't be dispatched. Otherwise you risk some incoherence in case of replaying. So for me, if this case is not solved you risk very bad stuff.
So the issue is not something that could happens in all case (imagine that you can save an account that have been changed by an event but not the event itself) but it's something that should be considered or at least have a warning about it.

As I have seen a lot of people using a tech to dispatch the event that store it for them (so dispatching the event = storing it) that solve this particular issue by itself.

A possible solution would be, not to have a priority system (laravel remove it and reading it is not a so nice solution (I have done it on my project)) but simply log the event to be able to restore it by some manual operation.

thanks for reading

Call to undefined function Spatie\EventProjector\Models\now()

Lumen 5.6

vendor/spatie/laravel-event-projector/src/Models/StoredEvent.php:31

public static function createForEvent(ShouldBeStored $event): StoredEvent
{
        $storedEvent = new static();
        $storedEvent->event_class = get_class($event);
        $storedEvent->attributes['event_properties'] = app(EventSerializer::class)->serialize(clone $event);
        $storedEvent->meta_data = [];
        $storedEvent->created_at = now(); 

        $storedEvent->save();

        return $storedEvent;
}

Error Setting Up Using the Documentation

After following the example in documentation, I tried using tinker to test and came up with this error:
Spatie/EventProjector/Exceptions/InvalidStoredEvent with message "Failed to unserialize an event with class App/Events/CompanyCreatedEvent on stored event with id "7". Are you sure that event class exists?"

The only thing that seems different from the example is that I'm not using uuid, rather a field from the request data. The field is unique for each entry and is provided by the user, hence can be used to query the DB for a specific entry.

Here is the CompanyCreatedEvent class:
`class CompanyCreatedEvent implements ShouldBeStored {
use Dispatchable, InteractsWithSockets, SerializesModels;

/**
 * @var array
 */
public $companyAttributes;

/**
 * Create a new event instance.
 *
 * @param array $attributes
 */
public function __construct(array $attributes) {
    $this->companyAttributes = $attributes;
}

}`

and here is the Company model:
`
class Company extends BaseModel {
use SoftDeletes;

protected $fillable = ["name", "domain", "slogan", "phones", "logo", "is_enabled"];
protected $dates = ["deleted_at"];
protected $casts = ["is_enabled" => "bool"];

/**
 * Returns the customer phone numbers as an array
 *
 * @return array
 */
public function getPhonesArray() {
    return explode(",", $this->phones);
}

/**
 * @param array $attributes
 * @return Company
 */
public static function createWithAttributes(array $attributes) {
    event(new CompanyCreatedEvent($attributes));

    return static::domain($attributes["domain"]);
}

public function remove() {
    event(new CompanyDeletedEvent($this->id));
}

/**
 * @param array $attributes
 * @return Company
 */
public static function updateWithAttributes(array $attributes) {
    event(new CompanyUpdatedEvent($attributes));

    return static::find($attributes["id"])->first();
}

/**
 * @param string $domain
 * @return Company
 */
public static function domain(string $domain) {
    return static::where("domain", $domain)->first();
}

}
`

RebuildCommand doesn't work if projector list is ommited

EventProjectionist::getProjectors() returns a collection of strings, while EventProjectionist::getProjector() instantiates a projector before returning it. This causes RebuildCommand to attempt to call reset() on strings, rather than projector instances, due to the way the SelectsProjectors trait fetches projectors when the projector list is ommited.

There are three ways to fix this:

  • Change EventProjectionist::getProjectors() to return Projector instances;
  • Change SelectsProjectors to assign $projectorClassNames = EventProjectionist::getProjectors() instead of the early return;
  • Change RebuildCommand to check if the projectors have been instantiated before attempting to call reset() on them.

The first option would probably be best, possibly with the addition of a new getProjectorNames method to let developers check what projectors have been registered without having to instantiate them.

UUID's vs ID's

In the docs, Freek says Using a uuid is not strictly required, but it will make your life much easier when using this package.

image

I've ended up using a mix of regular id's and uuids in my project and I sense a code smell.

Can someone explain why I should favour uuids?

Remove constructor arguments restriction for deserialize

Would it be possible to have the deserializer Just Workℒ️ without us needing to match the constructor signature with the stored attributes?

This doesn't work now:

<?php

namespace App\Products\Events;

use Spatie\EventProjector\ShouldBeStored;

class ProductCreated implements ShouldBeStored
{
    /** @var string */
    public $id;

    /** @var string */
    public $name;

    /** @var int */
    public $price;

    public function __construct(string $id, array $attributes)
    {
        $this->id = $id;

        $this->name = $attributes['name'];
        $this->price = $attributes['price'];
    }
}
Cannot create an instance of App\Products\Events\ProductCreated from serialized data because its constructor requires parameter "attributes" to be present.

Example in the documentation not working

Hi,

I was trying to test your package running the example from the documentation, but I cannot manage to do so. I created the model, the events and the projector strictly following the doc, but when I run $account = App\Account::createWithAttributes(['name' => 'Julien']);, $account is equal to null.

Am I doing something wrong or is the example erroneous?

Specify a DB Connection to find Stored Events

Context:

I'm working on an app where it makes sense to take a multi-tenant approach. I figured it may be optimal if I store each of the tenants' events in their own database. It also could make it easier down the road to handle some of the clients' events in seperate or dedicated event queues.

Problem:

I noticed you can't specify a database connection on the commands.

Solution:

I can solve the issue for storing events (StoredEvent@getConnectionName should set a tenants connection if the event is in this list of events.)

I can solve the issue for projecting because the models will be set to the tenant connection.

However, with the current implementation, is there a way I can specify a connection for the commands so they know where to find the stored events, or does this need to be PR'd in?

Question:

Do you think this is worth doing/solving? For my use case, there is a clear seperation of data (clients don't interact with one-another.) I'm not sure how performant event projection will be over time using a single event queue between many clients (as new projectors are created.)

Laravel 5.7 fails test

Hi,

Due to the recent bump to Laravel 5.7, tests are now partially failing for this package. Seeing the build logs, the tests continue to work when installing using --prefer-lowest.

I'd submit a PR myself, but I don't know which strategy you'd prefer. Do you want to continue supporting 5.6< or not?

Saving metadata from a projector: auth()->user() is null

Thanks for this well-written package. I've been playing around with it for the last couple of days, and it certainly offers amazing potential.

One thing I've come across, and it might be slightly related to #75, is that metadata is saved from the projector(s) itself. The docs state that $storedEvent->meta_data['user_id'] = auth()->user()->id can be used to store the user id. This works perfectly, but not when rebuilding events (e.g. calling artisan event-projector:rebuild), as auth()->user() is null in that case.

Perhaps I'm misunderstanding the usage of the rebuild command, but doesn't it make sense to set metadata in the event itself as well? In that case, the problem described above can be avoided.

Another scenario where metadata in the event might be handy is when two projectors listen to the same event. Imagine you'd want to save a user.id to an event's metadata. How do you decide in which projector you'd put the code for saving the user.id? I assume not in both projectors.

projector_statuses table not updating when executing projector from redis queue

Hello!

First of all thanks for the great work.

I am having an issue with the queued projectors. If I use the sync driver everything works fine, but when I set the driver to use the redis connection, the projector_statuses table does not get updated after the projector is executed.

I have tried to debug the issue myself but I don't have enough knowledge of the codebase to do it :(

I am using Laravel Horizon to handle the queues.

Consider adding a Replayable interface for projectors

When replaying a projector that doesn't have a resetState method, a method not found exception is thrown that's hard to debug for first-timers.

Requiring a Replayable interface would make this more explicit. Alternatively, do a method_exists call and provide an error message to clarify that the package user must implement resetState.

Usage

Can you please update the usage? I would like to try this out

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.