Finder : recherche de fichiers

Vous cherchez des fichiers correspondant à un certain masque ? Finder peut vous aider. C'est un outil polyvalent et rapide pour parcourir des arborescences de répertoires.

Installation :

composer require nette/utils

Les exemples supposent que l'alias de classe suivant a été créé :

use Nette\Utils\Finder;

Utilisation

Voyons d'abord comment utiliser Nette\Utils\Finder pour lister les noms des fichiers portant les extensions .txt et .md dans le répertoire courant :

foreach (Finder::findFiles(['*.txt', '*.md']) as $name => $file) {
	echo $file;
}

Le répertoire de recherche par défaut est le répertoire courant, mais vous pouvez le changer à l'aide des méthodes in() ou from(). La variable $file est une instance de la classe FileInfo, tandis que $name est une chaîne contenant le chemin du fichier.

Le chemin est retourné tel que vous l'avez écrit, avec les séparateurs de la plate-forme ; sous Windows, le résultat peut donc mélanger / et \. Appelez FileSystem::unixSlashes() s'il vous faut une forme uniforme.

Que rechercher ?

En plus de la méthode findFiles(), il existe findDirectories(), qui ne cherche que les répertoires, et find(), qui cherche les deux. Ces méthodes sont statiques, elles peuvent donc être appelées sans créer d'instance. L'argument du masque est facultatif ; s'il est omis, tout correspond.

foreach (Finder::find() as $file) {
	echo $file; // tous les fichiers et répertoires sont maintenant listés
}

Les méthodes files() et directories() permettent d'indiquer d'autres éléments à rechercher. Elles peuvent être appelées plusieurs fois et acceptent aussi un tableau de masques en argument :

Finder::findDirectories('vendor') // tous les répertoires
	->files(['*.php', '*.phpt']); // plus tous les fichiers PHP

Une alternative aux méthodes statiques consiste à créer une instance avec new Finder (un objet ainsi créé ne cherche d'abord rien) et à indiquer quoi chercher avec files() et directories() :

(new Finder)
	->directories()      // tous les répertoires
	->files('*.php');    // plus tous les fichiers PHP

Dans le masque, vous pouvez utiliser les caractères génériques *, **, ? et [...]. Vous pouvez même indiquer des répertoires : src/*.php trouve par exemple tous les fichiers PHP du répertoire src. Les liens symboliques sont eux aussi traités comme des répertoires ou des fichiers.

Où rechercher ?

Le répertoire de recherche par défaut est le répertoire courant. Vous le changez avec les méthodes in() et from() :

Finder::findFiles('*.php')
	->in(['src', 'tests']) // recherche directement dans src/ et tests/
	->from('vendor');      // recherche aussi dans les sous-répertoires de vendor/

Les deux méthodes diffèrent par la profondeur : in() ne cherche que dans le répertoire indiqué, tandis que from() descend aussi dans ses sous-répertoires (récursivement). Pour parcourir récursivement le répertoire courant, utilisez from('.').

La récursivité n'est toutefois pas décidée par from() seule : le caractère générique ** dans le masque la pilote également, si bien que findFiles('**/*.php')->in('src') cherche aussi récursivement. Autrement dit, from('src') n'est qu'un raccourci pour in('src') avec un masque récursif. Voyez Caractères génériques.

Ces méthodes peuvent être appelées plusieurs fois, ou vous pouvez leur passer plusieurs chemins sous forme de tableau ; les fichiers seront alors cherchés dans tous les répertoires indiqués. Si l'un des répertoires n'existe pas, une Nette\InvalidStateException est levée.

Les chemins relatifs le sont par rapport au répertoire courant, mais les chemins absolus sont également possibles :

Finder::findFiles('*.php')
	->in('/var/www/html');

Dans le chemin, vous pouvez utiliser les caractères génériques *, ** et ?, mais pas [...], qui y est pris au pied de la lettre. Cela évite les comportements inattendus quand vous cherchez par exemple in(__DIR__) et que le chemin contient des caractères []. Ainsi, src/*/*.php cherche tous les fichiers PHP situés dans les répertoires de deuxième niveau sous src.

Lors d'une recherche récursive de fichiers et de répertoires (en profondeur d'abord), le répertoire parent est retourné en premier, suivi des fichiers qu'il contient. Cet ordre peut être inversé avec childFirst().

Caractères génériques

Un masque peut contenir plusieurs caractères spéciaux :

  • * – un nombre quelconque de caractères, sauf le séparateur / (il reste à un seul niveau de répertoire)
  • ** – un nombre quelconque de caractères, y compris / (il traverse les niveaux de répertoires, voir ci-dessous)
  • ? – exactement un caractère, sauf /
  • [a-z] – un caractère de l'intervalle ou de l'ensemble indiqué entre crochets
  • [!a-z] – un caractère absent des crochets

Le point capital, facile à manquer : ** correspond à zéro ou plusieurs niveaux de répertoires. Il ne signifie pas “au moins un sous-répertoire”. src/**/*.php correspond donc aussi bien à un fichier placé directement dans src qu'à un fichier enfoui plusieurs niveaux plus bas. Prenons cette arborescence :

src/
├── app.php
├── Model/
│   ├── User.php
│   └── Repository/
│       └── UserRepository.php
└── Control/
    └── SignForm.php

Le tableau suivant montre ce à quoi correspondent les différents masques, pour les fichiers comme pour les répertoires :

Masque Correspond à
src/*.php uniquement src/app.php (directement dans src)
src/**/*.php src/app.php, src/Model/User.php, src/Model/Repository/UserRepository.php (tous les niveaux, y compris directement dans src)
src/* les enfants directs de src : app.php, ModelControl
src/** tout ce qui se trouve sous src, fichiers et répertoires (raccourci pour src/**/*)
src/*/ les sous-répertoires directs de src : ModelControl
src/**/ tous les sous-répertoires, à toute profondeur : Model, Model/RepositoryControl

Deux raccourcis méritent d'être retenus :

  • Un ** qui n'est pas immédiatement suivi de / se comporte comme **/ plus *. Ainsi, src/** est un raccourci pour src/**/*, et **.php pour **/*.php.
  • Une barre oblique finale restreint le masque aux seuls répertoires. Ainsi, find('log/') retourne les répertoires nommés log, mais jamais un fichier de ce nom. (findFiles() refuse une barre oblique finale, chercher un fichier “répertoire” n'ayant aucun sens.)

Autres exemples d'utilisation :

  • img/?.png – fichiers dont le nom tient en une lettre, comme 0.png, 1.pngx.png
  • logs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log – fichiers de log au format YYYY-MM-DD
  • docs/**/*.md – tous les fichiers portant l'extension .md dans docs et tous ses sous-répertoires

Exclusion

La méthode exclude() permet d'écarter des fichiers et des répertoires des résultats. L'argument est un masque auquel l'élément ne doit pas correspondre. Ici, nous cherchons les fichiers *.txt, à l'exception de ceux dont le nom contient la lettre X :

Finder::findFiles('*.txt')
	->exclude('*X*');

Le masque d'exclusion suit exactement la même grammaire que les masques de recherche : les mêmes caractères génériques, l'ancrage ./ et le raccourci **. Sa partie finale décide de la portée de l'exclusion :

Masque Exclut
temp tout fichier ou répertoire nommé temp, à toute profondeur
temp/ uniquement un répertoire temp (et son contenu) ; un fichier nommé temp est conservé
temp/* le contenu de temp, mais garde le répertoire temp lui-même
temp/** la même chose que temp/*

Un répertoire exclu n'est même pas parcouru, si bien qu'exclure des sous-arbres entiers accélère aussi la recherche. C'est ainsi que vous sautez des sous-répertoires précis :

Finder::findFiles('*.php')
	->from($dir)
	->exclude('temp', '.git');

Filtrage

Finder propose plusieurs méthodes pour filtrer les résultats (autrement dit, les réduire). Vous pouvez les combiner et les appeler plusieurs fois.

Avec size(), nous filtrons par taille de fichier. Nous trouvons ainsi les fichiers dont la taille est comprise entre 100 et 200 octets :

Finder::findFiles('*.php')
	->size('>=', 100)
	->size('<=', 200);

La méthode date() filtre par date de dernière modification du fichier. Les valeurs peuvent être des dates absolues ou relatives à la date et à l'heure courantes. Ceci trouve par exemple les fichiers modifiés au cours des deux dernières semaines :

Finder::findFiles('*.php')
	->date('>', '-2 weeks')
	->from($dir)

Les deux méthodes comprennent les opérateurs >, >=, <, <=, =, !=, <>.

Finder permet aussi de filtrer les résultats à l'aide de callbacks personnalisés. Le callback reçoit un objet Nette\Utils\FileInfo en paramètre et doit retourner true pour que le fichier figure dans les résultats.

Exemple : recherche des fichiers PHP contenant la chaîne 'Nette' (sans distinction de casse) :

Finder::findFiles('*.php')
	->filter(fn($file) => str_contains(strtolower($file->read()), 'nette'));

Filtrage en profondeur

Lors d'une recherche récursive, vous pouvez fixer la profondeur maximale de parcours avec la méthode limitDepth(). limitDepth(1) ne parcourt que le premier niveau de sous-répertoires, limitDepth(0) désactive entièrement le parcours en profondeur, et la valeur –1 supprime la limite.

Finder permet d'utiliser des callbacks personnalisés pour décider dans quels répertoires entrer pendant le parcours. Le callback reçoit un objet Nette\Utils\FileInfo représentant le répertoire et doit retourner true pour y entrer :

Finder::findFiles('*.php')
	->descentFilter(fn($file) => $file->getBasename() !== 'temp');

Répertoires illisibles

Par défaut, Finder passe les répertoires qu'il ne peut pas lire (faute de droits suffisants, par exemple). Si vous préférez qu'il lève une exception dans ces cas-là, appelez ignoreUnreadableDirs(false).

Finder::findFiles('*.php')
	->from($dir)
	->ignoreUnreadableDirs(false);

Tri

Finder offre également plusieurs méthodes pour trier les résultats.

La méthode sortByName() trie les résultats par nom de fichier. Le tri est naturel : il traite correctement les nombres dans les noms et retourne par exemple foo1.txt avant foo10.txt.

Finder permet aussi de trier à l'aide d'un callback personnalisé. Celui-ci reçoit deux objets Nette\Utils\FileInfo en paramètres et doit retourner le résultat de la comparaison avec l'opérateur <=> (soit -1, 0 ou 1). Voici par exemple comment trier les fichiers par taille :

$finder->sortBy(fn($a, $b) => $a->getSize() <=> $b->getSize());

Plusieurs recherches différentes

Si vous avez besoin de trouver plusieurs ensembles de fichiers à des emplacements différents ou répondant à des critères différents, utilisez la méthode append(). Elle retourne un nouvel objet Finder, ce qui permet d'enchaîner les appels de méthodes pour la recherche ajoutée :

($finder = new Finder) // stockez le premier Finder dans la variable $finder !
	->files('*.php')   // cherche les fichiers *.php dans src/
	->from('src')
	->append()
	->files('*.md')    // dans docs/, cherche les fichiers *.md
	->from('docs')
	->append()
	->files('*.json'); // dans le répertoire courant, cherche les fichiers *.json

La méthode append() peut aussi servir à ajouter un fichier précis (ou un tableau de fichiers). Dans ce cas, elle retourne le même objet Finder :

$finder = Finder::findFiles('*.txt')
	->append(__FILE__);

FileInfo

Nette\Utils\FileInfo est une classe représentant un fichier ou un répertoire trouvé dans les résultats de la recherche. Elle étend la classe SplFileInfo et fournit des informations comme la taille du fichier, la date de dernière modification, le nom, le chemin, etc.

Elle fournit en outre des méthodes retournant le chemin relatif, ce qui est pratique lors d'un parcours récursif :

foreach (Finder::findFiles('*.jpg')->from('.') as $file) {
	$absoluteFilePath = $file->getRealPath();
	$relativeFilePath = $file->getRelativePathname();
}

Des méthodes de lecture et d'écriture du contenu du fichier sont également disponibles :

foreach ($finder as $file) {
    $contents = $file->read();
    // ...
    $file->write($contents);
}

Retour des résultats sous forme de tableau

Comme le montrent les exemples, Finder implémente l'interface IteratorAggregate : vous pouvez donc parcourir les résultats avec foreach. Il est conçu pour ne charger les résultats que pendant l'itération, si bien qu'avec un grand nombre de fichiers, il n'attend pas de les avoir tous lus au préalable.

Vous pouvez aussi obtenir les résultats sous forme de tableau d'objets Nette\Utils\FileInfo avec la méthode collect(). Le tableau est indexé numériquement, pas associativement.

$array = Finder::findFiles('*.php')->collect();
version: 4.x