stechstudio/laravel-zipstream
Laravel ZipStream lets you create and stream ZIP archives on the fly without writing them to disk. Add files from strings or storage, then return a memory-efficient download response—ideal for large archives and dynamic exports.
Install & first look
composer require stechstudio/laravel-zipstream
The service provider and ZipStream facade are auto-discovered. Start with three files:
README.md — the canonical API surface (create, add, addRaw, download, saveTo).src/ZipStream.php — public methods and their option arrays.vendor/maennchen/zipstream-php — the underlying streaming engine; compression, ZIP64, and header behavior all come from here.First use case: a streamed export from a route/controller
use STS\ZipStream\Facades\ZipStream;
Route::get('/orders/{order}/download', function (Order $order) {
return ZipStream::create("order-{$order->id}.zip")
->add(storage_path("app/invoices/{$order->id}.pdf"))
->addRaw($order->toJson(JSON_PRETTY_PRINT), 'order.json')
->download();
});
download() returns a Laravel/Symfony StreamedResponse, so you return it like any other response. No temp file is written — bytes go out as they are read.
1. Chain calls in a controller, return the response
Keep the route handler thin; download() sets Content-Type: application/zip and Content-Disposition: attachment. For inline previews use response() instead of download().
2. Mix sources in one archive
Files on disk (add), arrays of paths (add([...])), and generated content (addRaw($string, $filename)) can be combined freely. This is the package's sweet spot: CSV/JSON manifests generated in memory next to existing PDFs/images.
3. Storage disks and remotes
add() resolves local paths. For S3/HTTP sources, use the version's source/URL helpers (e.g. addUrl, source objects) if present; otherwise pull a stream from Storage::disk('s3')->readStream(...) and either wrap it in a source object or use addRaw for small payloads. Verify the exact signature against your installed version — this area changed between v2 and v3.
4. Reusable exporter service
Wrap archive assembly in a small class returning a ZipStream instance:
class OrderZipExporter {
public function build(Order $order): \STS\ZipStream\ZipStream { /* ... */ }
}
Then controllers call ->download() and queued jobs call ->saveTo(...). One builder, two delivery modes.
5. Auth via signed routes
Since the download URL is the access point, protect the route with normal middleware, or generate a URL::temporarySignedRoute(...) and return the ZIP from that signed route. Don't try to authorize inside the ZIP builder.
6. Archive-then-link (queues)
For large sets or slow remotes, build in a job and ->saveTo('exports/orders-2026.zip') on a disk, then email/notify a signed link. Streaming is great for interactive downloads; queued archiving is better for anything that risks hitting request timeouts.
7. Progress UX
If total size is known (you supply file sizes), the response can carry Content-Length, and the browser/JS client can render a real progress bar (onDownloadProgress in axios/fetch). Without sizes, progress is indeterminate — plan your UI accordingly.
8. Testing
Feature tests can assert status and headers (Content-Disposition, Content-Type). To verify contents, saveTo() a tempnam() file, open with ZipArchive, and assert entry names. For streamed responses, $response->streamedContent() works in Laravel's test helpers, but assertDownload() behavior varies by version.
?>, a BOM, a stray dd(), or a debug toolbar's ob_start will corrupt the ZIP. Exclude the download route from Debugbar/Telescope and any middleware that mutates response bodies.StreamedResponse bypasses some middleware. Content-rewriting middleware (compression, minifiers, CSP nonce injectors) can break the archive. Prefer a dedicated route group for downloads.Content-Length. The ZIP central directory is written last, so the total isn't known unless the library is told each file's size. Pass sizes where the API supports it when accurate progress or non-chunked responses matter.STORE (no compression) for already-compressed payloads — JPEG, MP4, PDFs, nested ZIPs — saves significant CPU and speeds up the stream. DEFLATE for text/CSV/JSON. Compression level is configurable on create().zipstream-php version enables it and that the client's unzip tool supports it.report.pdf files from different directories can confuse extractors. Set explicit filenames via the per-file name argument/option.max_execution_time or fastcgi_read_timeout. Bump limits for the export route, or move to the queue pattern.config/zipstream.php (if your version ships one) to tune default filename and headers; remember to re-publish after major upgrades. v2 → v3 changed the API shape (create/facade + sources), so pin a version and read the matching docs.Source implementations (when exposed) are the clean way to plug in arbitrary backends; container-bind your own exporter subclass if you need organization-specific defaults without forking.saveTo('/tmp/test.zip') and unzip — this isolates archive-building bugs from HTTP/streaming bugs much faster than inspecting headers.How can I help you explore Laravel packages today?