nzo/file-downloader-bundle
nzo/file-downloader-bundle is a Symfony bundle that simplifies serving file downloads. Stream files as HTTP responses, set filenames and content types, and return attachment or inline downloads without boilerplate.
Install:
composer require nzo/file-downloader-bundle
Important framing first: this is a Symfony bundle, not a Laravel package. There is no NzoFileDownloaderServiceProvider, no config publish tag, and no facade. A Laravel developer has three realistic routes:
NzoFileDownloaderBundle in config/bundles.php, configure under nzo_file_downloader in config/packages/.FileDownloader class in src/. If its constructor has no required bundle-specific arguments, new FileDownloader() (or a thin app()->bind() in a service provider) works.Storage::download() / response()->file() — which cover 90% of the same ground with zero bundle friction.Version note (v4.3.0): the release notes are one line — "Code refactoring + Code enhancements" — with no documented features, deprecations, or breaking-change list. The package is now on the 4.x line, which means several majors have shipped since the 1.x-era source this assessment was written against. Treat every class name, namespace, and method signature below as illustrative: the refactor may have moved or renamed things without a changelog entry. Step one is reading src/, not reading this document.
Where to look first: open src/ and find the downloader service class and any *Trait (e.g. a controller trait exposing download() / show()). That class is what you actually depend on — the DependencyInjection/ folder is just registration glue you can ignore in Laravel.
First use case: stream a file stored outside the web root to a browser without exposing the path or allowing traversal:
// routes/web.php
Route::get('/files/{id}', function ($id) {
$path = storage_path("app/private/{$id}.pdf");
abort_unless(is_file($path), 404);
// Symfony BinaryFileResponse returned from a Laravel route is valid —
// Laravel's Router::prepareResponse() passes Symfony responses through untouched.
return (new FileDownloader())->download($path);
});
Verify the exact method names (download / read / show) and constructor arguments against the 4.x source — the API surface is small but not documented in a Laravel-friendly way, and after a refactor release the odds of drift are higher than usual.
Adapter service provider (cleanest Laravel integration):
// app/Providers/FileDownloaderServiceProvider.php
public function register(): void
{
$this->app->singleton(\Nzo\FileDownloaderBundle\FileDownloader::class, fn () => new \Nzo\FileDownloaderBundle\FileDownloader());
}
Then inject the class wherever needed. This isolates the Symfony dependency to one file, so swapping to Storage::download() later is a one-line change — and it's the only file you touch again if a future refactor changes the constructor.
Download vs inline read: the bundle's value is that it gives you both a "force download" and an "open in browser" mode. Use the inline mode for previewable types (PDF, images, plain text) and force-download for everything else:
if (in_array($mime, ['application/pdf', 'image/png', 'image/jpeg'])) {
return app(FileDownloader::class)->read($path); // Content-Disposition: inline
}
return app(FileDownloader::class)->download($path); // Content-Disposition: attachment
Path safety wrapper: the bundle handles response headers, but it does not know your storage layout. Always resolve user input against a fixed base directory and reject traversal before handing it off:
$base = realpath(storage_path('app/private'));
$target = realpath("{$base}/{$request->path}");
abort_unless($target && str_starts_with($target, $base), 403);
Authorization goes in a policy, not the downloader. The bundle streams whatever path you give it. Gate access in a FormRequest or middleware before invoking it — otherwise you have an IDOR with a nice Content-Disposition header.
Integration tip (hybrid Laravel/Symfony monorepo): if you genuinely need Symfony BinaryFileResponse semantics (e.g. X-Sendfile offloading, X-Accel-Redirect for nginx), this bundle is a shortcut to that behavior. Otherwise response()->streamDownload() with a callback gets you the same without the Symfony dependency.
Pin and lock, then re-verify on every bump. Because releases ship without meaningful changelogs, treat each version as a fresh audit: run your download route's feature test (filename, disposition, streaming, 404 path) against the new tag before deploying, with the adapter provider as the single seam that can absorb renames.
src/ between the tag you're on and the tag you're moving to, or don't move.HttpFoundation response from a Laravel route works, but test it. Laravel returns it verbatim, so middleware that assumes Illuminate\Http\Response (some CORS, ETag, or cookie packages) may misbehave. If you hit oddities, wrap: return response()->make($symfonyResponse->getContent(), $status, $symfonyResponse->headers->all()); — though this loads the file into memory, defeating streaming.memory_limit. Check the implementation; prefer anything that returns BinaryFileResponse (streams) over a Response with full content. For anything over ~50 MB, use Laravel's response()->streamDownload() regardless.Content-Type for security decisions (e.g. blocking HTML downloads) — sniff with finfo and force a safe header when in doubt.nzo_file_downloader in config/packages/. There is no config/file-downloader.php to vendor:publish, so don't waste time searching for a publish tag.4.3.0, not ^4.3), commit the lockfile, audit the source before shipping, and keep the adapter thin so replacement is trivial. This is a "convenience helper," not infrastructure.Storage::disk('private')->download($path, $name, $headers) and return response()->file($path) do exactly this, are maintained, and need no adapter — and this release gives you no reason to change that calculus.How can I help you explore Laravel packages today?