Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Delayed Event Bundle Laravel Package

tjovaisas/delayed-event-bundle

Schedule Symfony events to dispatch later instead of immediately. This bundle queues delayed events and processes them when due, useful for reminders, time-based workflows, and background follow-up actions.

View on GitHub
Deep Wiki
Context7

Getting Started

First, a reality check on scope: despite the name, this is a Symfony + Doctrine bundle, not a Laravel package. If you are coming from Laravel, translate it in your head to "dispatchAfterCommit / ShouldDispatchAfterCommit, but driven by Doctrine's postFlush instead of Eloquent model events." There is no Laravel integration, no service provider, and no queue wiring for you — you get a deferred event dispatcher and you wire the rest.

Minimal setup:

  1. composer require tjovaisas/delayed-event-bundle (pin the version — the last tagged release is Feb 2022).
  2. Register the bundle: config/bundles.php → Tjovaisas\DelayedEventBundle\...::class => ['all' => true] on Symfony 4/5+; app/AppKernel.php registerBundles() on legacy Symfony 3.
  3. Read the source before you write code against it. At 1 star and 0 dependents the README is effectively the only documentation, and it is thin. vendor/tjovaisas/delayed-event-bundle is small enough to read end-to-end in ten minutes: find the Doctrine event subscriber (the thing tagged doctrine.event_subscriber listening to postFlush) and the service definition for whatever holds the event queue. That subscriber is the package.

First use case: you persist an Order, and you want to send a confirmation email, push a document to an external search index, or fire a webhook. Dispatching inside prePersist/postPersist gives you no database ID and fires even if the surrounding transaction later rolls back. This bundle lets you enqueue a plain event object while Doctrine is working, then release it in one batch after the flush completes, when IDs are assigned and the unit of work is coherent.

Implementation Patterns

Queue-then-release. Treat it as a two-phase flow: during entity lifecycle callbacks you enqueue a domain event describing what happened; the bundle's postFlush listener drains the queue and hands each event to Symfony's EventDispatcher. Your listeners then run against flushed data.

Keep events small and serializable. Do not put managed entities in the queued event. By dispatch time entities may be detached, and if you later move to Messenger your event has to survive serialization. Put int $orderId, string $status, scalars. Let the listener re-fetch through the repository.

Make listeners thin — hand off to Messenger. The best pattern with this bundle is: postFlush → dispatch domain event → listener does $bus->dispatch(new SendOrderConfirmation($orderId)). You get "events fire at the right moment" from this bundle and "work happens off-request" from Messenger, and each concern stays debuggable independently.

Batch imports are the sweet spot. In an import command that flushes every 100 entities, per-entity listeners would fire 100 times mid-batch. Queueing makes it one drain at the flush boundary, which is exactly what you want for index rebuilds and notification digests.

Use wrapInTransaction for the outer boundary. The common shape is:

$em->wrapInTransaction(function () use ($order, $em) {
    $order->markPaid();
    $em->persist($logEntry);   // events queued here
    $em->flush();              // queue drains on postFlush
});

Test at the kernel level. Boot the kernel in a functional test, dispatch a request/command, then assert on an event-dispatcher spy (Symfony\Component\EventDispatcher\EventDispatcherInterface decorator or a TraceableEventDispatcher) that the event fired after the row exists. A second test should assert it does not fire when the transaction aborts.

Gotchas and Tips

  • postFlush is not afterCommit. This is the big one. postFlush fires when the unit of work completes, which can still be inside an enclosing transaction. If that outer transaction rolls back, your queued events have already been dispatched for data that never persisted. If correctness matters, wrap the flush in an explicit transaction and defer dispatch until after it commits — or make every listener idempotent and defensive (re-fetch by ID, no-op if missing).
  • One drain per flush. Multiple flush() calls in one request mean multiple dispatch rounds. Ordering between your listeners and other postFlush listeners depends on tag priority — set it explicitly rather than assuming.
  • No flush, no events. Read-only requests, or code paths that queue an event but never reach flush(), silently drop everything. Never rely on this mechanism for cleanup or accounting.
  • Long-running processes leak. In a Messenger worker or a long console command the queue can accumulate between messages. Drain explicitly between messages, or reset the collector, or you get cross-message event bleed.
  • Re-entrancy. A listener that persists and flushes triggers another postFlush and another drain. Either forbid flushing inside listeners or guard the collector with a "draining" flag.
  • Maintenance risk is real. One star, zero dependents, last release 2022 — assume Symfony 6.4/7 compatibility is untested and that there will be no fixes. Pin the version in composer.json. The good news: the whole package is roughly one subscriber plus a service definition, so vendoring it into src/ (or writing a 30-line postFlush listener plus a request-scoped event collector) is a viable escape hatch and arguably a better long-term bet.
  • Debugging. Log the queue contents inside the postFlush listener before draining; when an event "doesn't fire," it is almost always (a) no flush happened, (b) an exception before postFlush, or (c) the event went into a different collector instance than the one being drained — check the service scope (shared vs. non-shared) in the bundle's service config.
  • Extension points. Event class design is entirely yours; listener priority is standard Symfony; the dispatcher is injectable, so you can decorate it. The only thing you cannot easily change is when the drain happens — if you need true after-commit semantics, that is the seam to replace.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel