Git Product home page Git Product logo

eventsauce's Introduction

Build Status Scrutinizer Code Quality Code Coverage Latest Stable Version Total Downloads

EventSaucePHP

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

eventsauce's People

Contributors

dannyvdsluijs avatar dependabot[bot] avatar driesvints avatar fieg avatar frankdejonge avatar freekmurze avatar gquemener avatar herndlm avatar hyeio avatar jeremyfreeagent avatar johncongdon avatar localheinz avatar marcusmoore avatar marijn avatar matmcal avatar milroyfraser avatar morrisjobke avatar nusje2000 avatar pactode avatar peter-mein-mollie avatar pschmidt88 avatar robertbaelde avatar rojtjo avatar rosstuck avatar rskuipers avatar sandervanhooft avatar simensen avatar thomasschiet avatar toflar avatar yourwebmaker 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  avatar  avatar

eventsauce's Issues

Missing "id" reference in docs

I was check the docs and i notice an odd difference but im not 100% sure it is valid or not. There is table schema defined in the docs:
https://eventsauce.io/docs/message-storage/repository-table-schema/

When reviewing the DoctrineUuidV4MessageRepository it seems that the DefaultTableSchema has a reference to an id field which is not in the schema definition. Problem is that i am not 100% sure what the definition of this ID field should be. I assume part of the PK as an unsigned AI BIGINT?

Aggregate Root Version always set to 0

I'm applying a rename event to an item persisted in the DB with aggregate root version of 2, however EventSauce is setting the next version to 0. I've found the cause (I guess) is line 83 in file AggregateRootBehaviour.php

    public static function reconstituteFromEvents(AggregateRootId $aggregateRootId, Generator $events): AggregateRoot
    {
        /** @var AggregateRoot&static $aggregateRoot */
        $aggregateRoot = new static($aggregateRootId);
        /** @var object $event */
        foreach ($events as $event) {
            $aggregateRoot->apply($event);
        }

        $aggregateRoot->aggregateRootVersion = $events->getReturn() ?: 0;

        /* @var AggregateRoot $aggregateRoot */
        return $aggregateRoot;
    }

$aggregateRoot->aggregateRootVersion = $events->getReturn() ?: 0;

Doing a dump of $events->getReturn() is giving null, so the aggregateRootVersion is set to 0.

Here are my $events

App\Models\Catalog\Events\Imported^ {#721
  -name: "First Item"
}
App\Models\Catalog\Events\Renamed^ {#735
  -name: "Second Time"
}
App\Models\Catalog\Events\Renamed^ {#740
  -name: "55"
}

What is going wrong here that $events->getReturn() returns null?

Cheers

Installation Failed

I just followed the command in installation guide but I get this error

image

I just started with

$ composer init and ran the rest of the command. Are there other steps that I need to do? Also this is the content of my compose.json

{
    "name": "naren/kpaunchha.pv",
    "authors": [
        {
            "name": "Naren",
            "email": "[email protected]"
        }
    ],
    "require": {
        "eventsauce/eventsauce": "^0.2.2"
    },
    "minimum-stability" : "dev"
}

I even changed the minimum stability to dev just to make sure but not working. Looking forward to using this.

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.

Lack of consistency

Firstly I have to say that this is my first issue on GitHub that describes an error. Please don't be offended :). I wanted to use this library in my sample project but after digging into the code, I decided against it. I really like its api, so I can wait until this issue is closed.

This library only wraps messages in a transaction. While this is fine if you don't want immediately consistent read models, this is important to understand the real issue.

// from DoctrineMessageRepository.php

$this->connection->beginTransaction();
$this->connection->prepare($sql)->execute($params);
$this->connection->commit();

The consistency issues start in the next block of code.

// from ConstructingAggregateRootRepository.php

$this->repository->persist(...$messages);
$this->dispatcher->dispatch(...$messages);

After the messages have been persisted successfully, some of the following errors may occur. I take RabbitMq as an example, because you listed it in your documentation. But this can be replaced with any message broker, even sql implementations.

  • A general purpose network fault occurs.
  • RabbitMq has shut down because it is being updated.
  • RabbitMq can't handle new messages.
  • RabbitMq leaves you alone with a connection timeout.
  • ...

If one of the errors above occur, the application is in an inconsistent state because the handlers are not informed that something happened. This isn't even eventual consistency, because it never happened from the view of the handlers.

There are several solutions to this problem.

Firstly, if the library wants to provide immediately consistent read models, like stated in the documentation, it should call the handlers within the same transaction where the messages are persisted. Of course, this only works if the same database and connection is used.

Secondly, all messages which are persisted successfully should be published to all interested handlers in another process. But it's important, that it only publishes messages which are persisted successfully. This process is also called "store and forward". I've seen recommendations to read uncommitted transactions for not reading the events out of order, but this is also faulty because the last sql statement "COMMIT;" eventually never reaches the server and the handlers already read that uncommitted event. The best I've seen is the following approach. It's implemented in my sample project's event store. Actually, the logic is spread over several classes. For the sake of simplicity, I copied the code so it works procedurally. This approach works only for auto increment event ids. Of course you can also create a global event id, like uuidv4. The auto increment id is only important for the following code.

// This script was written down quickly. It's not tested.

// The client should specify the batch size.
$batchSize = 1000;
// The client should specify the throttle time.
$throttleTimeInMicroseconds = 100000;
$lastStoredEventId = 0; // Retrieve the pointer somewhere.
// Build the handlers somewhere.
// This can be read model builders,
// or something that forwards the events to another bounded context / application.
$storedEventSubscribers = [];

while(true) {
    $events = $eventStore->storedEventsSince($lastStoredEventId, $batchSize);

    if (count($events) !== 0) {
        $lastProcessedEventId = $lastStoredEventId;
        foreach ($events as $event) {
            // This following if statement can happen, when two transactions are open,
            // but the second is faster than the first.
            if ($event->id() !== $lastProcessedEventId + 1) {
                // Throw an exception to the client of this library.
                // The client knows best how to handle it.
                // Maybe it's not relevant in the clients particular situation.
                // Maybe the client needs to wait a short amount of
                // time until the previous message comes.
                // Maybe it never arrives because the first transaction was a failing one.
                // However, the client knows best how to handle it.
                throw new OutOfOrderException();
            }
            $storedEventPublisher->publish($event);
            $lastProcessedEventId = $event->id();
        }
        $lastStoredEventId = $lastProcessedEventId;
        // The $lastStoredEventId should be persisted somewhere (not only in memory).
        // If the process is crashed, this process can pick up where its left off.
    } else {
        // Don't stress the sql server if no messages are available.
        usleep($throttleTimeInMicroseconds);
    }
}

There are some caveats with this approach. This is at most once consistency. It happens definitely, but maybe more than one times. All handlers should be idempotent.

Messaging is hard, you may provide a link to a good book so users don't blindly think it works just because it does work in the happy path. Maybe I've overlooked the part where you talk about immediately and eventual consistency approaches in your documentation. I think you should mention these and provide links for further reading.

I can imagine that the library is used productively. Please inform the users about it if you see an issue here.

Some book recommendations

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. โ˜น๏ธ

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?

MessageSerializer::unserializePayload() should return Message

Hello, the current definition of MessageSerializer looks like this:

interface MessageSerializer
{
    public function serializeMessage(Message $message): array;

    public function unserializePayload(array $payload): Generator;
}

When it is used outside the MessageRepository it needs to be wrapped with iterator_to_array($serializer->unserializePayload($payload))[0] or call $serializer->unserializePayload($payload)->current(). In my opinion the signature should be unserializePayload(array $payload): Message;, since it is returning only one message and not a sequence. Message and Payload could also be left out of method names.

Any opinions on that?

Testing with multiple AggregateRoot

Hi all,

I'm working on a project and I'm bumping into the following issue.

First some background information; there exist some entities and relations seen in Figure 1. Events are the "governing" entity where currently a lot of logic is situated. First Categories are created within an Event. Then Persons can be added to Events with a chosen Category. We wish to separate our business rules more, as this is just a fraction of the domain we're examining.

Figure 1
Figure 1: Entity relations

We have event stormed with our development team and came to the conclusion we have the following events and aggregates, as seen in Figure 2. Again, this is just a fraction.

Figure 2
Figure 2: Domain events and aggregates

Suppose I would like to test the following behaviour: an event is started, and a person is added. This is quite simple and follows the examples similarily.

public function test_adding_a_person_to_an_event()
{
    $this->given(
        new EventStarted(),
    )->when(
        new AddPerson($this->aggregateRootId),
    )->then(
        new PersonAdded(),
    );
}

But now, we have the requirement that a Person must have a Category. How would we pass the reference to the Category?

public function test_adding_a_person_to_an_event_with_category()
{
    $this->given(
        new EventStarted(),
        new CategoryCreated()
    )->when(
        new AddPerson($this->aggregateRootId, $categoryId), // <- what is $categoryId?
    )->then(
        new PersonAdded(),
    );
}

Next to that, once we have a Person in place, I would too like to test the following behaviour. Here we have two aggregate roots that are used, the Event and the Person.

public function test_adding_a_person_to_an_event_and_adding_information()
{
    $this->given(
        new EventStarted(),
        new PersonAdded(),
    )->when(
        new AddInformation($this->aggregateRootId), // <- $this->aggregateRootId is from the Event, not the Person
    )->then(
        new InformationAdded(),
    );
}

I found the expressive testing that EventSauce facilitates very pleasing and it works well, however, I'm uncertain if I'm doing something wrong, or that this is just not (currently) supported in EventSauce.

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? ๐Ÿ˜‡

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.

Move all tests to separate folder tests

At first I would like to tell that you made a great job with your EventSauce project.

I am completely new to this project and I would like to advise one big change it the code: to separate all unit tests to new directory tests (at the same level as src is):

src/
tests/

The benefits can be really obvious:

  • better code reading experience,
  • better code structure,
  • smaller number of files in src
  • etc.

As we speak only about unit tests there shouldn't be any problems with backward compatibility or other things.

[Discussion] Aggregate-less Modelling

For a project I'm working on I had the need to use event-based modelling but had no need for an aggregate root. In this specific case used both the MessageDispatcher and MessageRepository implementations and created my own base test case. For this a "classic" event store might be a good encapsulation. But it leaves open a few cases to discuss.

  1. What are the responsibilities for the event store? (mediation between the outside world and dispatcher+repository?)
  2. Would the aggregate root repository be using it? (doesn't have to but could)
  3. Is there another way of looking at this issue? (could non-aggregate-bounded event modelling just be a separate concern altogether?)

Examples of persisting snapshots to a database

The only example of a snapshot repository you provide is InMemorySnapshotRepository. Where can we find an example of how to persist the snapshot to a database, does a simple serialize(), unserialize() work for the Snapshot object?

DoctrineOutboxRepository reserved words escaping

DoctrineOutboxRepository and some other Doctrine specific EventSauce classes are using backticks in sql statements. Postgresql doesn't support it, instead double quotes should be used.

db_1   [590] ERROR:  syntax error at or near "\`" at character 27
db_1   [590] STATEMENT:  INSERT INTO event_outbox (`payload`) VALUES ($1)

Projections

I will have a new challenge to get started soon.
The idea is to implement the event sourcing in Monolith so we can upstream their data to a new service.
I think that projection can be used as a contract in between.
What Do you think about it?
I would like to hear your thoughts.

[Proposal] Facilitate custom reconstitution strategies.

If #27 is completed we've one step away from allowing full decoupling from the library. If the AggregateRootRepository receives a AggregateRootFactory it could become unaware of how aggregate roots are reconstituted. This allows people to create their own hydration strategies. This allows them to wrap their own aggregate root in the library specific aggregate root, thereby allowing decoupling from the lib. This still allows us to use the aggregate root interface for message decoration.

Allow AggregateRootId to have a custom type in code generation.

Input:

namespace: AcmeDomain\ProcessName
aggregate_root_id:
    type: ProcessId
    name: processId
events:
    StartProcess:
        fields:
            reason:
                type: string

Expected output:

<?php

namespace AcmeDomain\ProcessName;

use EventSauce\EventSourcing\AggregateRootId;
use EventSauce\EventSourcing\Command;
use EventSauce\EventSourcing\Event;
use EventSauce\EventSourcing\PointInTime;

final class StartProcess implements Event
{
    /**
     * @var ProcessId
     */
    private $processId;

    /**
     * @var string
     */
    private $reason;

    /**
     * @var PointInTime
     */
    private $timeOfRecording;

    public function __construct(
        ProcessId $processId,
        PointInTime $timeOfRecording,
        string $description
    ) {
        $this->processId = $processId;
        $this->timeOfRecording = $timeOfRecording;
        $this->description = $description;
    }

    public function aggregateRootId(): AggregateRootId
    {
        return $this-> processId();
    }

    public function processId(): ProcessId
    {
        return $this->processId;
    }

    public function reason(): string
    {
        return $this->reason;
    }

    public function timeOfRecording(): PointInTime
    {
        return $this->timeOfRecording;
    }

    public static function fromPayload(
        array $payload,
        ProcessId $processId,
        PointInTime $timeOfRecording): Event
    {
        return new EventWithDescription(
            $processId,
            $timeOfRecording,
            (string) $payload['reason']
        );
    }

    public function toPayload(): array
    {
        return [
            'reason' => (string) $this->description,
        ];
    }

}

[Proposal] Remove Event interface

Proposal

The Event interface is only used for serialization and "intentional" hinting. If we remove the event interface. Since we already have a MessageSerializer the construction could become an implementation detail for the rest of the library. This means if people don't want to be tied to the library, they won't have to be. The AggregateRootRepository and AggregateRoot interface then only function as a reference implementation (easy to swap out when needed/wanted).

What would have have achieved?

Pretty much the possibility to be fully decoupled from the entire library.

Documentation for Hacktoberfest

Hacktoberfest is upon us. In order to make Hacktoberfest a positive experience for everybody, this issue describes what kind of PR's are welcomed with open arms.

Documentation has always been one of the things I've focused on, however, it's always something that can be improved upon. This is especially true for documentation contributions. Did you wish something was covered more (or at all) in the documentation? Please contribute! The more topics covered, the beter!

Happy Hacktoberfest!

PS: Meaningless drive-by PR's will be closed, let's keep this productive for all parties involved.

Docs - navigation buttons

Hi,

I think it helps to read documentation pages faster if they have next and back navigation buttons on each page at the end.

Is it possible to add such buttons?

`AggregateRootBehaviourWithRequiredHistory::reconstituteFromEvents` is incompatible with `AggreagateRoot::reconstituteFromEvents`

I am not really sure how to reconcile this issue, but the Trait throws InvalidAggregateRootReconstitutionException, however, the interface explicitly states that the method does not throw any exception.

This makes development very difficult for several reasons:

  • Static analysis tools are confused
  • IDE does not hint that an exception will be thrown
  • It is difficult to recognize when calling AggregateRootRepository::retrieve whether history for aggregate root is required or not.

The only idea I got is to add AggregateRootRepository::retrieveWithRequiredHistory that can have the exception hint.

No messages for "aggregate_root_id" returns empty object.

Hey there, I'm quite sure this is intentional and I'm aware that I can easily swap out the class.
But I'd love to understand it.

If I retrieve an UUID in ConstructingAggregateRootRepository and there are no messages for it I'm getting an empty object.
(Correct type, but with no messages applied)

I'd expect a AggregateRootNotFound-Exception or something like this.
Why do you return an empty Aggregate here?

Thanks for clarification :-)
Wonderful library.

// ConstructingAggregateRootRepository
public function retrieve(AggregateRootId $aggregateRootId): object
{
    // ...

    return $className::reconstituteFromEvents($aggregateRootId, $events);
}
// ConstructionBehaviour
public static function reconstituteFromEvents(AggregateRootId $aggregateRootId, Generator $events): AggregateRoot
{
    $aggregateRoot = new static($aggregateRootId);
    /** @var object $event */
    foreach ($events as $event) {
       $aggregateRoot->apply($event); 
       ++$aggregateRoot->aggregateRootVersion;
    }

    return $aggregateRoot;
}

Example implemantation custom repository does not use AggregateRootId as object

According to https://eventsauce.io/docs/advanced/custom-repository/ the example implementation should use an string. But it looks like that aggregateRootId is an object or should be an object when following this guide: https://eventsauce.io/docs/event-sourcing/create-an-aggregate-root/

 public function persist(Message ... $messages)
    {
        foreach ($messages as $message) {
            $aggregateRootId = $message->header(Header::AGGREGATE_ROOT_ID);
            $version = $message->header(Header::AGGREGATE_ROOT_VERSION);
            
            if ( ! is_dir(__DIR__.'/'.$aggregateRootId)) {
                mkdir(__DIR__.'/'.$aggregateRootId);
            }

            $payload = $this->serializer->serializeMessage($message);
            file_put_contents(__DIR__."/{$aggregateRootId}/{$version}.json", json_encode($payload, JSON_PRETTY_PRINT));
        }
    }

Message::withHeader() no longer allows array value

Trying to upgrade to EventSauce 1.x and discovered that Message::withHeader no longer accepts an array as a value, which is not mentioned in the upgrade documentation and seems to be an arbitrary restriction, given that an array can be encoded to JSON without issues. Before typing was added, this method had a mixed type and worked with arrays. As such, I believe this is a regression.

We use withHeader via a decorator to attach context variables to the message, such as the requesting IP address, user ID, etc. for audit purposes. Currently this is 9 separate values in the array and it would be burdensome to use separate headers for every one of the values. The code looks like:

return $message->withHeader(self::CONTEXT_HEADER, $this->context->export());

Shouldn't events be applied after event recorded

$this->apply($event);

Hello,

I'm facing an issue when a child aggregate needs to record its own events at instantiation.

Let's take the following exemple:

class Family extends AggregateRoot {

    protected $child;

    public static function create()
    {
        $self = new self();
        $self->recordThat(new ParentCreated());
        $self->recordThat(new ChildCreated());
        return $self;
    }

    public function applyChildCreated(ChildCreated $event)
    {
        $this->child = new Child();
    }
}

and

class Child extends SubAggregate {
    public function __construct()
    {
        $this->recordThat(new ChildCried());
    }
}

When i test this with a BDD approach, i'm expecting this:

$this
    ->given()
    ->when(new CreateFamily()) // which call $family = Family::create(); + $aggregateRepository->persist($family);
    ->then(
        new ParentCreated(),
        new ChildCreated(),
        new ChildCried()
    )

but i got this

$this
    ->given()
    ->when(new CreateFamily()) // which call $family = Family::create(); + $aggregateRepository->persist($family);
    ->then(
        new ParentCreated(),
        new ChildCried(),
        new ChildCreated()
    )

I noticed that to fix it, i needed to move around the content of the recordThat method to be like (so event recorded before apply the event.

    protected function recordThat(object $event): void
    {
        $this->recordedEvents[] = $event;
        $this->apply($event);
    }

I noticed that prooph does it in that order (https://github.com/prooph/event-sourcing/blob/master/src/Aggregate/EventProducerTrait.php#L51) as well as in the book DDD in PHP:

class AggregateRoot {
    protected function recordApplyAndPublishThat(DomainEvent $domainEvent) { 
        $this->recordThat($domainEvent);
        $this->applyThat($domainEvent);
        $this->publishThat($domainEvent);
    } 

    protected function recordThat(DomainEvent $domainEvent){
        $this->recordedEvents[] = $domainEvent;
    }
}

is there a reason why $this->apply($event); is called before $this->recordedEvents[] = $event; in EventSauce.io ? What would be the impact to move it around ?

Move timeOfRecording to headers.

In order to have the most minimal event interface we could move the timeOfRecording (PointInTime) to the headers. The default header message decorator would then account for filling in the default. Because the PointInTime is microsecond precise we'd still have the sequential integrity (when needed by the repository).

Thoughts?

[Additional Thoughts]

This also makes it more natural to use domain specific date/time objects when relevant to the business without having people use the pointInTime in their domain, effectively tying them to the package.

Aggregate root

Hi,

Is there another way to apply events outside the aggregator?

For example, using a handler class instead applyProcessStarted method.

Best,

Doubt about aggregate root version

When I have multiple connections in my app ab -n100 -c100 -t5 http://localhost/... I noticed that the aggregate version behavior is lost

In the collection, several records are created with the same version

Is this expected or could it be an implementation issue?

I'm using version 1.2 of the package

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/"
        }
    },

Date Time Format's Timezone in PointInTime is not accepted by MySQL

While using this library, I ran into the issue that, when I use the Doctrine Message Repository, persisting events fails because MySQL fails to accept the Date Time format.

After verifying this, I found that the DATE_TIME_FORMAT constant in the PointInTime class uses O as the timezone format (+0000) instead of P (+00:00). When I change this, inserting events in MySQL works, and the MySQL documentation suggests that the P modifier seems to be the accepted format (https://dev.mysql.com/doc/refman/8.0/en/date-and-time-functions.html#function_convert-tz).

Before issuing a PR to change this, I would like to ask whether this format worked in a prior version; or whether this works on Postgre and was not verified on MySQL (for example).

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 ๐Ÿ˜€.

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

Suggest snapshotting change

Hi :)

Event Sauce is great!
The snapshot documentation is a bit inconsistent.

On page example is:

 /**
  * bool type for state is incompatible with the AggregateRootWithSnapshotting interface. 
  * Maybe better you should use mixed type here and in interface?
  */
 protected static function reconstituteFromSnapshotState(AggregateRootId $id, bool $state): AggregateRootWithSnapshotting
 {

 }

Best regards. Andrew

Make AggregateRootTestCase methods more reusable

Right now AggregateRootTestCase contains methods to test interactions with AggregateRoots. The way to use this let a testclass extend AggregateRootTestCase.

Laravel apps have their own root TestCase with lots of methods to test the app. It would be nice if you we could use all methods in AggregateRootTestCase while still using the default Laravel TestCase.

On first though this could be made possible if all methods currently present in AggregateRootTestCase would be move to a trait called TestsAggregateRoot (of something similar). Then we could just use that trait in Laravel tests.

The AggregateRootTestCase provided by EventSauce could also use the same trait, so there would be no breaking changes. I can whip up a PR if you agree with this change.

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.

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.