Laravel has no dedicated ZIP facade of its own — creating and extracting ZIP archives goes through PHP's built-in ZipArchive extension directly, which is fine, since its native API already covers the common cases without much extra ceremony.
Creating a ZIP file from multiple individual files
use ZipArchive;
$zip = new ZipArchive();
$zipPath = storage_path('app/exports/report.zip');
if ($zip->open($zipPath, ZipArchive::CREATE) === true) {
$zip->addFile(storage_path('app/reports/sales.pdf'), 'sales.pdf');
$zip->addFile(storage_path('app/reports/inventory.pdf'), 'inventory.pdf');
$zip->close();
}
The second argument to addFile() sets the name the file will have inside the archive — this can differ from the original filename on disk, useful for giving a cleaner or more descriptive name inside the downloaded ZIP than the source file actually has.
Zipping an entire folder recursively
function zipDirectory(string $sourcePath, string $zipPath): void
{
$zip = new ZipArchive();
$zip->open($zipPath, ZipArchive::CREATE);
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($sourcePath, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($files as $file) {
$relativePath = substr($file->getPathname(), strlen($sourcePath) + 1);
$zip->addFile($file->getPathname(), $relativePath);
}
$zip->close();
}
RecursiveIteratorIterator combined with RecursiveDirectoryIterator is what walks through every file in every subdirectory — necessary since ZipArchive itself has no built-in "add this whole folder recursively" method, only a way to add individual files one at a time.
Serving the ZIP file as a download
public function download()
{
$zipPath = storage_path('app/exports/report.zip');
return response()->download($zipPath)->deleteFileAfterSend();
}
deleteFileAfterSend() removes the temporary ZIP file from disk once the download response has fully finished sending — worth using for a ZIP generated on the fly per request, avoiding accumulating orphaned temporary files in storage over time.
Extracting a ZIP file
$zip = new ZipArchive();
if ($zip->open(storage_path('app/uploads/archive.zip')) === true) {
$zip->extractTo(storage_path('app/extracted'));
$zip->close();
}
Extracting just one specific file from a ZIP archive
$zip = new ZipArchive();
$zip->open(storage_path('app/uploads/archive.zip'));
$zip->extractTo(storage_path('app/extracted'), ['config.json']);
$zip->close();
Validating a ZIP file before extracting it
public function store(Request $request)
{
$request->validate([
'archive' => 'required|file|mimes:zip|max:10240',
]);
$path = $request->file('archive')->storeAs('uploads', 'archive.zip');
$zip = new ZipArchive();
if ($zip->open(storage_path('app/'.$path)) !== true) {
throw new \Exception('Uploaded file is not a valid ZIP archive.');
}
// proceed with extraction
}
The mimes:zip validation rule alone checks the file extension and basic MIME type, but a genuinely corrupted or malformed ZIP can still pass that check — explicitly checking $zip->open()'s return value before attempting to extract is what catches a file that looks like a ZIP by extension but isn't actually a valid, openable archive.
A security note on extracting user-uploaded ZIP files
Extracting a ZIP file from an untrusted source carries a genuine "zip slip" path-traversal risk, where a maliciously crafted archive entry name (like ../../etc/passwd) could write outside the intended extraction directory — ZipArchive::extractTo() includes protection against this in modern PHP versions, but this is worth being aware of specifically when the ZIP file itself comes from an untrusted user upload rather than a trusted, internally generated source.