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

Zip Streamed Response Bundle Laravel Package

wamania/zip-streamed-response-bundle

A Symfony bundle that streams ZIP archives directly to the HTTP response, so you can build and download archives on the fly without temporary files on disk. Useful for large or dynamically generated downloads.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require wamania/zip-streamed-response-bundle

The bundle wraps a ZipStream implementation (check composer.json — it pulls in the older mcnetic/zipstream or maennchen/zipstream-php depending on version) behind a Symfony Response subclass.

Register (Symfony 2/3 era)

// app/AppKernel.php
$bundles = [ /* ... */ new Wamania\ZipStreamedResponseBundle\WamaniaZipStreamedResponseBundle(), ];

On Symfony 4+ this bundle predates auto-discovery recipes; you can skip registration entirely and use the response class directly, or wire it yourself. The bundle adds almost no services — it is essentially one class.

Where to look first

  1. Response/ZipStreamedResponse.php — the whole public API. Read it before anything else; the method signatures (option arrays, local name defaults) are the contract you'll actually use.
  2. composer.json — tells you which ZipStream class is underneath, which determines available options (store vs deflate, Zip64 behavior, UTF-8 handling).
  3. The underlying library's README for per-file options.

First use case

use Wamania\ZipStreamedResponseBundle\Response\ZipStreamedResponse;

public function downloadAction(): ZipStreamedResponse
{
    $zip = new ZipStreamedResponse('archive.zip'); // name optional, or set via setContentDisposition
    $zip->addFile('/var/data/report.pdf', 'report.pdf');

    return $zip;
}

Return it directly from a controller. The archive is never written to disk: the callback writes entries to php://output as the client downloads.

Implementation Patterns

Multiple files from disk (the bread-and-butter case)

$zip = new ZipStreamedResponse('invoices-2024.zip');
foreach ($invoices as $invoice) {
    $zip->addFile($invoice->getAbsolutePath(), "invoices/{$invoice->getNumber()}.pdf");
}
return $zip;

Use a sub-path as the second argument to build folders inside the archive without creating them on disk.

Generated content without temp files

$zip->addContent($this->renderView('export/readme.txt.twig'), 'README.txt');
$zip->addContent($csv, 'data/rows.csv');

addContent buffers the string in memory, so use it for small, generated payloads (CSV exports, JSON manifests, license files), not for large blobs.

One file per record (common export shape)

foreach ($repository->iterateAll() as $row) {
    $zip->addContent($serializer->serialize($row, 'csv'), "records/{$row->getId()}.csv");
}

Iterate with a Doctrine cursor / generator, not findAll(), so memory stays flat.

Large generated files: spill to php://temp

$tmp = fopen('php://temp', 'r+');   // or tmpfile() for >2GB spills
fwrite($tmp, $hugeGeneratedContent);
rewind($tmp);
$zip->addFile($tmp, 'big.csv');    // if the API accepts a resource; otherwise write to a temp path

This keeps big generated files off the heap while still streamed into the archive.

Mix sources in one archive — path files + string content + empty directories (if supported) in a single response is the main reason to reach for this package over a hand-rolled ZipArchive + readfile().

Security / access control — do all authorization before constructing the response. Once the StreamedResponse callback starts writing, you cannot change the status code or headers. Return a normal 403/404, or throw before returning the zip.

Caching — avoid Cache-Control for personalized archives; for public ones, set Content-Disposition and let the reverse proxy cache the full streamed body only if you also send Content-Length, which you can't know ahead of time. In practice, don't cache these.

Headers — the response sets Content-Type: application/zip and a Content-Disposition attachment by default; override the filename with the constructor argument or setContentDisposition().

Gotchas and Tips

No Content-Length. Streaming means unknown size. Don't add the header manually, and check middleware / listeners don't either — a wrong length truncates the client's view. Clients handle this fine; progress bars will show unknown total.

Kill output buffering first. If PHP or Symfony has buffering on, the "stream" is actually assembled in RAM. In the callback (or before returning), do:

while (ob_get_level() > 0) { ob_end_clean(); }

Same reasoning applies to Symfony's StreamedResponse: it already flushes per chunk, but only if PHP's layers are gone.

Reverse proxy is your enemy. Nginx buffers responses and gzips them by default:

$response->headers->set('X-Accel-Buffering', 'no');
$response->headers->set('Content-Encoding', 'identity');

Apache: SetEnv no-gzip 1 for the route, or flush() after each entry. Gzipping an already-deflated zip is pure waste and can blow up latency.

Don't deflate already-compressed files. JPEG, MP4, PDF, and other zips gain nothing and burn CPU. Pass ['store' => true] (or the equivalent option in your underlying ZipStream version) for those entries. Verify the option key against composer.json's pinned library — it changed name between forks.

Zip64 limits. The 2016-era ZipStream may not emit Zip64 records. Above 4 GB total or 65,535 entries, clients will report a corrupt archive. If you might cross either limit, test early and consider upgrading to a maintained maennchen/zipstream-php and building the StreamedResponse yourself — the bundle's wrapper value is low at this point.

Old-package compatibility. No release since 2016: expect friction on PHP 8 (dynamic properties, strlen(null)) and Symfony 5/6 (deprecated bundle base classes, Response::setCallback signature). Many teams use just the response class without registering the bundle, or copy it into src/ and patch. Given zero dependents, assume no upstream fixes.

Symfony debug toolbar. In dev, the profiler and toolbar inject content or buffer the response, producing corrupt zips. Test in prod (or at least with the toolbar disabled) before concluding the package is broken.

Timeouts on large archives. set_time_limit(0) and raise max_execution_time for big exports; a mid-stream fatal yields a truncated, unopenable zip with no HTTP error — the client sees a 200 and a broken file.

Exceptions mid-stream are silent. Once headers are sent, a thrown exception can't become a 500. Wrap the callback body in try/catch and log; ideally pre-validate every file's existence/permissions before returning the response.

Filename encoding. Use ASCII names or ensure UTF-8 flags are set by the underlying library; older ZipStream versions mangle accents, and Windows Explorer is unforgiving.

Testing. Functional tests can't assertResponse on the body of a streamed response — call $response->sendContent() into a buffer, save to a temp file, and validate with PHP's ZipArchive or unzip -t. Integration-test the real HTTP path with curl -o out.zip and check exit code plus entry count.

Debugging checklist for "corrupt zip": 1) any output before the callback (BOM, whitespace, echo in a listener)? 2) buffering still on? 3) gzip/proxy interference? 4) fatal mid-stream (check logs)? 5) Content-Length set by middleware? Six times out of ten it's #1 or #3.

Extension point. Subclass ZipStreamedResponse and override the callback construction (or the name/headers) to centralize conventions — a CompanyZipResponse that sets the filename pattern, X-Accel-Buffering, and stored-compression for media types will save repetition across controllers.

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