w3c/lifecycle-events-bundle
A Symfony bundle that integrates Doctrine entity lifecycle events with Symfony’s event dispatcher, letting you handle prePersist, postPersist, preUpdate, postUpdate, preRemove, and postRemove hooks in dedicated listeners instead of entity callbacks.
Be aware up front: this is a Symfony bundle backed by Doctrine ORM, not a Laravel package. You cannot drop it into a Laravel app — there is no service provider, no Eloquent integration, and no composer require that will "just work" in an app/ tree. Treat this assessment as evaluating the pattern (decoupled entity lifecycle events) through a Laravel lens, and only adopt the package itself if you're also running a Symfony/Doctrine service.
For a Symfony project the minimal steps are roughly:
composer require w3c/lifecycle-events-bundleW3C\LifecycleEventsBundle\W3CLifecycleEventsBundle in config/bundles.phpconfig/packages/w3c_lifecycle_events.yaml (which entity classes are tracked, which create/update/delete events to dispatch — check the README and DependencyInjection/Configuration.php for the actual keys, since defaults are not obvious)Where to look first in the repo: the README (short — read it end to end), then EventListener/ for the Doctrine lifecycle hooks, then Event/ for the actual event objects you'll consume. The DependencyInjection/Configuration.php is where the real config surface lives.
First use case: the canonical one is dispatch-on-postPersist/postUpdate/postRemove so you can send a notification, write an audit record, or push to a search index without coupling that logic to a controller or to the entity itself.
If you know Laravel's model events (creating, created, updating, updated, deleting, deleted), you already understand what this bundle is trying to do. The difference is where the events live and how they're typed:
| Laravel | This bundle |
|---|---|
Model::created(fn ($model) => ...) |
EventDispatcher listener on a typed CreateEvent |
php artisan make:observer UserObserver |
A tagged Symfony EventSubscriberInterface |
| Model events fire on Eloquent save/delete | Doctrine postPersist/postUpdate/postRemove |
Events are string-keyed (eloquent.created: App\Models\User) |
Events are typed objects with getEntity() |
The pattern is the same: keep persistence-adjacent side effects out of the write path and out of the entity. When you find yourself wanting to email on user creation, index on product update, or log on soft-delete, that's the shape this serves.
The idiomatic use is one subscriber per concern, each listening to the entity lifecycle events it cares about:
class NotifySalesOnOrderUpdate implements EventSubscriberInterface
{
public function __construct(private Mailer $mailer) {}
public static function getSubscribedEvents(): array
{
return [
OrderUpdatedEvent::class => 'onUpdate',
];
}
public function onUpdate(OrderUpdatedEvent $event): void
{
$order = $event->getEntity(); // per README the entity hangs off the event
// decide, dispatch, mail, etc.
}
}
This is the Laravel equivalent of a dedicated observer, but it's global — every configured entity type funnels through the same dispatcher, so your subscriber must be defensive about types unless the bundle filters per-class.
Because the events are fired for every tracked entity, the highest-value integration is a generic "on any create/update/delete" listener that writes an immutable audit row or busts a cache key. Do it once, keyed by entity class + id, and you never forget to invalidate a cache on a write again.
Since the events fire inside the Doctrine flush(), treat them as "inside the transaction" hooks. A common split is:
If the bundle only gives you in-flush events, write an outbox row in the subscriber and hand that to a separate worker/handler. In Laravel you'd do exactly this with ShouldDispatchAfterCommit (Laravel 8.38+) or by queuing the notification with dispatchAfterCommit().
If you're evaluating this for inspiration in a Laravel app, the equivalent stack is:
app/Observers/)ShouldBroadcast / ShouldQueue on the events so listeners don't slow the requestDB::afterCommit() (or event listeners implementing ShouldDispatchAfterCommit) to defer risky side effects until data is durableYou get 95% of this bundle's value with zero extra dependencies in a Laravel codebase via observers. That's the practical takeaway.
Don't waste a day trying to make this work in an Illuminate app. There is no Laravel bridge, no facade, no service provider. Use php artisan make:observer and app/Observers; that is the Laravel-native answer to this package.
Events fire inside EntityManager::flush(). Consequences:
flush() inside a listener. Doing so during a Doctrine postPersist/postUpdate is a classic footgun and can loop or trigger the UnitOfWork into inconsistent state. Persist related entities via the same flush() or defer.postRemove fires before the actual DELETE statement in some flows; if your listener touches the entity you can get weird state. Prefer capturing an id + class at event time.If a listener mutates a tracked entity, you can re-trigger the update event and create an infinite loop or a "unit of work is not clean" error. Guard by comparing a hash of relevant fields (or a dirty() snapshot) before firing side effects, the way you'd guard a Laravel model observer.
NOASSERTION. Before using in commercial code, read the actual LICENSE file. GitHub not being able to classify it is a yellow flag, not necessarily fatal, but verify.create/update event set is likely limited to persisted state changes, not "was this field actually changed?" — if you need dirty-field granularity, compare values in the listener ($event->getEntity()->field against a snapshot) or move to Doctrine's onFlush with UnitOfWork::getEntityChangeSet().composer.json constraints and W3CLifecycleEventsBundle::getContainerExtension() style boilerplate tend to lag; check the Symfony range in composer.json before installing.php bin/console debug:event-dispatcher — confirm the bundle's Doctrine listener is actually registered.var_dump(get_class($event), $event->getEntity()) at the very top — if you never see the subscriber called, it's an opt-in config issue, not a listener issue.EventListener/ to understand the filter — some bundles track everything, some require an interface.$em->flush(); persist() alone doesn't fire postPersist.UserCreatedEvent is far nicer to type in listeners than a generic LifecycleEvent with a string $action.kernel.event_subscriber autoconfiguration, so standard priorities (['onUpdate', 10]) work; use them when ordering matters (audit before side effects, not after).BaseModel booted() hook that wires an AuditableObserver and a CacheInvalidationObserver across every model — it's ~30 lines and gives you the same fan-out the bundle advertises.The pattern is sound and worth adopting. The package is a Symfony/Doctrine-specific implementation with a small, unaudited footprint and no Laravel path. If you're in Laravel: replicate it with observers, DB::afterCommit(), and queued events, and skip the dependency. If you're in Symfony and need entity lifecycle events without Doctrine's onFlush boilerplate, it's a reasonable small helper — just pin it, read the source, and keep all external side effects out of the flush.
How can I help you explore Laravel packages today?