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

Zipstream Php Laravel Package

maennchen/zipstream-php

ZipStream-PHP streams ZIP archives directly to the browser or any PSR-7 stream without writing to disk, quickly creating downloads from files, paths, or S3 buckets. Simple named arguments and a single finish call.

View on GitHub
Deep Wiki
Context7

Getting Started

Install with Composer and confirm PHP 8.2+ and a 64-bit build (both are hard requirements in v3.1.2+):

composer require maennchen/zipstream-php

Read in this order:

  1. The README usage block (the ZipStream constructor with outputName + sendHttpHeaders, then addFile / addFileFromPath / finish).
  2. The published docs at https://maennchen.github.io/ZipStream-PHP/ — especially the S3 / Flysystem examples, which is where most Laravel use cases live.
  3. The @internal notice in the 3.0 upgrade notes: the only public surface you should depend on is ZipStream\ZipStream, ZipStream\CompressionMethod, and ZipStream\Exception\*. Anything under ZipStream\Option\* from v2 no longer exists.

First use case worth wiring up — a controller action that streams several files from a disk without buffering:

use ZipStream\ZipStream;
use ZipStream\CompressionMethod;

public function export(Request $request)
{
    $zip = new ZipStream(
        outputName: 'export.zip',
        sendHttpHeaders: true,
        defaultCompressionMethod: CompressionMethod::DEFLATE,
    );

    foreach (Invoice::where('user_id', $request->user()->id)->cursor() as $invoice) {
        $zip->addFile(
            fileName: "invoices/{$invoice->number}.pdf",
            data: $invoice->pdfBytes(),
        );
    }

    $zip->finish();
}

Return null (or nothing) from the controller — the library writes headers and body directly. Do not wrap it in response()->download() or Response::stream().

Implementation Patterns

Streaming from a Laravel disk (local, S3, Flysystem). Open a PSR-7 stream via the disk adapter and hand it to addFileFromPsr7Stream. This is the pattern the 3.2.2 S3 example demonstrates, and it avoids loading file contents into memory:

$disk = Storage::disk('s3');
$zip->addFileFromPsr7Stream(
    fileName: 'report.pdf',
    stream: $disk->readStream('reports/2026/report.pdf'),
);

From a seekable path on the local disk, addFileFromPath is faster and avoids double-buffering:

$zip->addFileFromPath(
    fileName: 'invoice.pdf',
    path: storage_path('app/invoices/'.$id.'.pdf'),
);

Streaming into a queue job or a second destination. Since 3.2.0 you can pass a CallbackStreamWrapper instead of the default output stream — useful to simultaneously write to S3 and send the response, or to write from a queued job without touching HTTP:

use ZipStream\ZipStream;
use ZipStream\Stream\CallbackStreamWrapper;

$zip = new ZipStream(
    outputStream: CallbackStreamWrapper::open(function (string $data) use ($s3) {
        $s3->write($data); // or fwrite to a handle
    }),
    sendHttpHeaders: false,
);

Testing. Set sendHttpHeaders: false and pass a php://temp or a custom stream as outputStream. Capture the bytes, then ZipArchive::open() them in the assertion. The library's own test suite follows this pattern.

Choosing compression per file. The archive-level defaultCompressionMethod and defaultDeflateLevel can be overridden per addFile* call. Deflate is a good default for text/PDF; use CompressionMethod::STORE for already-compressed assets (JPEG, ZIP, MP4) to avoid wasted CPU. The removed largeFileSize / largeFileMethod options in v3 mean you implement the size-based switch yourself:

$method = $size > 10_000_000 ? CompressionMethod::STORE : CompressionMethod::DEFLATE;

File naming and directories. Pass forward-slash paths; the library handles directory entries. Prefer explicit lastModificationDateTime (a DateTimeInterface) when the source mtime is meaningful, otherwise files default to "now" and archives aren't byte-reproducible between runs.

Calculating size before streaming. Since 3.1.0 there's a ZipStream::calculateSize() helper — useful if you want to set Content-Length (via a ZipStream in simulation mode) before committing to the response. Don't call it against a live stream; it's a dry-run operation.

Gotchas and Tips

  • You must call finish(). The central directory is written at the end. Forgetting it produces a truncated, invalid ZIP with no error. Put it in a try/finally if any per-file work can throw.
  • Anything already echoed breaks the archive. Laravel middleware, debug output, or a stray dump() before streaming corrupts the ZIP because sendHttpHeaders: true fires immediately in the constructor. If you're unsure, use ob_end_clean() early or switch to sendHttpHeaders: false and set headers yourself.
  • Errors mid-stream are unrecoverable. Once bytes are on the wire you can't emit a JSON error. Validate inputs and check file existence before constructing ZipStream; after that, fail loudly (log, abort the connection) rather than trying to send a response.
  • defaultEnableZeroHeader is true since v3 (it was false in v2). This uses data descriptors and omits sizes from local headers — correct for streaming, but some very old unzip tools dislike it. Set defaultEnableZeroHeader: false on individual files if you know maxSize and need maximum compatibility.
  • Zip64 is opt-in per file size since 3.1.0 (previously always on). It's automatic when a file's maxSize or the aggregate exceeds 4 GB. If you're setting maxSize manually, under-reporting it can force Zip64 unnecessarily or, worse, break the directory; the 3.2.2 release fixed a related 4 GB offset bug, so pin to ^3.2.2 if you archive anything near that boundary.
  • 64-bit PHP only. On 32-bit builds, files over ~2 GB will silently misbehave due to integer overflow. Verify your production PHP arch before shipping large exports.
  • Method renames in v3. method → compressionMethod, time → lastModificationDateTime, size → maxSize. Old Option-class calls (ZipStream\Option\Archive, ZipStream\Option\File) fail with "class not found" — a common upgrade trap on older codebases.
  • Header callback type changed from callable to Closure. If you previously passed [$this, 'setHeaders'], wrap it.
  • Empty archives are legal but produce a file most unzip tools open fine; still, adding at least one file avoids weirdness with Content-Length-based download managers.
  • Extension points are intentionally slim. The @internal markers mean extending internals is unsupported. If you need behaviour that isn't exposed (custom header formats, per-file post-processing), your option is CallbackStreamWrapper — intercept the byte stream and transform it — rather than subclassing internals.
  • Streaming is the whole point. Avoid calling $zip->addFile(fileName: ..., data: file_get_contents(...)) in a loop over large files; you'll load everything into PHP memory. Use addFileFromPath / addFileFromPsr7Stream, or chunk data yourself.
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.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table