Crear un ZIP y forzar su descarga en PHP (2026)

¿Necesitas crear un archivo ZIP en PHP y forzar su descarga? Se hace con la clase ZipArchive (incluida en el core de PHP): abres un ZIP, le añades los archivos con addFile(), lo cierras y envías las cabeceras de descarga (Content-Type: application/zip y Content-Disposition: attachment) antes de leerlo con readfile(). Aquí tienes el código completo, funcional y actualizado, paso a paso.

Actualizado: junio 2026 — probado con PHP 8.4 y 8.5. ZipArchive forma parte del core de PHP desde la versión 5.2.

El código completo (PHP 8)

Este es el script entero. Crea un ZIP con dos imágenes (una en la raíz y otra dentro de una carpeta), lo fuerza a descargar y borra el temporal del servidor. Debajo lo desglosamos:

<?php
// crear-zip.php — Crea un ZIP y fuerza su descarga (PHP 8)

$zip = new ZipArchive();
$nombreZip = 'miarchivo.zip';

// 1. Abrir/crear el ZIP y COMPROBAR que se ha abierto bien
if ($zip->open($nombreZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
    exit('No se pudo crear el archivo ZIP.');
}

// 2. Añadir un directorio dentro del ZIP
$zip->addEmptyDir('miDirectorio');

// 3. Añadir archivos
$zip->addFile('imagen1.png', 'mi_imagen1.png');               // en la raíz del ZIP
$zip->addFile('imagen2.png', 'miDirectorio/mi_imagen2.png');  // dentro del directorio

// (Opcional) Añadir contenido sin necesidad de un archivo físico
$zip->addFromString('leeme.txt', 'Generado el ' . date('Y-m-d'));

// 4. Cerrar el ZIP para volcarlo a disco
$zip->close();

// 5. Forzar la descarga con las cabeceras correctas
header('Content-Type: application/zip');
header('Content-Disposition: attachment; filename="' . basename($nombreZip) . '"');
header('Content-Length: ' . filesize($nombreZip));
header('Pragma: no-cache');

readfile($nombreZip);

// 6. Borrar el ZIP temporal del servidor y terminar
unlink($nombreZip);
exit;

Para probarlo, crea una carpeta (por ejemplo prueba_zip), coloca dentro este crear-zip.php junto a dos imágenes llamadas imagen1.png e imagen2.png, y ábrelo desde el navegador. Se descargará miarchivo.zip automáticamente. Importante: el directorio necesita permisos de escritura para poder crear el archivo temporal.

Paso a paso, qué hace cada parte

1. Crear y abrir el ZIP (comprobando errores)

Instanciamos ZipArchive y abrimos el archivo. La diferencia clave respecto a ejemplos antiguos es comprobar el valor que devuelve open(): devuelve true si va bien o un código de error si no. Si no lo compruebas y algo falla (permisos, ruta), acabas sirviendo un ZIP vacío o corrupto.

if ($zip->open($nombreZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
    exit('No se pudo crear el archivo ZIP.');
}

Combinamos dos flags con |: ZipArchive::CREATE crea el archivo si no existe, y ZipArchive::OVERWRITE lo sobrescribe si ya existía (así no acumulas contenido de descargas anteriores).

2. Añadir carpetas, archivos y contenido

Tienes tres métodos muy útiles:

  • addEmptyDir('miDirectorio'): crea una carpeta vacía dentro del ZIP.
  • addFile($rutaOrigen, $rutaDentroDelZip): añade un archivo del disco. El segundo parámetro es opcional y define el nombre/ruta que tendrá dentro del ZIP; si lo omites, se guarda en la raíz con su nombre original.
  • addFromString($nombre, $contenido): crea un archivo dentro del ZIP directamente desde una cadena, sin necesidad de que exista físicamente. Perfecto para generar un .txt, un CSV o un JSON al vuelo.

3. Cerrar y forzar la descarga

El close() es el que realmente escribe el ZIP en disco, así que va siempre antes de servirlo. Después envías las cabeceras. Aquí hay dos mejoras respecto al clásico ejemplo de hace años:

  • Usamos Content-Type: application/zip (el tipo MIME correcto para un ZIP) en lugar del genérico application/octet-stream.
  • Añadimos Content-Length con el tamaño real del archivo: ayuda al navegador a mostrar la barra de progreso y a detectar descargas incompletas.

Por último, readfile() vuelca el ZIP al navegador, unlink() borra el temporal del servidor para no dejar basura, y exit evita que se envíe cualquier carácter extra que corrompa el archivo.

Comprimir una carpeta entera (con subcarpetas)

El ejemplo anterior añade archivos uno a uno, pero lo habitual es querer comprimir una carpeta completa respetando su estructura. Para eso recorremos el directorio de forma recursiva con RecursiveDirectoryIterator:

<?php
$rutaCarpeta = __DIR__ . '/prueba_zip';
$nombreZip   = 'carpeta.zip';

$zip = new ZipArchive();
if ($zip->open($nombreZip, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
    exit('No se pudo crear el ZIP.');
}

$archivos = new RecursiveIteratorIterator(
    new RecursiveDirectoryIterator($rutaCarpeta, FilesystemIterator::SKIP_DOTS)
);

foreach ($archivos as $archivo) {
    $rutaAbsoluta = $archivo->getRealPath();
    // Ruta relativa para mantener la estructura dentro del ZIP
    $rutaRelativa = substr($rutaAbsoluta, strlen($rutaCarpeta) + 1);
    $zip->addFile($rutaAbsoluta, $rutaRelativa);
}

$zip->close();
echo "ZIP creado con " . $zip->numFiles . " archivos.";

El FilesystemIterator::SKIP_DOTS evita que se cuelen las entradas . y .., y calculamos la ruta relativa con substr() para que dentro del ZIP se conserve la jerarquía de carpetas tal cual.

Errores típicos a evitar

  • No comprobar open(): es la causa nº1 de «el ZIP se descarga pero está corrupto».
  • Imprimir algo antes de las cabeceras: un espacio o un echo antes de header() rompe la descarga (error «headers already sent»).
  • Olvidar close() antes de readfile(): el archivo aún no está completo en disco.
  • Permisos de escritura: si la carpeta no es escribible, open() falla. En servidores compartidos, usa una carpeta temporal con permisos (o sys_get_temp_dir()).

Resumen

Crear y descargar un ZIP en PHP moderno son siempre los mismos pasos: new ZipArchive()open() comprobando errores → añadir contenido (addFile, addEmptyDir, addFromString) → close() → cabeceras (application/zip + attachment + Content-Length) → readfile()unlink() + exit. Con el iterador recursivo, además, comprimes carpetas enteras en un par de líneas.

Posts relacionados

¿Necesitas un desarrollo a medida en PHP?

Si este tipo de soluciones se te quedan cortas y necesitas una aplicación web o un desarrollo WordPress/PHP hecho a medida, soy desarrollador freelance y puedo ayudarte. Pídeme presupuesto para tu desarrollo WordPress o PHP a medida y te respondo sin compromiso.

Un saludo, y si aún no lo has hecho no olvides suscribirte a mi blog para no perderte los próximos posts  :-),

También puedes seguirme en Twitter en ‎@revi_apps y no olvides que me ayudas mucho si compartes este post en las redes sociales.

19 comentarios en “Crear un ZIP y forzar su descarga en PHP (2026)”

  1. Ramon Bravo Querol

    Tengo una web que la gente se descarga grupos de imagenes en archivos zip, pero los clientes que usan Mac tienen problemas a la hora de descomprimir el archivo resultante y les aparece vacio.

    Como se puede solucionar?

  2. Hola, estoy siguiendo tu codigo y en lugar de abrirse el cuadro de dialogo para descargar el zip, el mismo se descarga directamente en la pantalla.
    ¿alguna ayuda?
    Muchas gracias!!!

    1. Encontré el problema y lo quería comentar por si a alguien le pasa algo similar.
      El asunto es que había «algo» que estaba enviando una salida antes que la serie de header, con lo cual PHP no era capaz de modificar las cabeceras.
      Incluyo un config.php para poder conectarme a la base de datos, resulta que luego de cerrar el php, había tres lineas en blanco…. ahí estaba el problema
      Espero que mi torpeza le sirva a alguien.

      Saludos!!

  3. Jorge Luis Mondragon

    hola necesito ayuda es para crear y descargar archivos no me genera nada paso codigo eso es en parte de mi boton
    $(document).on(‘click’,’.descargar’,function(){
    var img=$(this);
    $.ajax({
    url: ‘querys.php’,
    type: ‘POST’,
    data: {query: ‘dwn_file_contratista’, id_contratista: $(img).data(«id_contratista»)},
    success: function(){

    }
    });
    });

    y esto ya es parte del archivo querys.php

    case ‘dwn_file_contratista’:

    $contratista=$_POST[‘id_contratista’];

    $sql_contratista_files=mysql_query(«SELECT * FROM contratista_files WHERE id_contratista=’$_POST[id_contratista]’ AND status=’0′») or die(mysql_error());
    $sql_contratista_auditoria=mysql_query(«SELECT * FROM contratista_auditoria WHERE id_contratista=’$_POST[id_contratista]'») or die(mysql_error());
    $sql_contratista_amonesta=mysql_query(«SELECT * FROM contratista_amonestacion WHERE id_contratista=’$_POST[id_contratista]'») or die(mysql_error());

    $zip = new ZipArchive();

    $filename = ‘files_’.$contratista.’.zip’;
    $zip->open($filename,ZipArchive::CREATE);

    while($file = mysql_fetch_array($sql_contratista_files))
    {
    $zip->addFile($file[url]);
    }
    while($auditoria= mysql_fetch_array($sql_contratista_auditoria))
    {
    $zip->addFile($auditoria[url_archivo]);
    }
    while($amonesta=mysql_fetch_array($sql_contratista_amonesta))
    {
    $zip->addFile($amonesta[url_archivo]);
    }

    $zip->close();

    header(«Content-type: application/octet-stream»);
    header(«Content-Disposition: attachment; filename=».$filename);
    readfile($filename);
    unlink($filename);

    break;

    1. Hola Jorge Luis, estás haciendo una llamada Ajax para descargar el archivo zip, esto no te va a funcionar, tienes que acceder a la url donde vas a generar el archivo directamente (sin peticiones Ajax) mediante un link en html o si lo vas ha hacer desde javascript con window.location.href=url_donde_generas_el_archivo_comprimido.

  4. Francisco Javier

    ¿Tengo un problema, deseo agregar bastantes archivos y los temporales no alcanzan como puedo solucionar eso?

    Saludos y gracias

  5. Hola, tengo un problema, me genera el archivo pero me da un error: «se produjo un error cargando el archivo», ¿se sabe a qué es debido? muchas gracias!

    Un saludo!

      1. Hola, ya sé que es antiguo, jeje, lo siento pero era por si se sabía el motivo. No es Mac, pero trabajo con máquina virtual Linux. Gracias por responder.

  6. Como se pueden agregar archivos que son generados por el mismo sistema, solo tengo la ruta donde se genera dicho archivo, alguien que pudiera ayudarme

  7. Hola, sé que el post es viejo pero tal vez alguien lo ve y me puede ayudar.

    Si uso ese mismo código, solo que obtengo todos los archivos de una carpeta y los incluyo en el zip, qué puede estar mal si el archivo zip que se descarga me da un error de archivo zip no válido si lo intento abrir pero si utilizo WinRAR sí me lo abre sin problemas?

    Gracias

    1. Hola, gracias por comentar.

      Ese error suele deberse a cómo se envía el archivo al navegador. Aunque el ZIP se cree bien, si antes de las cabeceras o del readfile() se genera cualquier salida (incluso un espacio en blanco fuera del ), el archivo puede corromperse y dar ese mensaje al abrirlo.

      Una forma de evitarlo es limpiar el búfer justo antes de enviar el archivo:

      ob_clean();
      flush();
      readfile(‘miarchivo.zip’);

      Y asegúrate de que el archivo se cierre con $zip->close() antes de intentar leerlo.

      Fuera de eso, el código sigue funcionando igual hoy en día, aunque ya existen formas más limpias o seguras de manejar descargas. Pero para un ejemplo sencillo como este, con esos pequeños ajustes debería bastar.

      Ten en cuenta que esta entrada es bastante antigua y puede no reflejar las prácticas más actuales, pero espero que al menos te sirva como referencia para entender cómo funciona el proceso.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Scroll al inicio