etshy/php-zip-bundle
A Symfony bundle that brings convenient ZIP archive handling to PHP applications. It provides services and configuration around native ZIP support so you can create, inspect, and extract compressed files with less boilerplate.
Before anything else: confirm you actually need this package. This is a Symfony bundle (Bundle subclass + DI extension + compiled service definitions), not a Laravel package. Laravel does not load Symfony bundles, does not process their Extension/Configuration classes, and will not autowire the services the bundle registers. composer require etshy/php-zip-bundle will install (dragging symfony/dependency-injection and symfony/http-kernel transitively) but nothing will boot, and debug:container will show no zip service. So "getting started" in Laravel really means one of two paths:
php -m | grep -i zip # "zip" must be present
# Debian/Ubuntu base images:
# apt-get install -y libzip-dev && docker-php-ext-install zip
Then create your own contract and implementation:
// app/Contracts/ArchiveService.php
interface ArchiveService
{
public function create(string $dest, array $files, array $opts = []): string;
public function extract(string $archive, string $to): array;
public function list(string $archive): array;
}
// app/Services/ZipArchiveService.php — ~80 lines over ZipArchive
Bind it in AppServiceProvider; test with Storage::fake() and fixture archives.
composer require in Laravel — expose the archive functionality via an HTTP endpoint or CLI on the Symfony side and call it. Cross-framework invocation through shared code is painful; RPC/CLI is honest.If you insist on inspecting it: read in this order —
composer.json (PHP/Symfony bounds, ext-zip requirement, license), src/DependencyInjection/Configuration.php (what config keys exist: temp dir, compression level, extension), src/DependencyInjection/*Extension.php and Resources/config/services.* (service IDs, public vs private), then the service class itself. That's the whole bundle. If it isn't on Packagist with tags, plan to vendor it from GitLab.
First use case once you've ported it: a console command that exports a directory.
#[AsCommand('app:export:zip')]
class ExportZipCommand extends Command
{
public function __construct(private ArchiveService $archives) { parent::__construct(); }
protected function execute(InputInterface $in, OutputInterface $out): int
{
$path = $this->archives->create(storage_path('app/exports/report.zip'), [
'report.pdf' => storage_path('app/reports/report.pdf'),
]);
$out->writeln("Wrote {$path}");
return self::SUCCESS;
}
}
Async export via queues. Zip work is CPU and I/O bound and can exceed max_execution_time on large sets. The pattern that holds up: controller stores an "export requested" record, dispatches a job, the job builds the archive into a per-job unique temp directory, then moves the finished file onto Storage (local or S3):
class BuildExportJob implements ShouldQueue
{
public int $timeout = 900; // explicit — default 60s will kill big zips
public int $tries = 1;
public function handle(ArchiveService $archives): void
{
$tmp = storage_path('app/tmp/export-' . $this->export->uuid);
Filesystem::ensureDirectoryExists($tmp);
$zip = $archives->create("{$tmp}/bundle.zip", $this->collectFiles());
Storage::disk('exports')->putFileAs('', new File($zip), "{$this->export->uuid}.zip");
Filesystem::deleteDirectory($tmp);
}
}
Unique working paths. Never write to a shared tmp/export.zip — concurrent workers will collide. Use storage_path('app/tmp/'.$uuid) or tempnam() and clean it in finally.
Stream downloads for large archives. Don't ->download() a 2GB file through PHP-FPM with a memory ceiling. For streaming zips use ZipStream-PHP (maennchen/zipstream-php is maintained and Laravel-friendly) so you never materialize the whole archive:
return (new ZipStream())->addFileFromPath('report.pdf', $path)->finish();
Port the config into config/. If the bundle had temp_dir / compression level, put them in config/filesystems.php or a dedicated config/archive.php and inject via config('archive.temp_dir'). That's how you get the Symfony-container configurability the original offered.
Keep it behind the contract. Controllers and jobs should depend on ArchiveService, never on ZipArchive or the bundle class. When you swap to a streaming writer for large archives, one binding changes.
Testing. Build fixture archives (small .zip files committed once), and test against hostile ones too — a zip containing ../../etc/passwd, absolute paths, symlinks. Assert your extract rejects them.
ext-zip is not on by default in many PHP base images. If it isn't installed, ZipArchive is undefined and every call throws. Add libzip-dev + docker-php-ext-install zip to your image and document it in CI.symfony/console, symfony/process, symfony/http-foundation, symfony/finder. A bundle with loose or stale Symfony bounds can force a downgrade or fail resolution outright..., and skip symlink entries. This is exactly the reason to own the code rather than depend on an unmaintained wrapper.ZipArchive operates against a materialized file. Large exports hit memory_limit, max_execution_time, and fill /tmp. Use streaming or queue-friendly timeouts and cleanup.$zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE) returning true/false/error code is far more useful than exception strings. Wrap it and map error codes.How can I help you explore Laravel packages today?