vanio/doctrine-domain-events
A Doctrine ORM integration that lets your entities record and dispatch domain events. Collect events during persistence, then release them after flush via a configurable dispatcher, keeping your domain model decoupled from infrastructure.
Architecture fit. The framing of this as a "Laravel/PHP package" is the first thing to correct: vanio/doctrine-domain-events is a Doctrine ORM extension, not an Eloquent/Laravel package. It targets applications that use Doctrine\ORM\EntityManager as their persistence layer. Laravel's default ORM is Eloquent, so for a standard Laravel application the architectural fit is poor by default and neutral-to-good only for a Laravel app that has already committed to Doctrine via a bridge such as laravel-doctrine/orm. The capability it enables — publishing domain events from entities and, critically, calling flush() inside a listener triggered by a flush — is genuinely valuable in a DDD-style codebase, because re-entrant flush into Doctrine's UnitOfWork has historically been a sharp edge. That value, however, is value delivered to Doctrine users, and Laravel teams have cheaper native analogues (model observers, DB::afterCommit, queue-backed listeners, or an outbox table).
Integration feasibility. Feasible but non-trivial. The work is not "install this package"; it is "adopt Doctrine ORM in Laravel, then install this package." The real integration surface is the bridge (laravel-doctrine/orm) and its Laravel-version compatibility, plus binding the EntityManager and wiring the package's event dispatch into the container. The package itself is small and MIT-licensed, so source-level auditing is cheap — that is the main mitigating factor. Expect a spike, not a ticket.
Technical risk. Elevated, on four counts:
UnitOfWork state and commit ordering — exactly the area Doctrine has been actively revising (ORM 2.14+ event changes, ORM 3.x). This is the part of Doctrine least protected by BC guarantees.Key questions to put to the maintainer (or answer by reading the source):
wrapInTransaction() / savepoints, and what happens if a listener throws mid-flush?Stack fit. There are two viable profiles and one non-viable one:
Migration path. If you proceed, prefer the narrowest path: introduce Doctrine and this package inside one bounded context rather than app-wide, and keep the domain-event contract in your own codebase (a thin interface + adapter) so the third-party API does not leak into entities. If what you actually want is reliable domain events rather than Doctrine integration, the cheaper alternatives are Eloquent observers with queued listeners, DB::afterCommit, a transactional outbox table with a relay worker, or an eventing framework such as Ecotone or a Laravel event-sourcing package. Compare those on cost before committing to a persistence-layer swap.
Compatibility. Verify against a pinned, locked matrix: PHP version, doctrine/orm major/minor, and the Laravel bridge version for your Laravel major. Check whether Doctrine's own evolving postFlush/transaction APIs have already subsumed part of this package's reason to exist — if upstream Doctrine now tolerates re-entrant flush, the package's core differentiator narrows and its obsolescence risk rises. Also confirm the bridge is maintained for your Laravel release; an unmaintained bridge is a larger risk than this package.
Sequencing.
app/ or a packages/ path) or maintain a fork. Decide this before adoption, not after upstream goes quiet.Maintenance. Treat this as code you own from day one. With 6 stars, 0 dependents, and a single publishing org, there is no realistic expectation of timely upstream fixes for your Doctrine or PHP version. Pin the version in composer.json, do not allow auto-updates, and budget an owner. The upside is that the surface is small enough that a senior engineer can fully audit it in under a day — the maintenance burden is bounded, which is the only thing that makes this defensible.
Support. There is no community to lean on and no meaningful dependent corpus to consult when something breaks. Your internal escalation path is the entire SLA. That is acceptable for a low-blast-radius internal tool and unacceptable for a critical path handling payments or compliance-relevant state.
Scaling. The performance cost is negligible in throughput terms — it is per-flush bookkeeping, not per-row. The scaling concern is transaction duration and lock hold time: re-entrant flush means additional SQL round trips and additional rows/locks inside what is already an open transaction, and heavy listeners compound it. Keep listeners thin, push I/O to async workers or an outbox relay, and instrument flush duration. Recursion depth is the real ceiling, not QPS.
Failure modes. Enumerate these explicitly in your runbook:
UnitOfWork in an inconsistent state, producing confusing downstream errors rather than the original cause.Ramp-up. The API surface is small, so learning the package is a day. Learning Doctrine's UnitOfWork and transaction semantics well enough to debug it is a sprint for an Eloquent-native team, and it is a niche skill in the Laravel hiring market. That is the hidden cost and the reason I would not recommend this as a default dependency: the package is cheap, the surrounding competency is not.
Recommendation. Do not adopt for a standard Laravel/Eloquent application — use native events plus after-commit dispatch or an outbox. Consider adoption only for a Doctrine-based DDD service, and only with an explicit decision to vendor/fork, a pinned dependency matrix, and integration tests covering recursion and rollback written before the pilot goes live.
How can I help you explore Laravel packages today?