PHPackages                             rela589n/doctrine-event-sourcing - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. rela589n/doctrine-event-sourcing

AbandonedArchivedLibrary[Database &amp; ORM](/categories/database)

rela589n/doctrine-event-sourcing
================================

Package for simple event-sourcing implementation utilizing Doctrine ORM

1.0.0(4y ago)211MITPHPPHP ^8.0

Since Jun 1Pushed 4y ago2 watchersCompare

[ Source](https://github.com/rela589n/doctrine-event-sourcing)[ Packagist](https://packagist.org/packages/rela589n/doctrine-event-sourcing)[ RSS](/packages/rela589n-doctrine-event-sourcing/feed)WikiDiscussions master Synced 2d ago

READMEChangelogDependencies (7)Versions (3)Used By (0)

Doctrine Event Sourcing
=======================

[](#doctrine-event-sourcing)

[![codecov](https://camo.githubusercontent.com/c4becf95e8f14be2d81a71d1ed61a3f56b40cb1516197d3e25bf2e648725adad/68747470733a2f2f636f6465636f762e696f2f67682f72656c613538396e2f646f637472696e652d6576656e742d736f757263696e672f6272616e63682f6d61737465722f67726170682f62616467652e7376673f746f6b656e3d4b5959583634394e5057)](https://codecov.io/gh/rela589n/doctrine-event-sourcing)

This package is intended to simplify implementation of Event Sourcing pattern in applications using Doctrine ORM.

Installation
------------

[](#installation)

You can install the package via composer:

```
composer require rela589n/doctrine-event-sourcing
```

Configure `vendor/rela589n/doctrine-event-sourcing/config/mappings/` as doctrine mappings directory with the higher priority than your mappings.

Getting Started
---------------

[](#getting-started)

### Introduce the event sourcing into domain model

[](#introduce-the-event-sourcing-into-domain-model)

Implement `\Rela589n\DoctrineEventSourcing\Entity\AggregateRoot` with your entity.

```
use Ramsey\Uuid\UuidInterface as Uuid;
use Rela589n\DoctrineEventSourcing\Entity\AggregateRoot;

class User implements AggregateRoot
{
    private Uuid $uuid;

    public static function getPrimaryName(): string
    {
        return 'uuid';
    }

    public function getPrimary(): Uuid
    {
        return $this->uuid;
    }
}
```

Create Base Event class for your entity.

```
use Rela589n\DoctrineEventSourcing\Event\AggregateChanged;

abstract class UserEvent extends AggregateChanged
{
    public function __construct(User $user)
    {
        parent::__construct(entity: $user);
    }
}
```

Abstract event class for each entity is useful because we can use it's type-hint later when applying events.

> If you don't like inheritance in general or can't inherit from `AggregateChanged` abstract class, you can simply implement interface `Contract\AggregateChanged` and use trait `Concern\AggregateChanged` as implementation. But keep in mind that in such case you would have to define your own configuration mapping similar to `Rela589n.DoctrineEventSourcing.Event.AggregateChanged.dcm.xml`. It is because `mapped-superclass` doesn't work with indirect interfaces.

Now, lets create event, which represents something happening in our application.

```
class UserRegistered extends UserEvent
{
    private Login $login;
    private Password $password;
    private UserName $userName;

    private function __construct(User $user, Login $login, Password $password, UserName $name)
    {
        parent::__construct($user);
        $this->login = $login;
        $this->password = $password;
        $this->userName = $name;
    }

    public static function withCredentials(User $user, Login $login, Password $password, UserName $name): self
    {
        return new self($user, $login, $password, $name);
    }

   // bunch of getters here

    public function NAME(): string
    {
        return 'user_registered';
    }
}
```

You may wonder what are `Login`, `Password`, `UserName` are all about. It all are value objects. Usually you map them to your entity as [embeddables](https://www.doctrine-project.org/projects/doctrine-orm/en/2.8/tutorials/embeddables.html).

> Although Events are by nature entities, you don't need to worry about relations or identifiers. It is all done for you.

Once you have an events, you can implement your business logic with Event-Driven principle.

```
use Rela589n\DoctrineEventSourcing\Event\Exceptions\UnexpectedAggregateChangeEvent;

class User implements AggregateRoot
{
    private Uuid $uuid;

    private Login $login;

    private Password $password;

    private UserName $name;

    private DateTimeInterface $createdAt;

    private DateTimeInterface $updatedAt;

    /** @var Collection */
    private Collection $recordedEvents;

    /** @var UserEvent[] */
    private array $newlyRecordedEvents = [];

    private function __construct(Uuid $uuid)
    {
        $this->uuid = $uuid;
        $this->recordedEvents = new ArrayCollection();
    }

    public static function register(Uuid $uuid, Login $login, Password $password, UserName $name): self
    {
        $user = new self($uuid);

        $user->recordThat(
            UserRegistered::withCredentials($user, $login, $password, $name)
        );

        return $user;
    }

    private function recordThat(UserEvent $event): void
    {
        switch (true) {
            case $event instanceof UserRegistered:
                $this->createdAt = $event->getTimestamp();
                $this->login = $event->getLogin();
                $this->password = $event->getPassword();
                $this->name = $event->getUserName();
                break;
            default:
                throw new UnexpectedAggregateChangeEvent($event);
        }

        $this->updatedAt = $event->getTimestamp();
        $this->newlyRecordedEvents[] = $event;
        $this->recordedEvents->add($event);
    }

    public static function getPrimaryName(): string
    {
        return 'uuid';
    }

    public function getPrimary(): Uuid
    {
        return $this->uuid;
    }

    public function releaseEvents(): array
    {
        $events = $this->newlyRecordedEvents;
        $this->newlyRecordedEvents = [];
        return $events;
    }
}
```

### When your domain entity is ready and events are driving the domain, it's time to think about persistence.

[](#when-your-domain-entity-is-ready-and-events-are-driving-the-domain-its-time-to-think-about-persistence)

First off, let's define mapping for Abstract `UserEvent` class. We use `xml` just for example:

```

```

> Main part here is that `` part, because it tells Doctrine how to hydrate data into correct event objects.

As well wee need annotate all final event classes as entities.

```

```

As you can see, final event classes don't need any annotations except an `@Entity`. This is because all payload fields are mapped into jsonb for you.

> For final event classes describing with xml may be tedious, so feel free to use annotations.

The last one step we should do is map `recordedEvents` and value objects for entity. Each entity using event driven approach should do this.

```

```

As well here we `embed` value objects such as `Login`, `Password`, `UserName`. What mapping for value object looks like:

```

```

### Check it out

[](#check-it-out)

```
$email = 'johndoe'.Str::random().'@example.com';

$user = User::register(
    Uuid::uuid4(),
    Login::fromString($email),
    Password::fromRaw('hello world'),
    UserName::fromString('John Doe'),
);

$this->entityManager->persist($user);
$this->entityManager->flush();
```

Now, if we look into users table, everything is pretty obvious:

uuidnameemailpasswordcreated\_atupdated\_at772bacea-9074...John Doe$2y$04$SKMHNCZ...2021-03-27 20:12:482021-03-27 20:12:48What's more interesting is `user_events` table:

idnameuser\_uuidpayloadtimestamp20user\_registered772bacea-9074...{"login": {"email": ""}, "password": {"password": "$2y$04$SKMHNCZ..."}, "userName": {"name": "John Doe"}}2021-03-27 20:12:48### Let's get a bit crazy

[](#lets-get-a-bit-crazy)

Imagine messenger application, where we have Users, Chats, Messages.

```
$message = Message::write(
    Uuid::uuid4(),
    $user,
    $chat,
    MessageContent::fromString('Some message')
);
```

What event would it trigger? Something like `MessageWritten` event:

```
class MessageWritten extends MessageEvent
{
    private function __construct(
        Message $message,
        private User $user,
        private Chat $chat,
        private MessageContent $content,
    ) {
        parent::__construct($message);
    }

    public static function withData(Message $message, User $user, Chat $chat, MessageContent $content): self
    {
        return new self($message, $user, $chat, $content);
    }

    // bunch of getters

    public function NAME(): string
    {
        return 'message_written';
    }
}
```

You may wonder how it would get saved into database?

In messages table everything looks pretty boring:

uuidstatuscontentuser\_uuidchat\_uuidcreated\_atupdated\_at3950eef0-ac89...NEWSome message69ab80be-b05e...b84a6b78-a173...2021-03-27 20:31:092021-03-27 20:31:09What regards table `messsage_events`, it is full of magic:

idnamemessage\_uuidpayloadtimestamp14message\_written3950eef0-ac89...{"chat": "b84a6b78-a173...", "user": "69ab80be-b05e...", "content": {"content": "Some message"}}2021-03-27 20:31:09Payload fields `chat` and `user` were populated with primary keys. At the time events are loaded back, these entities won't be loaded right away, but rather proxied by doctrine. If we really would like to deal with these related objects, doctrine will gracefully load them from the database to be used.

Customization
-------------

[](#customization)

### Customizing field names

[](#customizing-field-names)

By default, keys in json payload are fields names. You are free to customize this:

```
#[SerializeAs(name: 'chat_name')]
private ChatName $chatName;
```

### Customizing DBAL type

[](#customizing-dbal-type)

```
#[SerializeAs(type: Types::BOOLEAN)]
private int $arbitraryValue;

#[SerializeAs(type: 'carbondatetimetz', name: 'publish_at')]
private Carbon $someDate;
```

### Customizing Value Objects

[](#customizing-value-objects)

By default, for VO persistence, owning entity embedded metadata is analysed and if Value Object has been mapped on entity, it's mapping is used for persistence in events table.

To override this with your own logic, you may implement `Castable` interface with your value object. This interface declares one method `castUsing`, which should return `CastsAttributes` object.

```
use Rela589n\DoctrineEventSourcing\Serializer\Separate\Castable\Contract\Castable;
final class MessageContent implements Castable
{
    // main body

    public static function castUsing(array $arguments): MessageContentCast
    {
        return new MessageContentCast();
    }
}
```

Logic for casting value object:

```
use Rela589n\DoctrineEventSourcing\Serializer\Separate\Castable\Contract\CastsAttributes;

final class MessageContentCast implements CastsAttributesEloquent, CastsAttributes
{
    public function get($model, $key, $value, $attributes): MessageContent
    {
        Assert::isInstanceOfAny($model, [EloquentMessage::class, DoctrineMessage::class]);

        return MessageContent::fromString($attributes['content']);
    }

    public function set($model, $key, $value, $attributes): array
    {
        Assert::isInstanceOfAny($model, [EloquentMessage::class, DoctrineMessage::class]);
        Assert::isInstanceOf($value, MessageContent::class);

        return [
            'content' => (string)$value,
        ];
    }
}
```

As you may have noticed, these interfaces are completely compatible with `Eloquent` ones so that you may reuse Cast for both model and entity.

> If none of `Typed`, `Entity`, `Embedded`, `Castable` strategies matched value, `Noop` strategy will be used. This means value will be given as it is when serializing and received as it is when deserializing.

###  Health Score

26

—

LowBetter than 43% of packages

Maintenance20

Infrequent updates — may be unmaintained

Popularity8

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity58

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

1807d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/41589422?v=4)[Yevhen Sidelnyk](/maintainers/rela589n)[@rela589n](https://github.com/rela589n)

---

Top Contributors

[![rela589n](https://avatars.githubusercontent.com/u/41589422?v=4)](https://github.com/rela589n "rela589n (58 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/rela589n-doctrine-event-sourcing/health.svg)

```
[![Health](https://phpackages.com/badges/rela589n-doctrine-event-sourcing/health.svg)](https://phpackages.com/packages/rela589n-doctrine-event-sourcing)
```

###  Alternatives

[scienta/doctrine-json-functions

A set of extensions to Doctrine that add support for json query functions.

58523.9M36](/packages/scienta-doctrine-json-functions)[damienharper/auditor-bundle

Integrate auditor library in your Symfony projects.

4542.8M](/packages/damienharper-auditor-bundle)[sonata-project/entity-audit-bundle

Audit for Doctrine Entities

644989.8k1](/packages/sonata-project-entity-audit-bundle)[pixelfederation/doctrine-resettable-em-bundle

Symfony bundle for decorating default entity managers using a resettable decorator.

20113.5k](/packages/pixelfederation-doctrine-resettable-em-bundle)[rcsofttech/audit-trail-bundle

Enterprise-grade, high-performance Symfony audit trail bundle. Automatically track Doctrine entity changes with split-phase architecture, multiple transports (HTTP, Queue, Doctrine), and sensitive data masking.

1022.4k](/packages/rcsofttech-audit-trail-bundle)[ahmed-bhs/doctrine-doctor

Runtime analysis tool for Doctrine ORM integrated into Symfony Web Profiler. Unlike static linters, it analyzes actual query execution at runtime to detect performance bottlenecks, security vulnerabilities, and best practice violations during development with real execution context and data.

813.1k](/packages/ahmed-bhs-doctrine-doctor)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
