A pragmatic event sourcing library for PHP with a focus on developer experience.

Overview

EventSaucePHP

Scrutinizer Code Quality Code Coverage Build Status

EventSauce is a somewhat opinionated, no-nonsense, and easy way to introduce event sourcing into PHP projects. It's designed so storage and queueing mechanisms can be chosen based on your specific requirements. It has test tooling, designed to work with an event sourcing mindset.

That's it.

View the docs at eventsauce.io/docs

Comments
  • Retrieving an aggregate root with an invalid ID causes no error

    Retrieving an aggregate root with an invalid ID causes no error

    Here's an interesting question, perhaps an edge case. When using the \EventSauce\EventSourcing\ConstructingAggregateRootRepository::retrieve method with an AggregateRootId that does not actually exist (i.e. has no events) does not result in an error at all: it merely creates an instance of the AggregateRoot with no events.

    I imagine a simple check in \EventSauce\EventSourcing\AggregateRootBehaviour::reconstituteFromEvents would fix this, for example:

    /** @var AggregateRootBehaviour $aggregateRoot */
    $aggregateRoot = new static($aggregateRootId);
    
    $appliedEvents = 0;
    
    /** @var object $event */
    foreach ($events as $event) {
        $aggregateRoot->apply($event);
        $appliedEvents++;
    }
    
    if ($appliedEvents === 0) {
        throw new \RuntimeException('noooo');
    }
    
    /* @var AggregateRoot $aggregateRoot */
    return $aggregateRoot;
    

    Would be cleaner to check for the presence of events before the loop and return early, but since a generator is being used, have to check after the loop.

    If this is something desirable, let me know and I'll put together a patch.

    opened by asgrim 22
  • Recommendations? Composer psr-4 skipping yml files

    Recommendations? Composer psr-4 skipping yml files

    Hi @frankdejonge et al,

    Since I'm on composer v2 it skips the commands_and_events.php files because it does not comply with psr-4 autoloading standard. That's technically a correct error as the file defines multiple classes.

    Any recommendation on how to handle this? I expect more people are or will be running into this.

    I've tried loading the file using composer's files directive, but no luck there either.

    Here's an excerpt:

        "autoload": {
            "files": [
                "src/Domain/SomeModule/commands_and_events.php"
            ],
            "psr-4": {
                "App\\": "src/App/",
                "Database\\Factories\\": "database/factories/",
                "Database\\Seeders\\": "database/seeders/",
                "Domain\\": "src/Domain/",
                "Support\\": "src/Support/"
            }
        },
        "autoload-dev": {
            "psr-4": {
                "Tests\\": "tests/"
            }
        },
    
    opened by sandervanhooft 20
  • Isn't replaying domain message events by default slow as hell

    Isn't replaying domain message events by default slow as hell

    As the list of events grows for an aggregate root, just loading the aggregate root and having all those events replayed is just going to get slower and slower. If I am missing something please help me understand.

    opened by justageek 15
  • Doctrine MessageRepository appears to have no upgrade path

    Doctrine MessageRepository appears to have no upgrade path

    I am upgrading to EventSauce 1.x and was prompted to switch to MessageRepositoryForDoctrineV2, which is fundamentally incompatible with the old DoctrineMessageRepository due to this method.

    I cannot comprehend why using a binary UUID is preferable to a true UUID type, except for the people using MySQL/MariaDB. There does not appear to be any documentation for this change, nor any suggestion for how to migrate my existing tables to use this schema, even if I wanted to.

    And to make the situation worse, this method is marked as private so there is no way for me to extend the default implementation in a way that would make it compatible with my existing tables. It appears my only recourse is to copy/paste this whole file into my project and alter it.

    EventSauce is fantastic, but this change is essentially broken by design. ☹️

    opened by shadowhand 13
  • What does the project need? I want to contribute.

    What does the project need? I want to contribute.

    Hey @frankdejonge!

    First, I feel the need to say that "pragmatic event sourcing for PHP" is the PERFECT description of what 0.6 has achieved so far. I've worked with Prooph, Broadway and implemented my own ES stack 3 or 4 times after coming to feel that every one of them will be different and "building a ES framework is impossible!". I believe you have struck the best balance in small, focused interfaces to pull off the core without all the bloat and over-architecture present in other similar "frameworks".

    Second, I want to contribute and promote this package. What do you need help with? As far as I can tell, the library is more-or-less feature complete (certainly enough for my own needs), but what is your "wish-list" of things you would add? Do we want some example projects? If so, do you have any suggestions on domains more complex than "To-do" or "banking", which seem to be the only event-sourced applications folks are building in the open :grinning:.

    Anywho, thanks for putting time into this and I look forward to working with the project!

    opened by mdwheele 10
  • Reconstituting a specific version

    Reconstituting a specific version

    Hey Frank

    First of all, let me say thank you for this library and all the other OSS stuff you do! I've just started using EventSauce and I'm starting to get familiar with all the concepts. I love all the flexibility and the way you focussed on providing the building blocks so I can do whatever I want myself :) Great stuff! ❤️

    One thing I'm missing from the AggregateRootRepository is to retrieve an aggregate in a specific version. I think this is one of the areas where event sourcing really shines: comparing versions. I know it's basically doable by injecting the MessageRespository myself and ignoring all the versions after n but it feels like a little design issue because I would need to inject both, the correct AggregateRootRepository which itself depends on the MessageRepository as well as the repository itself. Or well, I would need to create my own implementation of AggregateRootRepository which decorates e.g. the EventSourcedAggregateRootRepository. Is that what I'm supposed to do?

    So basically: Did I miss anything built-in or is there really no way to fetch that currently? 😇

    opened by Toflar 8
  • Passing aggregateRoot to $snapshotRepository->persist(Snapshot $snapshot)

    Passing aggregateRoot to $snapshotRepository->persist(Snapshot $snapshot)

    We finally got around to upgrade to 0.7 and use the new snapshotting functionality. I like the new implementation, thanks @frankdejonge

    In our previous homegrown snapshot implementation we stored the aggregate type and code version of the aggregate so we could easily remove previous/outdated snapshots in our database.

    The eventsauce docs describe it on the snapshot page as well: "Versioning Snapshots Snapshots are stored in the database. When your aggregate root evolves, so must your snapshots. A good practise is to version your snapshots. Storing a version along with your snapshot allows you to filter out any outdated ones when trying to fetch your aggregate root."

    However the Snapshot object does not really provide a way to pass this information. I've now solved this by passing the aggregateRoot to the snapshot persist function and grabbing both type and code version. Would extending the interface for the snapshot repository make sense?

    Something like?

    interface SnapshotRepository
    {
        public function persist(Snapshot $snapshot, AggregateRootWithSnapshotting $aggregateRoot): void;
    
        public function retrieve(AggregateRootId $id): ?Snapshot;
    }
    

    My Repository then does something like this (VersionedSnapshots is extending the AggregateRootWithSnapshotting interface with a method to grab the current version of the code) :

        /**
         * @throws \Doctrine\ODM\MongoDB\MongoDBException
         */
        public function persist(Snapshot $snapshot, VersionedSnapshots $aggregateRoot = null): void
        {
            $collection = $this->collectionManager->getCollection(Snapshots::class);
    
            $data = [
                'aggregate_root_id' => $snapshot->aggregateRootId()->toString(),
                'aggregate_root_id_serialized' => serialize($snapshot->aggregateRootId()),
                'aggregate_root_version' => $snapshot->aggregateRootVersion(),
                'state' => json_encode($snapshot->state()),
            ];
            if (null !== $aggregateRoot) {
               // the $aggregateRoot should always be passed to this function, but the interface doesn't include it
                $data['aggregate_snapshot_code_version'] = $aggregateRoot->getAggregateSnapshotVersion();
                $data['aggregate_type'] = get_class($aggregateRoot);
            }
    
            $collection->replaceOne(['aggregate_root_id' => $snapshot->aggregateRootId()->toString()], $data, ['upsert' => true]);
        }
    

    Or should i solve this in a different way?

    opened by arendjantetteroo 8
  • Unexpected Exception in `->when()`

    Unexpected Exception in `->when()`

    When using the test helper ->when() there is a separate method ->expectToFail(). But there is no opposite of it.

    Any exception that is thrown in the events in when will just be caught in AggregateRootTestCase:

    } catch (Exception $exception) {
        $this->caughtException = $exception;
    }
    

    This way when using asserts on the retrieved aggregate root the values will just be wrong but I don't know about a failing command. Am I missing something or is there now way around that at them moment?

    Would be awesome if this would be provided.

    opened by christian-kolb 8
  • Feature: Snapshotting

    Feature: Snapshotting

    This PR introduces support for snapshotting as an opt-in feature. In order to start using snapshotting you'll need to the following:

    Step 1

    Make sure your aggregate root implements the EventSauce\EventSourcing\Snapshotting\AggregateRootWithSnapshotting interface. This interface extends the base interface so you only need one.

    Step 2

    Use the EventSauce\EventSouring\Snapshotting\SnapshottingBehaviour trait in addition to the AggregateRootBehaviour trait. This will require you to implement the protected function createSnapshotState(); and abstract protected function setPayloadState($state): void; methods.

    The goal of these methods is to facilitate storing of the aggregate's state at any given point in time.

    Step 3

    Create an implementation of the EventSauce\EventSourcing\Snapshotting\SnapshotRepository interface. You can use your preferred way of storing your snapshot in any persistence you like.

    Step 4

    Start using the \EventSauce\EventSourcing\Snapshotting\AggregateRootRepositoryWithSnapshotting interface, a concrete implementation is provided: \EventSauce\EventSourcing\Snapshotting\ConstructingAggregateRootRepositoryWithSnapshotting

    This implementation decorated the existing AggregateRootRepository interface.

    Usage

    An example aggregate root with snapshotting support:

    <?php
    
    namespace EventSauce\EventSourcing\Snapshotting\Tests;
    
    use EventSauce\EventSourcing\AggregateRootBehaviour;
    use EventSauce\EventSourcing\Snapshotting\SnapshottingBehaviour;
    use EventSauce\EventSourcing\Snapshotting\AggregateRootWithSnapshotting;
    
    class LightSwitch implements AggregateRootWithSnapshotting
    {
        use AggregateRootBehaviour, SnapshottingBehaviour;
    
        const OFF = false;
        const ON = true;
        private $state = self::OFF;
    
        private function createSnapshotState()
        {
            return $this->state;
        }
    
        private function setPayloadState($state)
        {
            $this->state = $state;
        }
    
        public function turnOn(): void
        {
            if ($this->state == self::OFF) {
                $this->recordThat(LightSwitchWasFlipped::on());
            }
        }
    
        public function turnOff(): void
        {
            if ($this->state == self::ON) {
                $this->recordThat(LightSwitchWasFlipped::off());
            }
        }
    
        protected function applyLightSwitchWasFlipped(LightSwitchWasFlipped $event)
        {
            $this->state = $event->state();
        }
    }
    

    Using the new aggregate root repository can be done as followed:

    
    $aggregateRoot = $aggregateRootRepository->retrieveFromSnapshot($aggregateRootId);
    
    // interact with the model
    
    // persist to store and dispatch events as usual
    $aggregateRootRepository->persist($aggregateRoot);
    
    // update the snapshot
    $aggregateRootRepository->storeSnapshot($aggregateRoot);
    

    Behaviour Explained

    The snapshotting mechanism stores a snapshot state along with the aggregate root id and version. When reconstituting from a snapshot, the snapshot is retrieved from storage, and any events that happened after the snapshot was stored are fetch and applied to the model. This prevents you from needing to store every version. It still gives you the latest state of your aggregate root, even when events have been recorded after storing the snapshot.

    opened by frankdejonge 8
  • Q: How does EventSauce handle concurrency issues?

    Q: How does EventSauce handle concurrency issues?

    I can see that there is a concept of a Aggregate Root version, but I'm not sure if it is used for preventing concurrency issues, i.e. two or more events occurring at the same time? Actually I'm not sure what is its purpose. Other solutions such as Prooph use unique key constraints, something like: UNIQUE KEY `ix_unique_event` (`aggregate_type`, `aggregate_id`, `aggregate_version`). Other languages rely on manual checks before persisting events: https://github.com/gregoryyoung/m-r/blob/master/SimpleCQRS/EventStore.cs#L52.

    What I'm also missing in the documentation is a sample DB schema and recommended way for persisting aggregates, for instance single table strategy, separate table for every aggregate type, and similar.

    question 
    opened by nikolaposa 8
  • DefaultHeadersDecorator order parameters deprecated

    DefaultHeadersDecorator order parameters deprecated

    Hi,

    The order of the parameters in the DefaultHeadersDecorator is deprecated. My console shows warnings about that .

    See - https://php.watch/versions/8.0/deprecate-required-param-after-optional

    Best Regards.

    opened by andrzepakula 6
  • Bump json5 from 2.2.0 to 2.2.3 in /docs

    Bump json5 from 2.2.0 to 2.2.3 in /docs

    Bumps json5 from 2.2.0 to 2.2.3.

    Release notes

    Sourced from json5's releases.

    v2.2.3

    v2.2.2

    • Fix: Properties with the name __proto__ are added to objects and arrays. (#199) This also fixes a prototype pollution vulnerability reported by Jonathan Gregson! (#295).

    v2.2.1

    • Fix: Removed dependence on minimist to patch CVE-2021-44906. (#266)
    Changelog

    Sourced from json5's changelog.

    v2.2.3 [code, diff]

    v2.2.2 [code, diff]

    • Fix: Properties with the name __proto__ are added to objects and arrays. (#199) This also fixes a prototype pollution vulnerability reported by Jonathan Gregson! (#295).

    v2.2.1 [code, diff]

    • Fix: Removed dependence on minimist to patch CVE-2021-44906. (#266)
    Commits
    • c3a7524 2.2.3
    • 94fd06d docs: update CHANGELOG for v2.2.3
    • 3b8cebf docs(security): use GitHub security advisories
    • f0fd9e1 docs: publish a security policy
    • 6a91a05 docs(template): bug -> bug report
    • 14f8cb1 2.2.2
    • 10cc7ca docs: update CHANGELOG for v2.2.2
    • 7774c10 fix: add proto to objects and arrays
    • edde30a Readme: slight tweak to intro
    • 97286f8 Improve example in readme
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 0
  • Bump minimatch from 3.0.4 to 3.1.2 in /docs

    Bump minimatch from 3.0.4 to 3.1.2 in /docs

    Bumps minimatch from 3.0.4 to 3.1.2.

    Commits

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 0
  • Type mismatched... Expected `AggregateRootId` found `AggregateRootIdType`

    Type mismatched... Expected `AggregateRootId` found `AggregateRootIdType`

    This type of mismatch is repeated multiple times.

    P.S: I see you are updating the docs, you might want to remove the deprecated stuff too, please. Thanks very much for this amazing library.

    <?php
    
    declare(strict_types=1);
    
    namespace EventSauce\EventSourcing;
    
    use Generator;
    
    /**
     * @template AggregateRootIdType of AggregateRootId
    +  * This might be the cause of it... not sure why you have it here. IMHO, `AggregateRootId` is just perfect.
     * 
     *
     * @see AggregateRootBehaviour
     */
    interface AggregateRoot
    {
        /**
    -      * @return AggregateRootIdType
    +     * @return AggregateRootId
    
         */
        public function aggregateRootId(): AggregateRootId;
    
        public function aggregateRootVersion(): int;
    
        /**
         * @return object[]
         */
        public function releaseEvents(): array;
    
        /**
    -     * @param AggregateRootIdType               $aggregateRootId
    +   * @param AggregateRootId              $aggregateRootId
    
         * @param Generator<int, object, void, int> $events
         */
        public static function reconstituteFromEvents(AggregateRootId $aggregateRootId, Generator $events): static;
    }
    
    
    opened by Parables 0
Owner
EventSauce
An event sourcing library (not framework) for PHP.
EventSauce
A developer-friendly way to handle backend work in Magento 2

A dev-friendly approach to handle background jobs in Magento 2 Overview ?? Now and then we need to create processes that can take some time to execute

Discorgento 24 Dec 2, 2022
Événement is a very simple event dispatching library for PHP.

Événement Événement is a very simple event dispatching library for PHP. It has the same design goals as Silex and Pimple, to empower the user while st

Igor 1.2k Jan 4, 2023
[READ-ONLY] The event dispatcher library for CakePHP. This repo is a split of the main code that can be found in https://github.com/cakephp/cakephp

CakePHP Event Library This library emulates several aspects of how events are triggered and managed in popular JavaScript libraries such as jQuery: An

CakePHP 21 Oct 6, 2022
An asynchronous event driven PHP socket framework. Supports HTTP, Websocket, SSL and other custom protocols. PHP>=5.3.

Workerman What is it Workerman is an asynchronous event-driven PHP framework with high performance to build fast and scalable network applications. Wo

walkor 10.2k Jan 4, 2023
Event-driven, non-blocking I/O with PHP.

Event-driven, non-blocking I/O with PHP. ReactPHP is a low-level library for event-driven programming in PHP. At its core is an event loop, on top of

ReactPHP 8.5k Jan 8, 2023
Revolt is a rock-solid event loop for concurrent PHP applications.

Revolt is a rock-solid event loop for concurrent PHP applications.

Revolt PHP 586 Jan 2, 2023
Infrastructure and testing helpers for creating CQRS and event sourced applications.

Broadway Broadway is a project providing infrastructure and testing helpers for creating CQRS and event sourced applications. Broadway tries hard to n

null 1.5k Dec 27, 2022
Icicle is a PHP library for writing asynchronous code using synchronous coding techniques

Icicle is now deprecated in favor of Amp v2.0. This version is is currently under development, but close to release. The v2.0 branches are amp_v2 in a

icicle.io 1.1k Dec 21, 2022
🚀 Coroutine-based concurrency library for PHP

English | 中文 Swoole is an event-driven asynchronous & coroutine-based concurrency networking communication engine with high performance written in C++

Swoole Project 17.7k Jan 5, 2023
The Hoa\Websocket library.

Hoa is a modular, extensible and structured set of PHP libraries. Moreover, Hoa aims at being a bridge between industrial and research worlds. Hoa\Web

Hoa 419 Dec 30, 2022
The Hoa\Eventsource library.

Hoa is a modular, extensible and structured set of PHP libraries. Moreover, Hoa aims at being a bridge between industrial and research worlds. Hoa\Eve

Hoa 106 Dec 5, 2022
A non-blocking concurrency framework for PHP applications. 🐘

Amp is a non-blocking concurrency framework for PHP. It provides an event loop, promises and streams as a base for asynchronous programming. Promises

Amp 3.8k Jan 6, 2023
Asynchronous coroutines for PHP 7.

Recoil An asynchronous coroutine kernel for PHP 7. composer require recoil/recoil The Recoil project comprises the following packages: recoil/api - T

Recoil 787 Dec 8, 2022
PHP 7.4 EventStore Implementation

Prooph Event Store Common classes and interface for Prooph Event Store implementations. Installation You can install prooph/event-store via composer b

null 532 Dec 9, 2022
Reactive extensions for PHP.

This package is abandoned. Use https://github.com/ReactiveX/RxPHP instead Rx.PHP Reactive extensions for PHP. The reactive extensions for PHP are a se

Alexander 208 Apr 6, 2021
Golang's defer statement for PHP

PHP Defer A defer statement originally comes from Golang. This library allows you to use defer functionality in PHP code. Usage <?php defer($context,

null 249 Dec 31, 2022
Reactive extensions for PHP

RxPHP Reactive extensions for PHP. The reactive extensions for PHP are a set of libraries to compose asynchronous and event-based programs using obser

ReactiveX 1.6k Dec 12, 2022
PHP Application using DDD CQRS Event Sourcing with Hexagonal Architecture

PHP Application using DDD CQRS Event Sourcing with Hexagonal Architecture Application built with Ecotone Framework and powered by integrations with Pr

EcotoneFramework 65 Dec 27, 2022
Ecotone Framework is Service Bus Implementation. It enables message driven architecture and DDD, CQRS, Event Sourcing PHP

This is Read Only Repository To contribute make use of Ecotone-Dev repository. Ecotone is Service Bus Implementation, which enables message driven arc

EcotoneFramework 308 Dec 29, 2022
A "from scratch" PHP-based implementation of Event-Sourcing

In here, you will find a "from scratch" PHP-based implementation of Event-Sourcing, kept to a minimum on purpose, to allow workshop attendees to explore and experiment with its concepts.

Marco Pivetta 94 Jan 1, 2023