dessimoney/event-source
Event sourcing for Laravel and PHP applications: persist domain events, rebuild aggregates from history, and maintain projections. The package provides an event store, aggregate support, and Laravel-friendly tooling for building event-driven applications with event-sourced persistence.
composer require dessimoney/event-source. The core is framework-agnostic; the Laravel bridge is a thin service provider/facade on top of it.src/Laravel/ (or src/Bridge/Laravel/) namespace to config/app.php, then publish config with php artisan vendor:publish --provider="Dessimoney\EventSource\Laravel\EventSourceServiceProvider" (check the README for the exact tag).README.md for the minimal example, src/ for the EventSource/Event core, and the Laravel bridge for the facade, route macro, and config keys. The API surface is small, so ten minutes in src/ tells you everything the docs don't.composer.json for supported Laravel/PHP versions. The last release is recent, but with 3 stars and 0 dependents, pin an exact version.curl -N http://app.test/events, and confirm you see event:/data: frames arrive incrementally rather than as one buffered blob.// routes/web.php — confirm facade/method names against the README first
use Dessimoney\EventSource\Laravel\Facades\EventSource;
Route::get('/events', function () {
return EventSource::response(function ($stream) {
$stream->event('hello', ['user' => auth()->id()]);
while (!connection_aborted()) {
$stream->comment('keep-alive');
sleep(15);
}
});
})->withoutMiddleware([\Barryvdh\Debugbar\Middleware\InjectDebugbar::class]);
Open it from the browser with new EventSource('/events') and add a listener for hello.
progress:{jobId} to the cache or a Redis hash, and let the SSE endpoint poll every 250–500 ms, emit a frame, and exit when status is done or failed. This is the lowest-friction fit for Laravel and avoids a pub/sub dependency.Redis::blpop("sse:{$channel}", 20) loop is usually enough) or a database outbox table polled by id > $lastSeenId.next(): ?Event method. The route/controller just loops and writes. This makes the interesting logic unit-testable without fighting streamed responses.EventSource cannot send custom headers, so use session cookies (same-origin) or URL::temporarySignedRoute() for cross-origin/third-party consumers. Exclude stream routes from aggressive global throttles, or reconnects will 429; use a dedicated limiter instead.id: so clients send Last-Event-ID on reconnect (implement a small replay buffer if you care about gap-free delivery), retry: to control client backoff, and event: so the front end can use addEventListener('progress', ...) instead of parsing everything in onmessage.StreamedResponse/PSR-7 stream; the Laravel facade is not doing anything magic. If you later drop the package, response()->stream() with the same headers is a viable fallback.fastcgi_buffering, proxy_buffering) and Apache will hold frames until the response ends. Send X-Accel-Buffering: no plus Content-Type: text/event-stream, Cache-Control: no-cache. Also disable PHP output_buffering and zlib.output_compression for the route, and turn off Debugbar/Telescope middleware — they buffer output.session_write_close() before entering the loop, otherwise every other request from that user blocks until the stream ends.ignore_user_abort(true) and check connection_aborted(); PHP often only notices a closed socket on the next write. A : keep-alive comment every 15–30 s doubles as proxy idle-timeout protection.?>, or an earlier echo/dd() corrupts the stream. Nothing may be printed before the streamed response is returned.DB::reconnect() (or equivalent) in jobs that run for hours. Also set set_time_limit(0) for the stream route and watch request_terminate_timeout in PHP-FPM.assertSee frequently sees nothing. Test the feed/service class directly, and for HTTP use $response->streamedContent() with an iteration cap so the test terminates.php artisan config:cache) means published changes need a recache; check whether the package reads env vars directly rather than through config(). Verify default heartbeat/retry values match your proxy timeouts.response()->stream() or Mercure without touching call sites. Extension seams are typically the service-provider bindings, the event formatter/serializer, and middleware.How can I help you explore Laravel packages today?