maennchen/zipstream-php
Stream ZIP archives on the fly in PHP without writing to disk. Fast ZIP downloads with optional HTTP headers, supports adding files from strings/paths, works with S3 and PSR-7 streams, and can output to custom callbacks.
Installation:
composer require maennchen/zipstream-php
Ensure your project uses PHP 8.2+ (check composer.json constraints).
First Use Case: Stream a ZIP file directly to the browser without disk I/O:
use ZipStream\ZipStream;
$zip = new ZipStream(
outputName: 'download.zip',
sendHttpHeaders: true
);
$zip->addFile('hello.txt', 'Hello, ZipStream!');
$zip->finish();
sendHttpHeaders: true auto-sends Content-Type: application/zip and Content-Disposition.Where to Look First:
ZipStream\CompressionMethod enum for compression options (e.g., CompressionMethod::DEFLATE).addFile*() methods for different data sources (strings, files, streams).Workflow: Build ZIPs on-the-fly from database records, API responses, or user uploads.
// Example: Zip user-uploaded files from a Laravel request
$zip = new ZipStream(outputName: 'user_uploads.zip');
foreach ($request->file('files') as $file) {
$zip->addFileFromPath(
fileName: $file->getClientOriginalName(),
path: $file->getRealPath(),
compressionMethod: CompressionMethod::DEFLATE
);
}
$zip->finish();
Integration Tip: Use CallbackStreamWrapper to pipe ZIP data to cloud storage:
use ZipStream\Stream\CallbackStreamWrapper;
use League\Flysystem\Filesystem;
$fs = new Filesystem($adapter);
$zip = new ZipStream(
outputStream: CallbackStreamWrapper::open(
fn(string $data) => $fs->writeStream('remote.zip', $data)
),
sendHttpHeaders: false
);
// ... add files ...
$zip->finish();
Pattern: Optimize compression per file type/size:
$zip = new ZipStream(outputName: 'optimized.zip');
foreach ($files as $file) {
$method = $file['size'] > 10_000_000 // >10MB
? CompressionMethod::STORE
: CompressionMethod::DEFLATE;
$zip->addFile($file['name'], $file['content'], compressionMethod: $method);
}
Tip: Create a service class for reusability:
// app/Services/ZipService.php
class ZipService {
public function create(array $files, string $name): void {
$zip = new ZipStream(outputName: $name);
foreach ($files as $file) {
$zip->addFile($file['name'], $file['content']);
}
$zip->finish();
}
}
// Usage in Controller:
ZipService::create($files, 'export.zip');
Best Practice: For files >4GB, ensure:
Zip64 is auto-enabled (no config needed in v3.1+).CompressionMethod::STORE for uncompressed large files:
$zip->addFileFromPath(
'huge_file.dat',
'/path/to/huge_file.dat',
compressionMethod: CompressionMethod::STORE
);
Headers Sent Twice:
sendHttpHeaders: true with frameworks like Laravel/Symfony, disable framework headers first:
// Laravel: Remove default headers
header_remove('Content-Type');
$zip = new ZipStream(outputName: 'file.zip', sendHttpHeaders: true);
Memory Leaks:
addFileFromPath() for files >100MB instead of loading into PHP:
// ❌ Bad: Loads entire file into memory
$zip->addFile('large.txt', file_get_contents('/path/to/large.txt'));
// ✅ Good: Streams directly
$zip->addFileFromPath('large.txt', '/path/to/large.txt');
Timezone Issues:
lastModificationDateTime is timezone-aware:
$zip->addFile(
'file.txt',
'content',
lastModificationDateTime: new \DateTime('2023-01-01', new \DateTimeZone('UTC'))
);
CallbackStreamWrapper Quirks:
$zip = new ZipStream(
outputStream: CallbackStreamWrapper::open(
fn(string $chunk) => [
$s3Client->putObject(...), // Async
echo $chunk, // Browser
]
)
);
Validate ZIP Output:
Use ZipArchive to verify:
$zipData = file_get_contents('php://temp', 'r', null, 0, 0, true);
$zip = new ZipArchive();
$zip->openFromString($zipData);
if ($zip->status !== ZipArchive::ER_OK) {
throw new \RuntimeException('Invalid ZIP generated');
}
Log Stream Errors:
Wrap CallbackStreamWrapper to catch failures:
$zip = new ZipStream(
outputStream: CallbackStreamWrapper::open(
fn(string $data) => try {
$fs->writeStream('backup.zip', $data);
} catch (\Throwable $e) {
Log::error('Zip stream failed', ['error' => $e]);
throw $e;
}
)
);
Custom Compression:
Extend via ZipStream\CompressionMethod or create a wrapper:
class CustomCompressor implements \ZipStream\CompressionInterface {
public function compress(string $data, int $level): string {
// Custom logic (e.g., Brotli)
return gzcompress($data, $level);
}
}
Note: Requires internal API access (check GitHub Discussions).
PSR-7 Stream Support:
Use ZipStream\Stream\Psr7StreamWrapper for HTTP clients:
use ZipStream\Stream\Psr7StreamWrapper;
$zip = new ZipStream(
outputStream: Psr7StreamWrapper::fromPsr7Stream($response->getBody())
);
Progress Tracking:
Override ZipStream to emit events:
class ProgressZipStream extends ZipStream {
public function addFile(string $fileName, string $data): void {
event(new ZipFileAdded($fileName));
parent::addFile($fileName, $data);
}
}
defaultEnableZeroHeader: Set to false if you need legacy ZIP compatibility (default: true).$zip->addFile('file.txt', 'data', deflateLevel: 9);
'') still sends headers (set sendHttpHeaders: false to disable).How can I help you explore Laravel packages today?