Muhammad Yasir Amin

Loading websiteโ€ฆ

More from Muhammad Yasir Amin

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Subscribe on YouTube

How to Create a ZIP Archive in Laravel 13 with ZipArchive and Download It

๐Ÿ› ๏ธ Laravel Tutorials4 min read

How to Create a ZIP Archive in Laravel 13 with ZipArchive and Download It

Muhammad Yasir Amin

Muhammad Yasir Amin

Senior Full Stack Engineerโ€ขNov 12, 2026
How to Create a ZIP Archive in Laravel 13 with ZipArchive and Download It

To create a ZIP file in Laravel 13, use PHP's built-in ZipArchive class: open a new archive, add your files with addFile() or addFromString(), close it, and return it with response()->download(). Laravel has no special ZIP helper, so this works with plain PHP and needs the zip PHP extension. The steps below also show how to zip a whole folder and how to clean up the temporary file.

Targets: Laravel 13 (PHP 8.3 to 8.5) with the zip extension enabled.

Step 1: Check the zip extension

Code
php -m | grep -i zip

If nothing is printed, enable extension=zip in php.ini, or install the package php-zip on Linux. In Docker images, run docker-php-ext-install zip.

Step 2: Create the route

CSS
use App\Http\Controllers\ReportZipController;
use Illuminate\Support\Facades\Route;

Route::get('/reports/download', ReportZipController::class)->middleware('auth');

Step 3: Zip a few files and download

JavaScript
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use ZipArchive;

class ReportZipController extends Controller
{
    public function __invoke(Request $request): BinaryFileResponse
    {
        $zipPath = storage_path('app/private/reports-'.now()->format('Ymd-His').'.zip');

        $zip = new ZipArchive();

        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            abort(500, 'Could not create the ZIP file.');
        }

        $zip->addFile(storage_path('app/private/report-a.pdf'), 'report-a.pdf');
        $zip->addFile(storage_path('app/private/report-b.pdf'), 'report-b.pdf');
        $zip->addFromString('readme.txt', 'Generated on '.now()->toDateTimeString());

        $zip->close();

        return response()->download($zipPath)->deleteFileAfterSend(true);
    }
}

The second argument of addFile() is the name inside the archive. addFromString() creates a file from text without needing it on disk. deleteFileAfterSend(true) removes the temporary ZIP after the response is sent, so your storage does not fill up.

Make sure the destination folder exists and is writable. storage/app/private exists in a fresh Laravel project and is not publicly reachable.

How do I zip a whole folder?

Loop over the files with Laravel's File facade and keep the relative path inside the archive:

PHP
use Illuminate\Support\Facades\File;

$folder = storage_path('app/private/invoices');
$zip = new ZipArchive();
$zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);

foreach (File::allFiles($folder) as $file) {
    $zip->addFile($file->getRealPath(), $file->getRelativePathname());
}

$zip->close();

allFiles() returns every file in the folder and its subfolders. getRelativePathname() keeps the folder structure inside the ZIP instead of storing absolute server paths.

How do I zip files stored on a disk such as S3?

ZipArchive works with local files. For files on a remote disk, download them to a temporary local path first, or add their content from a stream:

PHP
use Illuminate\Support\Facades\Storage;

$zip->addFromString('photo.jpg', Storage::disk('s3')->get('photos/photo.jpg'));

This loads the whole file into memory, so it is fine for small files. For large files, stream each one to a temporary file and use addFile().

How do I protect the download?

  • Put the route behind auth middleware and check that the user may access every file you add.
  • Never build file paths from user input. If you must, check the path with realpath() and make sure it is inside the folder you expect.
  • Give the archive a generated name, not a name supplied by the user.

How do I check that it works?

  1. Place two small test files in storage/app/private and open /reports/download.
  2. Open the downloaded ZIP and confirm it contains report-a.pdf, report-b.pdf and readme.txt.
  3. Confirm the temporary ZIP is gone from storage/app/private after the response.

Common mistakes

  • Class ZipArchive not found: the zip extension is missing or disabled for the PHP version your web server uses.
  • Calling close() too late or not at all: the file is only written when you close the archive.
  • Adding a file that does not exist: addFile() returns false. Check File::exists() first.
  • Using absolute paths as entry names: always pass the second argument.
  • Leaving temporary ZIP files behind: use deleteFileAfterSend(true) or a scheduled clean-up.

FAQ

Can I password-protect the ZIP?

Yes. ZipArchive supports setPassword() and setEncryptionName(), but support depends on the zip library on your server, so test it.

Is there a package for this?

Packages exist for streaming ZIPs without writing temporary files, which helps with very large downloads. For ordinary sizes, ZipArchive is enough.

What about very large archives?

Generate them in a queued job, store the result on a disk and send the user a link, instead of building the ZIP inside the web request.

References

Article Index & Topic Tags
LaravelZipFilesDownload

YouTube Channel

Build Along With Me

Practical walkthroughs on Laravel, React, APIs and deployment workflows.

Muhammad Yasir Amin
Available for Contracts
Author & Solutions Architect

Senior Full Stack Engineer

Independent Senior Full Stack Engineer working on Laravel, React and Next.js applications, APIs and delivery workflows.

Laravel & PHP ApplicationsReact & Next.js InterfacesREST APIs & IntegrationsDatabase Queries & DebuggingDocker Development WorkflowsClear Scope & Handover
Discuss Your Next Step

What Does Your Application Need Next?

Share your current system, the problem you want to solve and what a useful result looks like. We can identify the right starting point and agree on a practical scope.

Technical Audit

Find the Next Useful Fix

Application Development

Scoped, Reviewable Milestones

Ongoing Support

Agreed Capacity and Priorities

Share your goals, current stack and requirements before the call.