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
=== trueo 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_256requiere libzip >= 1.6. Comprueba conphpinfo()la versión de libzip si el cifrado falla.
