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

Content Disposition Laravel Package

cardinalby/content-disposition

A small PHP library for parsing and generating HTTP Content-Disposition headers, including attachment/inline types, quoted filenames, and RFC 5987/6266 filename* encoding for safe, browser-compatible file downloads.

View on GitHub
Deep Wiki
Context7

Getting Started

This is a single-class, dependency-free library — no service provider, no config file, no facade. Installation is the whole setup:

composer require cardinalby/content-disposition

Nothing to register in config/app.php. The class autoloads via PSR-4 and is used directly.

Where to look first:

  1. The README (short — it is essentially the API surface).
  2. src/ContentDisposition.php — the entire package is one file; read it top to bottom in ~5 minutes. Do this before writing code, because the method names below are the shape of the API, not a guarantee of exact spelling.
  3. tests/ — the fastest way to see the exact expected header strings for edge cases like filename*=UTF-8''....

First use case — the one that actually justifies the dependency: you are proxying or re-emitting a file and both produce and consume Content-Disposition headers. Laravel's response()->download() handles the produce side well, but nothing in the framework parses an incoming Content-Disposition header into a filename and disposition type. That parsing is the gap this fills:

use cardinalby\ContentDisposition\ContentDisposition;

$cd = ContentDisposition::parse($request->header('Content-Disposition'));

$cd->getType();      // 'attachment' | 'inline'
$cd->getFilename();  // decoded filename, honours filename*

If all you need is return response()->download($path, $name);, stop here — you don't need this package.

Implementation Patterns

1. Streamed downloads with non-ASCII filenames

The common Laravel case: the file lives in S3 / is generated on the fly, so response()->download() (which wants a local path) is awkward, and you need a correct RFC 6266 header with both a legacy filename fallback and a UTF-8 filename*.

use cardinalby\ContentDisposition\ContentDisposition;

return response()->streamDownload(function () use ($stream) {
    fpassthru($stream);
}, 'placeholder.pdf', [
    'Content-Type'        => 'application/pdf',
    // replace the header Laravel generated with a fully RFC-compliant one
    'Content-Disposition' => (string) ContentDisposition::createAttachment('Отчёт 2024.pdf'),
]);

Note the cast to (string) — see Gotchas.

2. Parsing incoming headers (the primary differentiator)

Typical trigger: a webhook or an upstream API returns a file and tells you its name only via the header.

$filename = 'download.bin';

if ($header = $response->header('Content-Disposition')) {
    $cd = ContentDisposition::parse($header);
    $filename = $cd->getFilename() ?? $filename;
}

Wrap this in a small service or a PendingRequest macro so you're not repeating it in every controller:

// AppServiceProvider::boot()
Http::macro('filenameFrom', function ($response, string $default = 'download.bin') {
    $header = $response->header('Content-Disposition');
    return $header
        ? (ContentDisposition::parse($header)->getFilename() ?? $default)
        : $default;
});

3. Value object in a stored-file DTO

Because the parsed value survives __toString(), it is a clean fit for persisting the semantic fields rather than the raw string:

$cd = ContentDisposition::parse($request->header('Content-Disposition'));

Upload::create([
    'original_name' => $cd->getFilename(),
    'disposition'   => $cd->getType(),
]);

Downside: if you need to replay the exact header later, store the raw string too — re-serialising can reorder or normalise parameters.

4. Proxying a file end-to-end

Combining 1 and 2, the pattern is parse → sanitise → re-emit:

$upstream = Http::withToken($token)->get($url);

$cd = ContentDisposition::parse($upstream->header('Content-Disposition', 'attachment'));

return response()->streamDownload(
    fn () => print($upstream->body()),
    $cd->getFilename() ?? 'file.bin',
    ['Content-Disposition' => (string) $cd]
);

5. Explicit inline vs attachment

For PDF preview / image display you want inline; for forcing a save you want attachment. Keep the choice at the call site, not buried in the class:

$header = $request->boolean('preview')
    ? ContentDisposition::createInline($name)
    : ContentDisposition::createAttachment($name);

6. Testing

Assert on the final rendered string, not on object internals — that's what the browser sees:

$this->get('/export')
    ->assertHeader('Content-Disposition', "attachment; filename=\"report.pdf\"; filename*=UTF-8''report.pdf");

Copy the expected strings straight from the package's own test suite so you inherit its encoding decisions.

Gotchas and Tips

Laravel already generates this header. response()->download() and BinaryFileResponse go through Symfony's HeaderUtils::makeDisposition(), which implements RFC 6266/5987 including filename*. If you add this package only to generate headers, you're duplicating working framework code. Its real value is the parse direction and the explicit object model.

__toString() vs toString(). Small libraries in this space are inconsistent; some expose only an explicit method. If you pass the object straight into a header array and get Object of class ... could not be converted to string, add the explicit cast/call.

filename* wins over filename. Per RFC 6266, a receiver must prefer filename* when both are present. If you hand-roll parsing with explode(';') you will get this wrong — the legacy fallback is often ASCII-transliterated (Otchet 2024.pdf), so a naive parser silently mangles diacritics.

Don't explode(';', $header) yourself. Quoted parameter values can legally contain ; and escaped ". That's the whole point of using a parser. Same for the type: compare case-insensitively (Attachment is valid).

Percent-encoding is not urlencode. RFC 5987 uses %20 for spaces, never +. If you write your own encoder alongside this package the two will disagree on filenames containing spaces or +.

Sanitise before you serialise. A filename of "; filename="evil.exe or one containing a CRLF is a header-injection vector. Strip control characters and path separators (/, \, ..) before handing the name to the class — the library formats, it doesn't sanitise. Use basename() plus a regex strip for anything user-supplied.

Header size limits. Emitting both filename and filename* roughly doubles the header. Very long filenames can hit proxy/nginx buffer limits and get truncated; cap the filename length at ~150 chars before generating.

Low-maintenance, low-adoption package. 7 stars, 0 dependents, last release May 2023. That's not disqualifying for a pure-function class with no dependencies, but: pin the exact version in composer.json, and consider vendoring the single file into app/Support/ if you want to guarantee it never breaks under a PHP upgrade. There will be no security patches — though the attack surface here is genuinely tiny.

Unicode filenames need a browser test, not just a unit test. Chrome, Firefox and Safari differ in whether they honour filename* on inline dispositions, and Safari has historically mangled UTF-8 in inline; a green test suite does not mean the download lands with the right name on an iPhone.

Where to extend: since it's one class with no extension points, don't fight it. If you need policy (max length, transliteration table, blocked extensions), put that in a thin wrapper service in your app and keep the package as a pure formatter/parser. If you find yourself wanting to subclass it, you probably want your own 40-line implementation instead.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings