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

Symfony2 Stream Response Laravel Package

irongit/symfony2-stream-response

A Symfony2 bundle that provides streamed HTTP responses, letting you push large or generated content to the client incrementally instead of buffering it all in memory. Useful for big file downloads, exports, and long-running output where streaming improves performance and reduces memory use.

View on GitHub
Deep Wiki
Context7

Getting Started

Step 0 — the reality check that saves you a sprint. irongit/symfony2-stream-response is a Symfony 2 bundle, not a Composer library you can require into a Laravel app. It ships a Response subclass plus bundle/DI scaffolding registered via AppKernel::registerBundles() — a mechanism Laravel does not have. Laravel pulls Symfony in only at the component level (symfony/http-foundation via illuminate/http), and the version there is ≥4.x, whose HttpFoundation API diverges from the Symfony2-era surface this package targets. Installing it would produce an API-incompatible duplicate of a class you already have.

Step 1 — look here first, in this order:

  1. app/Http/Controllers/ — find the endpoint that's OOMing or timing out (exports, reports, media).
  2. routes/web.php / routes/api.php — confirm the route, its middleware, and whether it's behind auth/rate limits.
  3. Illuminate\Routing\ResponseFactory — stream(), streamDownload(), file(), download(). This is your entire "package."
  4. config/filesystems.php — if the bytes already live on S3/local disk, you may not need PHP streaming at all.

Step 2 — first use case: replace a buffered export. The canonical day-one task is a large CSV/report export that currently does response()->json($hugeArray) or builds a string in memory. The native one-liner:

// app/Http/Controllers/ExportController.php
public function export(): StreamedResponse
{
    return response()->streamDownload(function () {
        $out = fopen('php://output', 'w');
        fputcsv($out, ['id', 'email', 'created_at']);

        App\Models\User::query()->lazyById(1000)->each(function ($user) use ($out) {
            fputcsv($out, [$user->id, $user->email, $user->created_at]);
        });

        fclose($out);
    }, 'users-' . now()->format('Ymd') . '.csv', [
        'Content-Type' => 'text/csv',
    ]);
}

That is functionally what the package provides, minus the abandonment risk. If you want the package's genuinely reusable insight — closing the session and clearing output buffers before the first byte goes out — read its Response class source, port those ~10 lines into app/Support/StreamedDownloadResponse.php, and move on. Do not add a composer.json entry.

Where to stop: if your use case is "serve a file from disk," you're done before you start — response()->file($path) (Symfony's BinaryFileResponse) already gives you Range, ETag, and Last-Modified handling that this package almost certainly lacks.

Implementation Patterns

Translation table — package concept → Laravel primitive:

Package behaviour Laravel equivalent
StreamedResponse subclass with a callback response()->stream($cb) / response()->streamDownload($cb, $name)
Chunked file send loop response()->file($path) (BinaryFileResponse, range-capable)
Forced download with Content-Disposition response()->download($path, $name, $headers)
Custom Content-Type/headers on a stream Second/third args to streamDownload(), or ->headers->set() on a StreamedResponse
Manual ob_flush()/flush() chunking StreamedResponse + ob_end_clean(); Laravel handles the send loop
Session close before streaming session()->save() (or ->close() on old Laravel) at the top of the callback

Pattern 1 — generated content, always lazy/cursor, never get(). The stream callback must be pull-based. Iterating an eager-loaded collection defeats the purpose: memory is consumed before the first chunk.

User::query()->select('id','email')->lazyById(500)->each(fn ($u) => fputcsv($out, [(array) $u]));

Pattern 2 — disk files: pre-check, then response()->file().

abort_unless(Storage::disk('exports')->exists($path), 404);
return response()->file(Storage::disk('exports')->path($path));

Prefer file() over a hand-rolled fread loop — you get conditional requests (If-None-Match, If-Modified-Since), 206 Partial Content, and Accept-Ranges for free, which this package does not reliably give you.

Pattern 3 — raw StreamedResponse when you need byte-level control (SSE, custom flush intervals, non-standard headers). Build it manually rather than wrapping anything:

return new StreamedResponse(function () use ($job) {
    while (ob_get_level() > 0) { ob_end_flush(); }
    foreach ($job->chunks() as $chunk) {
        echo $chunk;
        flush();
    }
}, 200, ['Content-Type' => 'application/x-ndjson', 'X-Accel-Buffering' => 'no']);

Pattern 4 — middleware for cross-cutting download concerns. Auth, per-user concurrency caps, and audit logging belong in middleware, not in the response construction. This is also where you bound the blast radius of a slow client pinning a PHP-FPM worker:

// app/Http/Middleware/LimitConcurrentStreams.php
RateLimiter::for('downloads', fn (Request $r) => Limit::perMinute(10)->by($r->user()->id));

Pattern 5 — offload before you stream. If the file is on disk behind Nginx, or in S3/GCS, PHP should never touch the bytes:

// Nginx internal redirect — zero PHP bytes, zero worker time
return response('', 200, ['X-Accel-Redirect' => "/protected/{$path}"]);
// or
return redirect(Storage::disk('s3')->temporaryUrl($path, now()->addMinutes(5)));

Pattern 6 — integrate with the existing pipeline, don't bypass it. A StreamedResponse is still a Symfony\Component\HttpFoundation\Response; it flows through Illuminate\Foundation\Http\Kernel middleware normally, so auth, can:, throttle, and LogRequests all still apply. The one thing you should not do is attach global middleware that buffers or decorates the body (compression, dd(), debug toolbars).

Gotchas and Tips

  • Silent truncation is the signature failure. A client disconnects mid-stream, the write fails, and a 200 is already committed — the client receives a short file with a success status. Log completion inside the callback (Log::info('stream.complete', ...)) so you can distinguish "aborted" from "delivered."
  • Output-buffer contamination. A BOM in a PHP file, stray whitespace after ?>, a debug middleware calling ob_start(), or a dd() upstream corrupts the byte stream — or worse, silently buffers the whole payload, turning "streaming" into an OOM. Start the callback with while (ob_get_level() > 0) { ob_end_clean(); } and keep php.ini output_buffering = Off for streaming routes.
  • Close the session first. Without session()->save() at the top of the callback, every concurrent stream holds the session lock and your app serializes. This is the one behaviour from the package genuinely worth porting.
  • No Content-Length = chunked transfer = proxy buffering. Nginx buffers the full response by default, converting your streaming design into a memory spike at the edge with no application-side symptom. Set proxy_buffering off; and send X-Accel-Buffering: no for the route.
  • Raise fastcgi_read_timeout deliberately, not by accident. A default 60s cut mid-export produces a truncated file that looks like a code bug.
  • ignore_user_abort defaults off. After a disconnect, your loop keeps writing to a dead socket until PHP notices. Check connection_aborted() inside long loops if you care.
  • Debug with memory_get_peak_usage(true), not wall-clock. A "streaming" endpoint that pegs peak memory at the size of the export is silently buffering. Assert in a test that peak memory stays flat as row count doubles.
  • Do not vendor the bundle. If you're tempted to copy it, you're copying PHP 5.3-era idioms (short-array-less syntax, @ suppression, likely PSR-0 layout that Composer 2 won't autoload without a classmap), plus implicit-nullable and dynamic-property breakage on PHP 8.x. Port the ideas (buffer clear, session close, chunk flush), not the files, and license-check the irongit fork's provenance against toretto460 upstream first.
  • Extension point is your own class, not theirs. Put the ported logic in app/Support/ behind an interface, register it in a service provider, and test it. That's the maintainable seam — it lets you swap in a queue-backed export or an S3 pre-signed URL later without touching controllers.
  • Add the vetting gate. 1 star, 0 dependents, Symfony 2 EOL since 2018. Any dependency with no dependents and no maintenance activity should be filtered before it reaches sprint planning, not after someone has wired it into a controller.
  • Escape hatch for real gaps. If you genuinely need bespoke range/resume semantics beyond what BinaryFileResponse does, raise it as an engineering task and write it in app/Support/ — it's 50–100 lines you own, with tests, and no CVE feed you're missing.
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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle