Reader Stacks

Creating and Extracting ZIP Files in Laravel

PHP's ZipArchive extension does the actual work here — Laravel adds essentially no abstraction on top of it, which is genuinely fine, since the native API is already fairly simple for the common cases.

Creating and Extracting ZIP Files in Laravel

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.