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, ModelControl
src/** todo lo que hay bajo src, archivos y directorios (atajo de src/**/*)
src/*/ los subdirectorios directos de src: ModelControl
src/**/ todos los subdirectorios a cualquier profundidad: Model, Model/RepositoryControl

Conviene recordar dos atajos:

  • Un ** que no vaya seguido inmediatamente de / se comporta como **/ más *. Así, src/** es un atajo de src/**/*, y **.php lo es de **/*.php.
  • Una barra final restringe la máscara solo a directorios. Así, find('log/') devuelve directorios llamados log, 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, como 0.png, 1.pngx.png
  • logs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log – archivos de registro en formato YYYY-MM-DD
  • docs/**/*.md – todos los archivos con extensión .md en docs y 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();
versión: 4.x