Antes de leer, escribir o procesar un fichero, conviene saber si existe, qué tipo es, cuánto ocupa y cuándo fue modificado. PHP tiene funciones específicas para cada uno de estos metadatos, más una trampa importante: el caché interno de stat que puede devolver información obsoleta.
file_exists(), is_file() e is_dir()
<?php
$ruta = '/var/www/app/uploads/imagen.jpg';
// file_exists() devuelve true tanto para ficheros como para directorios
var_dump(file_exists($ruta)); // true si existe (sea lo que sea)
var_dump(file_exists('/no-existe')); // false
// is_file() es true solo para ficheros regulares (no directorios, no enlaces simbólicos rotos)
var_dump(is_file($ruta)); // true si es un fichero regular
// is_dir() es true solo para directorios
var_dump(is_dir('/var/www/app/uploads')); // true
var_dump(is_dir($ruta)); // false (es un fichero)
// is_link() para enlaces simbólicos
var_dump(is_link('/var/www/app/storage')); // true si es un symlink
?>
filesize() y filemtime()
<?php
$ruta = '/var/www/app/uploads/documento.pdf';
// Tamaño en bytes
$bytes = filesize($ruta);
echo number_format($bytes / 1024, 2) . ' KB';
echo number_format($bytes / 1024 / 1024, 2) . ' MB';
// Timestamp de última modificación
$mtime = filemtime($ruta);
echo date('d/m/Y H:i:s', $mtime);
// Timestamp de creación (en Linux = tiempo de cambio de inodo, no creación real)
$ctime = filectime($ruta);
echo date('d/m/Y H:i:s', $ctime);
// Timestamp de último acceso
$atime = fileatime($ruta);
echo date('d/m/Y H:i:s', $atime);
?>
is_readable() e is_writable()
<?php
$ruta = '/var/www/app/uploads';
// Antes de intentar escribir, verificar que el directorio tiene permisos
if (!is_dir($ruta)) {
mkdir($ruta, 0755, true);
}
if (!is_writable($ruta)) {
throw new RuntimeException("El directorio $ruta no tiene permisos de escritura");
}
// Antes de leer un fichero de configuración
$config = '/etc/app/config.json';
if (!is_readable($config)) {
throw new RuntimeException("No se puede leer $config");
}
?>
stat(): todos los metadatos de una vez
stat() devuelve un array con 13 campos de metadatos, igual que la llamada al sistema stat():
<?php
$info = stat('/var/www/app/uploads/imagen.jpg');
echo $info['size']; // tamaño en bytes (igual que filesize())
echo $info['mtime']; // timestamp de modificación (igual que filemtime())
echo $info['atime']; // timestamp de acceso
echo $info['ctime']; // timestamp de cambio de inodo
echo $info['uid']; // ID del propietario
echo $info['gid']; // ID del grupo
echo $info['mode']; // permisos + tipo (octal)
echo $info['nlink']; // número de enlaces duros
echo $info['ino']; // número de inodo
?>
Ejemplo real: validar subidas de ficheros
<?php
function validarSubida(array $fichero, int $maxBytes = 5_242_880): void
{
if (!isset($fichero['tmp_name']) || !is_uploaded_file($fichero['tmp_name'])) {
throw new InvalidArgumentException('Fichero no válido');
}
$tamano = filesize($fichero['tmp_name']);
if ($tamano === false || $tamano > $maxBytes) {
throw new InvalidArgumentException(
'El fichero supera el límite de ' . number_format($maxBytes / 1024 / 1024, 1) . ' MB'
);
}
$ext = strtolower(pathinfo($fichero['name'], PATHINFO_EXTENSION));
$permitidas = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'pdf'];
if (!in_array($ext, $permitidas, true)) {
throw new InvalidArgumentException("Extensión .$ext no permitida");
}
}
?>
La trampa del caché de stat
PHP mantiene un caché interno de las llamadas a stat() para evitar llamadas repetidas al sistema de ficheros. Si modificas un fichero desde PHP y luego llamas a filesize() o filemtime(), puedes obtener los valores antiguos del caché:
<?php $ruta = '/tmp/contador.txt'; file_put_contents($ruta, '100'); echo filesize($ruta); // 3 (correcto) file_put_contents($ruta, '10000'); echo filesize($ruta); // 3 ? ¡caché! sigue devolviendo el valor anterior // Solución: limpiar el caché explícitamente clearstatcache(true, $ruta); // solo para esta ruta // o clearstatcache() para limpiar todo el caché echo filesize($ruta); // 5 ? correcto ahora ?>
Caché por tiempo de modificación
<?php
function necesitaActualizacion(string $cacheFichero, int $maxEdadSegundos = 3600): bool
{
if (!file_exists($cacheFichero)) {
return true;
}
clearstatcache(true, $cacheFichero);
return (time() - filemtime($cacheFichero)) > $maxEdadSegundos;
}
if (necesitaActualizacion('/tmp/datos-cache.json')) {
// regenerar caché
}
?>
La documentación de stat() y la de clearstatcache() detallan todos los campos disponibles y el comportamiento del caché con enlaces simbólicos.
