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
php -m | grep -i zipIf 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
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
<?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:
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:
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
authmiddleware 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?
- Place two small test files in
storage/app/privateand open/reports/download. - Open the downloaded ZIP and confirm it contains
report-a.pdf,report-b.pdfandreadme.txt. - Confirm the temporary ZIP is gone from
storage/app/privateafter 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()returnsfalse. CheckFile::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.



