orkhanahmadov/laravel-zip-validator
Laravel validation rules for ZIP archives. Verify uploads are valid ZIPs, restrict allowed or denied file extensions, cap file counts and uncompressed sizes, and inspect archive contents with simple, chainable rules—no manual extraction required.
orkhanahmadov/laravel-zip-validator adds a single, focused validation rule that inspects the contents of an uploaded ZIP archive — something the built-in mimes:zip / extensions:zip rules cannot do (they only look at the file itself).
composer require orkhanahmadov/laravel-zip-validator
Requires the zip PHP extension. As of 1.11.0 the package requires Laravel 9+ / PHP 8.0+ — Laravel 8 support was dropped, so an existing L8 app will simply resolve to 1.10.x when constrained with ^1.0. If you pin explicitly, use ^1.10 on L8 and ^1.11 on L9+.
First use case — an upload form that must accept exactly two files, only images:
use Orkhanahmadov\ZipValidator\Rules\Zip;
$request->validate([
'archive' => ['required', 'file', 'mimes:zip', new Zip(2, ['jpg', 'png'])],
]);
Where to look first: the package README for the rule constructor signature (file count, allowed extensions, required file names), and the tests/ directory — the 1.11.0 wildcard feature is best understood by reading the test cases that were added alongside it.
FormRequest over inline validation. The rule is stateful per configuration, so defining it once in rules() keeps controllers clean:
public function rules(): array
{
return [
'archive' => [
'bail', 'required', 'file', 'mimes:zip', 'max:20480',
new Zip(
fileCount: 3,
extensions: ['csv'],
fileNames: ['data-*.csv', 'README.txt'], // 1.11.0 wildcards
),
],
];
}
Lean on wildcards instead of looping rules. Before 1.11.0 you had to enumerate every expected entry (['image-1.png', 'image-2.png', 'image-3.png']). With shell-style * patterns you can assert a shape: ['*.csv'], ['data-*.json', '*.txt'], ['export-2024-*.csv']. This is the single biggest day-to-day improvement in this release — it turns the rule from "validates one very specific archive" into "validates a vendor's file convention".
Chain after the cheap checks. Always place file and mimes:zip/extensions:zip before the Zip rule and use bail. The rule needs a real path on disk to open; letting it run against a string or a non-zip upload produces a confusing failure instead of the obvious "must be a zip".
Tier the configuration. For multi-tenant/plan-based limits, build the rule from config rather than hardcoding:
new Zip(config('imports.max_files'), config('imports.allowed_extensions'))
Separate rule instances for separate messages. One Zip instance yields one failure message. If you need distinct feedback for "wrong count" vs "wrong file names", use two instances (new Zip(3), new Zip(null, [], ['*.csv'])) rather than one combined rule.
Testing. Fake uploads have no useful contents, so build real fixtures:
$path = tempnam(sys_get_temp_dir(), 'zip');
$zip = new \ZipArchive();
$zip->open($path, \ZipArchive::CREATE);
$zip->addFromString('data-2024-01.csv', 'a,b');
$zip->close();
$file = new UploadedFile($path, 'import.zip', 'application/zip', null, true);
Then assert with assertValid() / assertInvalid(['archive' => ...]). Wrap fixture creation in a helper — you will need it repeatedly.
^1.11 constraint on an L8 project fails to resolve. Pin ^1.10 if you are mid-upgrade, and check your CI matrix still installs the expected tag.__MACOSX/ and .DS_Store poison file counts. Archives created on macOS carry extra entries that count toward fileCount and can trip extension checks. If your users are on Macs, validate wildcards/extensions rather than an exact count, or document "re-zip without resource forks".csv/ plus csv/a.csv may count as two. Exact fileCount assertions are fragile against any producer that stores explicit folder records.folder/data-1.csv). Test a pattern against your real producer's layout before trusting *.csv to mean "any CSV anywhere".*.jpg into the extensions argument — that argument is a plain allow-list. Case sensitivity also bites: .JPG is not .jpg.ZipArchive::statIndex) in a temporary test when users report false negatives.ext-zip is a hard dependency. It is easy to omit locally and discover in CI or on a minimal container. Add it to composer.json require and to your deploy image.extractTo() without path-traversal checks (../ entries), size limits, and a per-entry cap. The package validates; it does not make the archive trusted.Rule class or composing instances. That is usually the right call for anything project-specific (e.g. rejecting directories explicitly).How can I help you explore Laravel packages today?