lake/larke-pclzip
Laravel wrapper for the PclZip library, enabling PHP applications to create, extract, and inspect ZIP archives without requiring the ZipArchive extension. Provides a simple API for handling zip files within Laravel projects.
This is a thin Laravel wrapper around the classic pure-PHP PclZip class — the point of it is that you can create and extract .zip archives without the zip extension being installed. That is the entire value proposition. If ext-zip is available on your servers, ZipArchive will be faster and better-tested; this package earns its place on shared hosting, locked-down containers, or legacy boxes where you cannot enable extensions.
Install:
composer require lake/larke-pclzip
The package ships a Laravel service provider and (conventionally for the lake/* family) a facade plus a publishable config file. Start by reading three files before you write any code:
composer.json → extra.laravel.providers and extra.laravel.aliases — confirms auto-discovery and the exact facade alias.src/ → the service class that wraps PclZip. The README is thin, so the public methods in that class are the documentation.config/ → the config key namespace and the archive temp directory it uses.Publish config if the package provides one:
php artisan vendor:publish --provider="Lake\LarkePclzip\LarkePclzipServiceProvider"
First use case — extract a user-uploaded archive into a scratch directory:
use Lake\LarkePclzip\Facades\LarkePclzip; // verify alias name in composer.json
$result = LarkePclzip::extract(
storage_path('app/uploads/bundle.zip'),
storage_path('app/extracted/bundle')
);
// PclZip returns 0 (int) on failure, not an exception — check it.
if ($result === 0) {
abort(422, 'Archive could not be extracted.');
}
Do this inside a queued job, not in the request cycle, even for modest archives. Once you have that working end to end, the rest of the API is the familiar extract / create / listContent / add / delete set that PclZip exposes.
Wrap it in your own service class. Do not scatter facade calls through controllers. Bind a small ArchiveService in the container that takes the wrapper as a constructor dependency, so you can later swap to ZipArchive on hosts that have it:
final class ArchiveService
{
public function __construct(private \Lake\LarkePclzip\LarkePclzip $zip) {}
public function extractSafely(string $archive, string $dest): void { /* ... */ }
}
List before you extract. This is the pattern that saves you in production. Use the package's content-listing call to enumerate entries, and reject the whole upload if any entry name contains .., starts with /, or is a symlink. Then extract. Never trust an upload enough to extract it blind.
Create an archive from a directory and stream it as a download. Point the create call at a directory path, let it recurse, write to a temp file under storage/app/tmp, then return response()->download() and unlink in a terminating callback. Avoid building the archive fully in memory by setting the wrapper's temp-file option (see Gotchas) so large inputs spill to disk.
Incremental archives. PclZip's add() is genuinely useful for "append today's export to the existing monthly archive" workflows — you do not have to re-read and rewrite the whole file. Pair it with a "replace newer" style option if you are rebuilding entries that already exist.
Batch/queue everything. A ShouldQueue job per archive, with $timeout raised above the default 60s and $tries = 1, is the correct shape. Extraction of a 50 MB archive in pure PHP can easily exceed a request timeout on a shared host.
Validate the MIME and magic bytes before extraction. mime_content_type() plus a check that the file starts with PK\x03\x04 (or PK\x05\x06 for empty archives) filters out most junk before you hand it to the parser.
Testing. Use Storage::fake('local') and build a tiny fixture archive committed to tests/Fixtures/. Assert on extracted file names and contents, not on return codes — the wrapper's success signalling is inconsistent across PclZip versions (see below).
PclZip returns 0 on failure, not exceptions. This is the single biggest trap. extract() and create() return 0 when something goes wrong, and truthy result arrays otherwise. Any code that assumes an exception on error will silently succeed. Wrap every call in a helper that converts 0 and false into a thrown exception.
No ZIP64, no encryption, no >4 GB archives, no >65535 entries. PclZip is a pre-ZIP64 implementation. If users might upload large or password-protected archives, this package is the wrong tool and you should escalate to ext-zip or a shell-out to unzip/7z.
Memory and CPU. PclZip reads significant data into memory and is roughly an order of magnitude slower than ZipArchive. Use PCLZIP_OPT_TEMP_FILE_ON (or PCLZIP_OPT_TEMP_FILE_THRESHOLD with a byte value) so large entries are streamed to a temp file rather than buffered. Make sure sys_get_temp_dir() is writable and on the same filesystem as your destination if you care about rename performance. Set set_time_limit(0) inside the queue job and give the job a generous $timeout.
Zip-slip is your responsibility. Entry names come from a hostile file. PclZip has a directory-restriction option (PCLZIP_OPT_EXTRACT_DIR_RESTRICTION), but treat it as defence in depth — canonicalise each target path with realpath() and assert it is inside your destination before writing anything.
Filename encoding will bite you on Chinese archives. PclZip does not set the UTF-8 general-purpose flag (bit 11) on entries it creates, so archives produced here and opened in Windows Explorer will show mojibake for non-ASCII names. Files created by other tools with GBK-encoded names may come out garbled on extraction too. If your users are Chinese-speaking — likely, given the package's origin — test a real archive from their machine early, and expect to run names through mb_convert_encoding on the way in or out.
Symlinks and empty directories. Neither round-trips reliably. If your archive contains empty dirs, they may vanish; symlinks may be followed and their targets copied, which can balloon an archive or leak files outside the intended tree. Exclude both when creating from a directory walk.
Permissions. The extraction path inherits your process umask. If extracted files must be web-readable, chmod explicitly after extraction rather than relying on defaults — PclZip's chmod option only applies to entries that carry a mode in the archive, which archives from Windows typically do not.
LGPL-3.0. Fine to depend on in a closed-source application, but modifications to the library itself must be released under the same licence. If you patch PclZip to fix filename encoding or add ZIP64 support, you are obliged to publish the patch — keep those changes in your own wrapper where possible.
Bus factor. Zero stars, zero dependents, last release March 2024. There is no upstream you can rely on for fixes. Vendor the package into your own namespace if it is load-bearing, or better, take the PclZip class directly and own the ~15 lines of wrapper you actually need.
Check PHP 8.3+ compatibility explicitly. The bundled PclZip is old code; dynamic property creation and implicit nullable parameter deprecations in PHP 8.2+ are common failure points. Run the test suite with error_reporting(E_ALL) before trusting it on a modern runtime.
Extension point. If you must fix something, subclass the wrapper rather than editing vendor code — the service provider's container binding is the natural place to swap in your subclass via $this->app->bind(...) in a register() in your own AppServiceProvider.
How can I help you explore Laravel packages today?