ZipArchive en PHP: crear, leer y extraer ficheros ZIP

La clase ZipArchive de PHP permite crear, leer y extraer ficheros ZIP directamente desde PHP sin necesidad de comandos del sistema operativo. Está disponible por defecto en PHP moderno (requiere la extensión zip) y cubre desde casos sencillos como ofrecer una descarga de múltiples ficheros hasta backups recursivos de directorios o importación de datos desde un ZIP subido por el usuario.

Verificar que ZipArchive está disponible

<?php
if (!class_exists('ZipArchive')) {
    throw new RuntimeException('La extensión zip no está habilitada');
}
// En Debian/Ubuntu: sudo apt install php-zip
// En php.ini: extension=zip

Crear un ZIP y añadir ficheros

<?php
$zip = new ZipArchive();
$ruta = '/tmp/documentos.zip';

// ZipArchive::CREATE crea el fichero; ZipArchive::OVERWRITE lo sobreescribe si existe
if ($zip->open($ruta, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
    throw new RuntimeException('No se pudo crear el ZIP');
}

// Añadir fichero desde disco con nombre de destino dentro del ZIP
$zip->addFile('/var/www/docs/manual.pdf', 'documentos/manual.pdf');
$zip->addFile('/var/www/docs/factura.pdf', 'documentos/factura.pdf');

// Añadir desde string (sin necesidad de escribir en disco)
$csv = "nombre,emailnAna,[email protected],[email protected]";
$zip->addFromString('exportacion.csv', $csv);

// Añadir directorio completo recursivamente (ver siguiente sección)
$zip->addEmptyDir('logs');  // directorio vacío

$zip->close();
echo "ZIP creado: $ruta (" . round(filesize($ruta) / 1024, 1) . " KB)n";

Generar un ZIP para descarga directa

<?php
// Sin escribir en disco: el ZIP se envía directamente al navegador
function descargarComoZip(array $ficheros, string $nombreZip = 'descarga.zip'): never {
    $temporal = tempnam(sys_get_temp_dir(), 'zip_');

    $zip = new ZipArchive();
    $zip->open($temporal, ZipArchive::CREATE | ZipArchive::OVERWRITE);

    foreach ($ficheros as $rutaReal => $nombreEnZip) {
        if (file_exists($rutaReal)) {
            $zip->addFile($rutaReal, $nombreEnZip);
        }
    }
    $zip->close();

    // Cabeceras de descarga
    header('Content-Type: application/zip');
    header('Content-Disposition: attachment; filename="' . addslashes($nombreZip) . '"');
    header('Content-Length: ' . filesize($temporal));
    header('Cache-Control: no-cache');

    readfile($temporal);
    unlink($temporal);
    exit;
}

// Uso: ofrecer varios PDFs como un ZIP
descargarComoZip([
    '/var/facturas/factura_001.pdf' => 'Factura_001.pdf',
    '/var/facturas/factura_002.pdf' => 'Factura_002.pdf',
    '/var/contratos/contrato.docx'  => 'Contrato.docx',
], 'documentos_cliente.zip');

Backup recursivo de un directorio

<?php
function zipDirectorio(string $dirOrigen, string $zipDestino): void {
    $zip = new ZipArchive();
    if ($zip->open($zipDestino, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
        throw new RuntimeException("No se pudo crear: $zipDestino");
    }

    $iter = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator($dirOrigen, RecursiveDirectoryIterator::SKIP_DOTS),
        RecursiveIteratorIterator::LEAVES_ONLY
    );

    $prefijo = strlen(rtrim($dirOrigen, '/')) + 1;

    foreach ($iter as $fichero) {
        $rutaAbsoluta = $fichero->getRealPath();
        $rutaEnZip    = substr($rutaAbsoluta, $prefijo);

        // Excluir ficheros de caché y logs
        if (str_contains($rutaEnZip, '/cache/') || str_ends_with($rutaEnZip, '.log')) {
            continue;
        }

        $zip->addFile($rutaAbsoluta, $rutaEnZip);
    }

    $zip->close();
    echo "Backup: $zipDestino (" . round(filesize($zipDestino) / 1024 / 1024, 2) . " MB)n";
}

// Ejemplo de uso
zipDirectorio('/var/www/miapp', '/var/backups/miapp_' . date('Ymd_His') . '.zip');

Cifrado con contraseña AES-256

<?php
// ZipArchive soporta cifrado AES-256 desde PHP 7.2 con libzip >= 1.6
$zip = new ZipArchive();
$zip->open('/tmp/privado.zip', ZipArchive::CREATE | ZipArchive::OVERWRITE);

// Contraseña global para el archivo
$zip->setPassword('mi_contraseña_segura');

// Añadir ficheros con cifrado AES-256
$zip->addFile('/var/datos/informe.pdf', 'informe.pdf');
$zip->setEncryptionName('informe.pdf', ZipArchive::EM_AES_256);

$zip->addFromString('datos_sensibles.json', json_encode(['api_key' => 'secreta']));
$zip->setEncryptionName('datos_sensibles.json', ZipArchive::EM_AES_256);

$zip->close();

// Verificar que se creó correctamente
$verificar = new ZipArchive();
$verificar->open('/tmp/privado.zip');
echo "Ficheros: " . $verificar->numFiles . "n";
$verificar->close();

Importar datos desde un ZIP subido por el usuario

<?php
function procesarZIPSubido(array $archivo): array {
    // Validar el fichero subido
    if ($archivo['error'] !== UPLOAD_ERR_OK) {
        throw new RuntimeException('Error al subir el fichero');
    }
    if ($archivo['size'] > 10 * 1024 * 1024) {  // máx 10 MB
        throw new RuntimeException('El fichero es demasiado grande');
    }

    // Verificar que es un ZIP real (no solo por extensión)
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    if ($finfo->file($archivo['tmp_name']) !== 'application/zip') {
        throw new RuntimeException('El fichero no es un ZIP válido');
    }

    $zip     = new ZipArchive();
    $dirTemp = sys_get_temp_dir() . '/importacion_' . uniqid();
    mkdir($dirTemp, 0700);

    if ($zip->open($archivo['tmp_name']) !== true) {
        throw new RuntimeException('No se pudo abrir el ZIP');
    }

    // Prevenir path traversal: verificar todas las rutas antes de extraer
    for ($i = 0; $i < $zip->numFiles; $i++) {
        $nombre = $zip->getNameIndex($i);
        // Rechazar rutas con .. o absolutas
        if (str_contains($nombre, '..') || str_starts_with($nombre, '/')) {
            $zip->close();
            throw new RuntimeException("Ruta sospechosa en ZIP: $nombre");
        }
    }

    $zip->extractTo($dirTemp);
    $zip->close();

    // Procesar los CSV extraídos
    $resultados = [];
    foreach (glob("$dirTemp/*.csv") as $csv) {
        $lineas = file($csv, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        $cabecera = str_getcsv(array_shift($lineas));
        foreach ($lineas as $linea) {
            $resultados[] = array_combine($cabecera, str_getcsv($linea));
        }
    }

    // Limpiar directorio temporal
    array_map('unlink', glob("$dirTemp/*"));
    rmdir($dirTemp);

    return $resultados;
}

// En el handler del formulario de importación:
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['archivo'])) {
    try {
        $datos = procesarZIPSubido($_FILES['archivo']);
        echo count($datos) . " registros importados";
    } catch (RuntimeException $e) {
        echo "Error: " . $e->getMessage();
    }
}

Leer el contenido de un ZIP sin extraer

<?php
$zip = new ZipArchive();
$zip->open('documentos.zip');

// Listar todos los ficheros
for ($i = 0; $i < $zip->numFiles; $i++) {
    $info = $zip->statIndex($i);
    echo $info['name'] . " — " . round($info['size'] / 1024, 1) . " KBn";
}

// Leer un fichero específico sin extraer
$contenido = $zip->getFromName('exportacion.csv');
$lineas    = explode("n", $contenido);

// Leer como stream (para ficheros grandes)
$stream = $zip->getStream('documentos/manual.pdf');
$pdf    = stream_get_contents($stream);
fclose($stream);

// Buscar un fichero por nombre (busca recursivamente)
$indice = $zip->locateName('factura.pdf');  // false si no existe
if ($indice !== false) {
    $info = $zip->statIndex($indice);
    echo "Encontrado en el índice $indice: " . $info['size'] . " bytes";
}

$zip->close();

Errores frecuentes

  • open() devuelve un int de error, no false: siempre compara con === true o con las constantes de error (ZipArchive::ER_NOENT, etc.). if (!$zip->open(...)) no es suficiente.
  • No llamar a close(): ZipArchive escribe los datos finales del ZIP al cerrar. Si omites close(), el ZIP puede quedar corrompido.
  • Path traversal en la extracción: siempre valida los nombres de los ficheros dentro del ZIP antes de extraer. Un ZIP malicioso puede intentar escribir fuera del directorio destino con rutas como ../../etc/passwd.
  • Cifrado no soportado: ZipArchive::EM_AES_256 requiere libzip >= 1.6. Comprueba con phpinfo() la versión de libzip si el cifrado falla.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP