Finder: cercare i file
Avete bisogno di trovare i file che corrispondono a una certa maschera? Finder può aiutarvi. È uno strumento versatile e veloce per esplorare le strutture di directory.
Installazione:
composer require nette/utils
Gli esempi presuppongono che sia stato creato questo alias di classe:
use Nette\Utils\Finder;
Uso
Vediamo per prima cosa come usare Nette\Utils\Finder
per elencare i nomi dei file con estensione .txt e .md nella directory corrente:
foreach (Finder::findFiles(['*.txt', '*.md']) as $name => $file) {
echo $file;
}
La directory di ricerca predefinita è quella corrente, ma potete cambiarla con i metodi in()
o from(). La variabile $file è un'istanza della classe FileInfo, mentre
$name è una stringa che contiene il percorso del file.
Il percorso viene restituito così come lo avete scritto, conservando i separatori della piattaforma; su Windows il risultato
può quindi mescolare / e \. Chiamate FileSystem::unixSlashes() se vi serve una forma
uniforme.
Cosa cercare?
Oltre al metodo findFiles() esistono findDirectories(), che cerca solo le directory, e
find(), che cerca entrambi. Questi metodi sono statici, quindi si possono chiamare senza creare un'istanza.
L'argomento con la maschera è facoltativo; se lo omettete, viene trovato tutto.
foreach (Finder::find() as $file) {
echo $file; // ora vengono elencati tutti i file e tutte le directory
}
Con i metodi files() e directories() potete indicare altri elementi da cercare. I metodi si possono
chiamare più volte e come argomento si può passare anche un array di maschere:
Finder::findDirectories('vendor') // tutte le directory
->files(['*.php', '*.phpt']); // più tutti i file PHP
Un'alternativa ai metodi statici è creare un'istanza con new Finder (un oggetto appena creato così all'inizio
non cerca nulla) e indicare cosa cercare con files() e directories():
(new Finder)
->directories() // tutte le directory
->files('*.php'); // più tutti i file PHP
Nella maschera potete usare i caratteri jolly come *, **,
? e [...]. Potete indicare anche delle directory: per esempio src/*.php trova tutti i file
PHP nella directory src. Anche i link simbolici vengono trattati come directory o file.
Dove cercare?
La directory di ricerca predefinita è quella corrente. La cambiate con i metodi in() e from():
Finder::findFiles('*.php')
->in(['src', 'tests']) // cerca direttamente in src/ e tests/
->from('vendor'); // cerca anche nelle sottodirectory di vendor/
I due metodi differiscono per profondità: in() cerca solo dentro la directory indicata, mentre
from() scende anche nelle sue sottodirectory (ricorsivamente). Per cercare ricorsivamente nella directory corrente
usate from('.').
La ricorsione non è però decisa solo da from(): la guida anche il carattere jolly ** nella
maschera, quindi anche findFiles('**/*.php')->in('src') cerca ricorsivamente. In altre parole,
from('src') è solo una scorciatoia per in('src') con una maschera ricorsiva. Vedi Caratteri jolly.
Questi metodi si possono chiamare più volte, oppure potete passare più percorsi come array; i file verranno allora cercati
in tutte le directory indicate. Se una delle directory non esiste, viene sollevata Nette\InvalidStateException.
I percorsi relativi sono relativi alla directory corrente, ma si possono usare anche percorsi assoluti:
Finder::findFiles('*.php')
->in('/var/www/html');
Nel percorso potete usare i caratteri jolly *, ** e ?, ma non
[...], che lì viene preso alla lettera. Questo evita comportamenti indesiderati quando, per esempio, cercate con
in(__DIR__) e il percorso contiene per caso i caratteri []. Per esempio src/*/*.php cerca
tutti i file PHP nelle directory di secondo livello sotto src.
Cercando file e directory in modo ricorsivo (in profondità), viene restituita prima la directory superiore e poi i file che
contiene. Questo ordine si può invertire con childFirst().
Caratteri jolly
Una maschera può contenere diversi caratteri speciali:
*– un numero qualsiasi di caratteri, tranne il separatore/(resta all'interno di un solo livello di directory)**– un numero qualsiasi di caratteri,/compreso (attraversa i livelli di directory, vedi sotto)?– esattamente un carattere, tranne/[a-z]– un carattere dell'intervallo o dell'insieme indicato tra parentesi[!a-z]– un carattere non presente tra parentesi
Il punto essenziale, e facile da trascurare: ** corrisponde a zero o più livelli di directory. Non
significa “almeno una sottodirectory”. Perciò src/**/*.php trova un file collocato direttamente in
src esattamente come uno sepolto a diversi livelli di profondità. Consideriamo questo albero:
src/
├── app.php
├── Model/
│ ├── User.php
│ └── Repository/
│ └── UserRepository.php
└── Control/
└── SignForm.php
La tabella seguente mostra cosa trovano le singole maschere, sia per i file sia per le directory:
| Maschera | Trova |
|---|---|
src/*.php |
solo src/app.php (direttamente in src) |
src/**/*.php |
src/app.php, src/Model/User.php, src/Model/Repository/UserRepository.php (tutti
i livelli, compreso direttamente in src) |
src/* |
i figli diretti di src: app.php, Model, Control |
src/** |
tutto ciò che si trova sotto src, file e directory (forma abbreviata di src/**/*) |
src/*/ |
le sottodirectory dirette di src: Model, Control |
src/**/ |
tutte le sottodirectory a qualsiasi profondità: Model,
Model/Repository, Control |
Vale la pena ricordare due scorciatoie:
- Un
**non seguito immediatamente da/si comporta come**/più*. Quindisrc/**è una forma abbreviata disrc/**/*e**.phpdi**/*.php. - Una barra finale limita la maschera alle sole directory. Quindi
find('log/')restituisce le directory chiamatelog, ma mai un file con quel nome. (findFiles()rifiuta la barra finale, perché cercare un file “directory” non ha senso.)
Altri esempi d'uso:
img/?.png– file con un nome di una sola lettera, come0.png,1.png,x.pnglogs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log– file di log nel formatoYYYY-MM-DDdocs/**/*.md– tutti i file con estensione.mdindocse in tutte le sue sottodirectory
Esclusione
Usate il metodo exclude() per togliere file e directory dai risultati. L'argomento è una maschera alla quale
l'elemento non deve corrispondere. Qui cerchiamo i file *.txt tranne quelli che contengono la lettera
X nel nome:
Finder::findFiles('*.txt')
->exclude('*X*');
La maschera di esclusione usa la stessa identica grammatica delle maschere di ricerca: gli stessi caratteri jolly, l'ancoraggio ./ e la scorciatoia **. La sua parte
finale decide l'ampiezza dell'esclusione:
| Maschera | Esclude |
|---|---|
temp |
qualsiasi file o directory chiamati temp, a qualsiasi profondità |
temp/ |
solo una directory temp (e il suo contenuto); un file chiamato temp viene mantenuto |
temp/* |
il contenuto di temp, ma mantiene la directory temp stessa |
temp/** |
lo stesso di temp/* |
Una directory esclusa non viene nemmeno percorsa durante l'attraversamento, quindi escludere interi sottoalberi velocizza anche la ricerca. Ecco come saltare determinate sottodirectory:
Finder::findFiles('*.php')
->from($dir)
->exclude('temp', '.git');
Filtraggio
Finder offre diversi metodi per filtrare i risultati (cioè per ridurli). Potete combinarli e chiamarli più volte.
Con size() filtriamo per dimensione del file. In questo modo troviamo i file di dimensione compresa tra 100 e
200 byte:
Finder::findFiles('*.php')
->size('>=', 100)
->size('<=', 200);
Il metodo date() filtra per data dell'ultima modifica del file. I valori possono essere date assolute oppure
relative alla data e all'ora correnti. Per esempio, così troviamo i file modificati nelle ultime due settimane:
Finder::findFiles('*.php')
->date('>', '-2 weeks')
->from($dir)
Entrambi i metodi comprendono gli operatori >, >=, <, <=,
=, !=, <>.
Finder permette anche di filtrare i risultati con callback personalizzate. La callback riceve come parametro un oggetto
Nette\Utils\FileInfo e deve restituire true perché il file venga incluso nei risultati.
Esempio: cercare i file PHP che contengono la stringa 'Nette' (senza distinguere maiuscole e minuscole):
Finder::findFiles('*.php')
->filter(fn($file) => str_contains(strtolower($file->read()), 'nette'));
Filtrare per profondità
Cercando ricorsivamente potete impostare la profondità massima di attraversamento con il metodo limitDepth().
Impostando limitDepth(1) viene percorso solo il primo livello di sottodirectory, limitDepth(0) disattiva
del tutto l'attraversamento in profondità e il valore –1 toglie il limite di profondità.
Finder permette di usare callback personalizzate per decidere in quali directory entrare durante l'attraversamento. La callback
riceve un oggetto Nette\Utils\FileInfo che rappresenta la directory e deve restituire true perché vi
si entri:
Finder::findFiles('*.php')
->descentFilter(fn($file) => $file->getBasename() !== 'temp');
Directory non leggibili
Per impostazione predefinita Finder salta le directory che non riesce a leggere (per esempio per permessi insufficienti). Se
preferite che in questi casi sollevi un'eccezione, chiamate ignoreUnreadableDirs(false).
Finder::findFiles('*.php')
->from($dir)
->ignoreUnreadableDirs(false);
Ordinamento
Finder offre anche diversi metodi per ordinare i risultati.
Il metodo sortByName() ordina i risultati per nome del file. L'ordinamento è naturale, cioè gestisce
correttamente i numeri nei nomi e restituisce per esempio foo1.txt prima di foo10.txt.
Finder permette anche di ordinare con una callback personalizzata. Riceve come parametri due oggetti
Nette\Utils\FileInfo e deve restituire il risultato del confronto con l'operatore <=> (cioè
-1, 0 o 1). Ecco per esempio come ordiniamo i file per dimensione:
$finder->sortBy(fn($a, $b) => $a->getSize() <=> $b->getSize());
Più ricerche diverse
Se avete bisogno di trovare più insiemi di file in posizioni diverse o secondo criteri diversi, usate il metodo
append(). Restituisce un nuovo oggetto Finder, che vi permette di concatenare le chiamate ai metodi
della ricerca aggiunta:
($finder = new Finder) // salvate il primo Finder nella variabile $finder!
->files('*.php') // cerca i file *.php in src/
->from('src')
->append()
->files('*.md') // in docs/ cerca i file *.md
->from('docs')
->append()
->files('*.json'); // nella cartella corrente cerca i file *.json
In alternativa il metodo append() si può usare per aggiungere un file specifico (o un array di file). In tal
caso restituisce lo stesso oggetto Finder:
$finder = Finder::findFiles('*.txt')
->append(__FILE__);
FileInfo
Nette\Utils\FileInfo è una classe che rappresenta un file o una directory trovati nei risultati della ricerca. Estende la classe SplFileInfo e offre informazioni come la dimensione del file, la data dell'ultima modifica, il nome, il percorso e così via.
Offre inoltre metodi per restituire il percorso relativo, cosa utile durante l'attraversamento ricorsivo:
foreach (Finder::findFiles('*.jpg')->from('.') as $file) {
$absoluteFilePath = $file->getRealPath();
$relativeFilePath = $file->getRelativePathname();
}
Sono disponibili anche metodi per leggere e scrivere il contenuto del file:
foreach ($finder as $file) {
$contents = $file->read();
// ...
$file->write($contents);
}
Restituire i risultati come array
Come si vede negli esempi, Finder implementa l'interfaccia IteratorAggregate, quindi potete usare
foreach per scorrere i risultati. È progettato in modo che i risultati vengano caricati solo durante l'iterazione:
se avete un gran numero di file, non aspetta quindi che siano stati letti tutti in anticipo.
Potete anche ottenere i risultati come array di oggetti Nette\Utils\FileInfo con il metodo
collect(). L'array ha indici numerici, non associativi.
$array = Finder::findFiles('*.php')->collect();