Finder: búsqueda de archivos
¿Necesita encontrar archivos que encajen con cierta máscara? Finder puede ayudarle. Es una herramienta versátil y rápida para recorrer estructuras de directorios.
Instalación:
composer require nette/utils
Los ejemplos suponen que se ha creado el siguiente alias de clase:
use Nette\Utils\Finder;
Uso
Veamos primero cómo usar Nette\Utils\Finder para listar
los nombres de los archivos con extensión .txt y .md del directorio actual:
foreach (Finder::findFiles(['*.txt', '*.md']) as $name => $file) {
echo $file;
}
El directorio de búsqueda predeterminado es el actual, pero puede cambiarlo con los métodos in()
o from(). La variable $file es una instancia de la clase FileInfo, mientras que
$name es una cadena con la ruta del archivo.
La ruta se devuelve tal como usted la escribió, conservando los separadores de la plataforma; en Windows, el resultado puede
mezclar por tanto / y \. Llame a FileSystem::unixSlashes() si necesita una forma
uniforme.
¿Qué buscar?
Además del método findFiles() existe findDirectories(), que busca solo directorios, y
find(), que busca ambas cosas. Estos métodos son estáticos, así que se pueden llamar sin crear una instancia. El
argumento de la máscara es opcional; si se omite, encaja todo.
foreach (Finder::find() as $file) {
echo $file; // ahora se listan todos los archivos y directorios
}
Con los métodos files() y directories() puede indicar qué más buscar. Los métodos se pueden
llamar repetidamente y también admiten un array de máscaras como argumento:
Finder::findDirectories('vendor') // todos los directorios
->files(['*.php', '*.phpt']); // más todos los archivos PHP
Una alternativa a los métodos estáticos es crear una instancia con new Finder (un objeto recién creado así no
busca nada de entrada) e indicar qué buscar con files() y directories():
(new Finder)
->directories() // todos los directorios
->files('*.php'); // más todos los archivos PHP
En la máscara puede usar comodines como *, **, ? y
[...]. Puede incluso indicar directorios; por ejemplo, src/*.php encuentra todos los archivos PHP del
directorio src. Los enlaces simbólicos se tratan también como directorios o archivos.
¿Dónde buscar?
El directorio de búsqueda predeterminado es el actual. Lo cambia con los métodos in() y from():
Finder::findFiles('*.php')
->in(['src', 'tests']) // busca directamente en src/ y tests/
->from('vendor'); // busca también en los subdirectorios de vendor/
Los dos métodos difieren en la profundidad: in() busca solo dentro del directorio indicado, mientras que
from() desciende también a sus subdirectorios (de forma recursiva). Para buscar recursivamente en el directorio
actual, use from('.').
La recursión, sin embargo, no la decide solo from(): el comodín ** de la máscara también la
dirige, así que findFiles('**/*.php')->in('src') busca igualmente de forma recursiva. Dicho de otro modo,
from('src') no es más que un atajo de in('src') con una máscara recursiva. Vea Comodines.
Estos métodos se pueden llamar varias veces, o puede pasar varias rutas en un array; los archivos se buscarán entonces en
todos los directorios indicados. Si alguno de los directorios no existe, se lanza Nette\InvalidStateException.
Las rutas relativas lo son respecto al directorio actual, pero también se pueden usar rutas absolutas:
Finder::findFiles('*.php')
->in('/var/www/html');
En la ruta puede usar los comodines *, ** y ?, pero no [...], que
allí se toma de forma literal. Esto evita comportamientos no deseados cuando, por ejemplo, busca en in(__DIR__) y la
ruta contiene por casualidad los caracteres []. Así, src/*/*.php busca todos los archivos PHP de los
directorios de segundo nivel bajo src.
Al buscar archivos y directorios de forma recursiva (en profundidad), se devuelve primero el directorio padre y después los
archivos que contiene. Este orden se puede invertir con childFirst().
Comodines
Una máscara puede contener varios caracteres especiales:
*– cualquier número de caracteres, salvo el separador/(se queda dentro de un mismo nivel de directorio)**– cualquier número de caracteres, incluido/(atraviesa niveles de directorio, vea más abajo)?– exactamente un carácter, salvo/[a-z]– un carácter del rango o conjunto indicado entre corchetes[!a-z]– un carácter que no esté entre los corchetes
El punto crucial, y fácil de pasar por alto: ** encaja con cero o más niveles de directorio. No
significa „al menos un subdirectorio“. Por eso, src/**/*.php encaja tanto con un archivo situado directamente en
src como con otro enterrado varios niveles más abajo. Considere este árbol:
src/
├── app.php
├── Model/
│ ├── User.php
│ └── Repository/
│ └── UserRepository.php
└── Control/
└── SignForm.php
La siguiente tabla muestra con qué encaja cada máscara, tanto para archivos como para directorios:
| Máscara | Encaja con |
|---|---|
src/*.php |
solo src/app.php (directamente en src) |
src/**/*.php |
src/app.php, src/Model/User.php, src/Model/Repository/UserRepository.php (todos los
niveles, incluido directamente en src) |
src/* |
los hijos directos de src: app.php, Model, Control |
src/** |
todo lo que hay bajo src, archivos y directorios (atajo de src/**/*) |
src/*/ |
los subdirectorios directos de src: Model, Control |
src/**/ |
todos los subdirectorios a cualquier profundidad: Model,
Model/Repository, Control |
Conviene recordar dos atajos:
- Un
**que no vaya seguido inmediatamente de/se comporta como**/más*. Así,src/**es un atajo desrc/**/*, y**.phplo es de**/*.php. - Una barra final restringe la máscara solo a directorios. Así,
find('log/')devuelve directorios llamadoslog, pero nunca un archivo con ese nombre. (findFiles()rechaza la barra final, porque buscar un archivo „directorio“ no tiene sentido.)
Más ejemplos de uso:
img/?.png– archivos con un nombre de una sola letra, como0.png,1.png,x.pnglogs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log– archivos de registro en formatoYYYY-MM-DDdocs/**/*.md– todos los archivos con extensión.mdendocsy en todos sus subdirectorios
Exclusión
Use el método exclude() para descartar archivos y directorios de los resultados. El argumento es una máscara con
la que el elemento no debe encajar. Aquí buscamos archivos *.txt salvo los que contienen la letra
X en el nombre:
Finder::findFiles('*.txt')
->exclude('*X*');
La máscara de exclusión usa exactamente la misma gramática que las de búsqueda: los mismos Comodines, el anclaje ./ y el atajo **. Su parte final decide el alcance de la
exclusión:
| Máscara | Excluye |
|---|---|
temp |
cualquier archivo o directorio llamado temp, a cualquier profundidad |
temp/ |
solo un directorio temp (y su contenido); un archivo llamado temp se conserva |
temp/* |
el contenido de temp, pero conserva el propio directorio temp |
temp/** |
lo mismo que temp/* |
En un directorio excluido ni siquiera se entra durante el recorrido, así que excluir subárboles enteros acelera además la búsqueda. Así es como se saltan subdirectorios concretos:
Finder::findFiles('*.php')
->from($dir)
->exclude('temp', '.git');
Filtrado
Finder ofrece varios métodos para filtrar los resultados (es decir, reducirlos). Se pueden combinar y llamar repetidamente.
Con size() filtramos por el tamaño del archivo. Así encontramos archivos de un tamaño de entre 100 y
200 bytes:
Finder::findFiles('*.php')
->size('>=', 100)
->size('<=', 200);
El método date() filtra por la fecha de última modificación del archivo. Los valores pueden ser fechas
absolutas o relativas a la fecha y hora actuales. Por ejemplo, esto encuentra los archivos modificados en las últimas dos
semanas:
Finder::findFiles('*.php')
->date('>', '-2 weeks')
->from($dir)
Ambos métodos entienden los operadores >, >=, <, <=,
=, !=, <>.
Finder permite además filtrar los resultados con callbacks propios. El callback recibe como parámetro un objeto
Nette\Utils\FileInfo y debe devolver true para que el archivo se incluya en los resultados.
Ejemplo: buscar archivos PHP que contengan la cadena 'Nette' (sin distinguir mayúsculas):
Finder::findFiles('*.php')
->filter(fn($file) => str_contains(strtolower($file->read()), 'nette'));
Filtrado por profundidad
Al buscar de forma recursiva puede fijar la profundidad máxima del recorrido con el método limitDepth(). Poner
limitDepth(1) recorre solo el primer nivel de subdirectorios, limitDepth(0) desactiva por completo el
descenso en profundidad, y el valor –1 elimina el límite de profundidad.
Finder permite usar callbacks propios para decidir en qué directorios entrar durante el recorrido. El callback recibe un
objeto Nette\Utils\FileInfo que representa el directorio y debe devolver true para entrar en él:
Finder::findFiles('*.php')
->descentFilter(fn($file) => $file->getBasename() !== 'temp');
Directorios ilegibles
De forma predeterminada, Finder se salta los directorios que no puede leer (por ejemplo, por permisos insuficientes). Si
prefiere que lance una excepción en esos casos, llame a ignoreUnreadableDirs(false).
Finder::findFiles('*.php')
->from($dir)
->ignoreUnreadableDirs(false);
Ordenación
Finder ofrece también varios métodos para ordenar los resultados.
El método sortByName() ordena los resultados por el nombre del archivo. La ordenación es natural, es decir,
trata correctamente los números de los nombres y devuelve, por ejemplo, foo1.txt antes que
foo10.txt.
Finder permite también ordenar con un callback propio. Este recibe como parámetros dos objetos
Nette\Utils\FileInfo y debe devolver el resultado de la comparación con el operador <=> (es
decir, -1, 0 o 1). Así ordenamos, por ejemplo, los archivos por tamaño:
$finder->sortBy(fn($a, $b) => $a->getSize() <=> $b->getSize());
Varias búsquedas distintas
Si necesita encontrar varios conjuntos de archivos en lugares distintos o que cumplan criterios distintos, use el método
append(). Devuelve un nuevo objeto Finder, lo que le permite encadenar las llamadas de la búsqueda
añadida:
($finder = new Finder) // ¡guarde el primer Finder en la variable $finder!
->files('*.php') // busca archivos *.php en src/
->from('src')
->append()
->files('*.md') // en docs/ busca archivos *.md
->from('docs')
->append()
->files('*.json'); // en la carpeta actual busca archivos *.json
Como alternativa, el método append() se puede usar para añadir un archivo concreto (o un array de archivos). En
ese caso devuelve el mismo objeto Finder:
$finder = Finder::findFiles('*.txt')
->append(__FILE__);
FileInfo
Nette\Utils\FileInfo es una clase que representa un archivo o directorio encontrado en los resultados de la búsqueda. Extiende la clase SplFileInfo y ofrece información como el tamaño del archivo, la fecha de última modificación, el nombre, la ruta, etc.
Ofrece además métodos para devolver la ruta relativa, algo útil durante el recorrido recursivo:
foreach (Finder::findFiles('*.jpg')->from('.') as $file) {
$absoluteFilePath = $file->getRealPath();
$relativeFilePath = $file->getRelativePathname();
}
También dispone de métodos para leer y escribir el contenido del archivo:
foreach ($finder as $file) {
$contents = $file->read();
// ...
$file->write($contents);
}
Devolver los resultados como array
Como se ve en los ejemplos, Finder implementa la interfaz IteratorAggregate, así que puede usar
foreach para recorrer los resultados. Está diseñado de modo que los resultados se cargan solo durante la
iteración, lo que significa que si tiene un gran número de archivos no espera a leerlos todos de antemano.
También puede obtener los resultados como un array de objetos Nette\Utils\FileInfo con el método
collect(). El array está indexado numéricamente, no de forma asociativa.
$array = Finder::findFiles('*.php')->collect();