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

File Downloader Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. Use it inside a Symfony app (the intended path) — register NzoFileDownloaderBundle in config/bundles.php, configure under nzo_file_downloader in config/packages/.
  2. Use the underlying service class directly in Laravel — the bundle is mostly DI wiring around a 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.
  3. Skip it and use Laravel's native 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.

Implementation Patterns

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.

Gotchas and Tips

  • Bundle ≠ package. Installing it in Laravel does not auto-register anything. Nothing happens until you instantiate the service or write the binding. If you see "class not found" for a bundle extension, you're reading Symfony docs — ignore them.
  • v4.3.0 gives you no upgrade contract. "Code refactoring + Code enhancements" is not a changelog. It could be internal cleanup, or it could have renamed the service class or changed method signatures — there's no way to know from the notes. Diff src/ between the tag you're on and the tag you're moving to, or don't move.
  • Symfony 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 and streaming. If the downloader does file I/O by reading content, large files will blow 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.
  • MIME guessing is unreliable for extensionless or user-uploaded files. Don't trust the auto-detected Content-Type for security decisions (e.g. blocking HTML downloads) — sniff with finfo and force a safe header when in doubt.
  • Config key. In Symfony the package config lives under 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.
  • Maintenance risk is real, and the versioning makes it sharper. 8 stars, 0 dependents, low-activity cadence, and now a major-version jump (1.x → 4.x) accompanied by a one-line release note. Multiple majors with no documented breaking changes usually means either rapid semver churn or unreleased-API volatility — neither is what you want under an auth-sensitive download path. Pin exactly (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.
  • Extension point: the downloader service is not final in most NAYZO packages — subclass it or bind a decorated instance in your service provider if you need custom filename sanitization or auditing hooks. That's your escape hatch instead of forking. Verify the class is still non-final after the refactor before relying on it.
  • Recommendation for pure-Laravel shops: adopt it only for its inline/attachment switching ergonomics if you don't want to hand-roll headers. Otherwise 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.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console