fervo/deferred-event-bundle
DeferredEventBundle for Symfony queues domain events during a request or transaction and dispatches them at a controlled later point, so listeners and side effects run after persistence completes and stay consistent with committed data.
Read this first: this is a Symfony bundle (Symfony 2.x/3.x era, PHP 5.5–7.0), not a Laravel package. There is no service provider, no facade, no config/deferred-event.php. If you're evaluating it from a Laravel codebase, the honest read is: the idea is worth stealing, the code is not portable. In Laravel the equivalent is dispatch(new Job)->afterResponse() or the defer() helper — you'd only reach for this bundle if you inherited a Symfony app that already uses it.
For the Symfony case:
composer require fervo/deferred-event-bundle (pin ~1.0; last tagged release was 2016-01-13, so expect to resolve against old Symfony constraints).new Fervo\DeferredEventBundle\FervoDeferredEventBundle() in AppKernel::registerBundles().config.yml and list the events you want deferred.Where to look in the repo, in this order:
DependencyInjection/Configuration.php — the whole config surface is small; this tells you exactly which keys exist.event_dispatcher service) — this is the mechanism. Understanding that it decorates the global dispatcher explains 90% of the behavior and all of the surprises.EventListener/ — the kernel.terminate / FastCGI hook that actually flushes the queued events to another process.First use case: you have a listener that fires on user.registered and does something slow and non-critical — send a welcome email through a third-party API, push a lead into a CRM, POST to an analytics endpoint. The user shouldn't wait 800ms for that. Mark that event for deferral, let the response go out, and let the deferred pass do the work.
Retrofit by event name, not by listener. The bundle lets you defer events, which means every listener on that event moves out of the request cycle. This is great for adoption (no code change in your controllers) and terrible for precision. Before enabling, grep for every ->addListener('user.registered', ...) and confirm all of them are safe to run after the response is sent.
Keep request-critical listeners inline. Don't defer an event whose handlers mutate state the response depends on — e.g. a listener that writes an audit row you immediately read back, or that invalidates a cache the redirect page uses. Deferral is for side effects, not for computed state.
Design payloads as IDs, not objects. The event is serialized to cross a process boundary. Any listener that receives the event and calls $event->getUser()->getEmail() needs the user to survive serialization — which for Doctrine entities means you're shipping a detached, possibly lazy-proxy-laden object into a fresh process. Strongly prefer events whose getters return scalars, or re-fetch inside the deferred listener:
class UserRegisteredEvent
{
public function __construct(private int $userId) {}
public function getUserId(): int { return $this->userId; }
}
// deferred listener — fetch fresh, in its own process
$user = $this->users->find($event->getUserId());
if (!$user) { return; } // deleted between request and deferral
$this->mailer->sendWelcome($user);
Watch the transaction boundary. Deferral that runs at kernel.terminate can execute before your Doctrine flush/commit has landed on disk — especially with a wrapping sub-request that opens its own connection. The classic bug: the deferred listener reads the row that the originating request hasn't committed, gets null, and silently no-ops. If your listener depends on freshly written rows, this ordering matters more than anything else in the setup.
Fall back to cron on hosts without FastCGI. The whole premise is a FastCGI sub-request after the response is flushed. On Apache mod_php, CLI, or a container without PHP-FPM, the deferral degrades — usually into "runs later in the same process," which still helps latency but gives you none of the isolation. Have a scheduled command or a kernel.terminate sweep as a safety net for anything left in the queue store.
Treat deferred listeners as idempotent jobs. There is no retry/backoff/dead-letter machinery here — it's a deferral mechanism, not a queue. Write each handler so running it twice is harmless, and log failures loudly, because nobody is watching the second process's return value.
SplFileInfo, DI containers, entities with lazy proxies — all blow up or silently corrupt at defer time. Reproduce with a plain serialize()/unserialize() on the event in isolation before blaming the bundle.try/catch in the heavy listeners that logs with a correlation id you also log at dispatch time. Otherwise "the email never sent" has no trail at all.Configuration.php. YAML anchors and %parameters% in the event list are easy to get wrong, and a typo'd event name fails open (runs inline) rather than erroring, so your "it's deferred" assumption may be false.EventDispatcherInterface::dispatch($name, Event $event) ordering). It will not run cleanly on Symfony 5+/PHP 8 without patching the dispatch() call sites and return types. Budget for vendor fork or, more realistically, port the pattern: kernel.terminate + fastcgi_finish_request() is ~40 lines you can own outright.How can I help you explore Laravel packages today?