Finder: wyszukiwanie plików
Potrzebujesz znaleźć pliki pasujące do określonej maski? Finder Ci w tym pomoże. To wszechstronne i szybkie narzędzie do przeglądania struktury katalogów.
Instalacja:
composer require nette/utils
Przykłady zakładają, że utworzony został następujący alias klasy:
use Nette\Utils\Finder;
Użycie
Najpierw zobaczmy, jak za pomocą Nette\Utils\Finder
wypisać nazwy plików z rozszerzeniami .txt i .md w bieżącym katalogu:
foreach (Finder::findFiles(['*.txt', '*.md']) as $name => $file) {
echo $file;
}
Domyślnym katalogiem wyszukiwania jest katalog bieżący, ale możesz go zmienić metodami in()
albo from(). Zmienna $file to instancja klasy FileInfo, a $name to
string ze ścieżką do pliku.
Ścieżka zwracana jest tak, jak ją zapisałeś, z zachowaniem separatorów platformy; w Windows wynik może więc mieszać
/ i \. Jeśli potrzebujesz jednolitej postaci, wywołaj FileSystem::unixSlashes().
Czego szukać?
Oprócz metody findFiles() istnieje findDirectories(), która szuka wyłącznie katalogów, oraz
find(), która szuka jednego i drugiego. Metody te są statyczne, można je więc wywoływać bez tworzenia
instancji. Argument z maską jest opcjonalny; jeśli go pominiesz, pasuje wszystko.
foreach (Finder::find() as $file) {
echo $file; // teraz wypisywane są wszystkie pliki i katalogi
}
Za pomocą metod files() i directories() możesz określić kolejne rzeczy do wyszukania. Metody
można wywoływać wielokrotnie, a jako argument można podać także tablicę masek:
Finder::findDirectories('vendor') // wszystkie katalogi
->files(['*.php', '*.phpt']); // plus wszystkie pliki PHP
Alternatywą dla metod statycznych jest utworzenie instancji przez new Finder (tak utworzony obiekt początkowo
niczego nie szuka) i określenie, czego szukać, metodami files() i directories():
(new Finder)
->directories() // wszystkie katalogi
->files('*.php'); // plus wszystkie pliki PHP
W masce możesz używać symboli wieloznacznych, takich jak *,
**, ? i [...]. Możesz podać nawet katalogi, na przykład src/*.php znajdzie
wszystkie pliki PHP w katalogu src. Dowiązania symboliczne również traktowane są jak katalogi albo pliki.
Gdzie szukać?
Domyślnym katalogiem wyszukiwania jest katalog bieżący. Zmieniasz go metodami in() i from():
Finder::findFiles('*.php')
->in(['src', 'tests']) // szuka bezpośrednio w src/ i tests/
->from('vendor'); // szuka też w podkatalogach vendor/
Te dwie metody różnią się głębokością: in() szuka tylko w podanym katalogu, natomiast from()
schodzi również do jego podkatalogów (rekurencyjnie). Aby przeszukać bieżący katalog rekurencyjnie, użyj
from('.').
O rekurencji nie decyduje jednak samo from() – napędza ją też symbol ** w masce, więc
findFiles('**/*.php')->in('src') również szuka rekurencyjnie. Inaczej mówiąc, from('src') to
jedynie skrót dla in('src') z maską rekurencyjną. Zobacz Symbole
wieloznaczne.
Metody te można wywoływać wielokrotnie albo przekazać kilka ścieżek jako tablicę; pliki będą wtedy wyszukiwane we
wszystkich podanych katalogach. Jeśli któryś z katalogów nie istnieje, zgłaszany jest
Nette\InvalidStateException.
Ścieżki względne odnoszą się do bieżącego katalogu, ale można używać też ścieżek bezwzględnych:
Finder::findFiles('*.php')
->in('/var/www/html');
W ścieżce możesz używać symboli *, ** i ?, ale nie [...],
które traktowane jest tam dosłownie. Zapobiega to niezamierzonemu zachowaniu, gdy na przykład szukasz in(__DIR__),
a ścieżka akurat zawiera znaki []. Na przykład src/*/*.php szuka wszystkich plików PHP w katalogach
drugiego poziomu pod src.
Przy rekurencyjnym wyszukiwaniu plików i katalogów (w głąb) najpierw zwracany jest katalog nadrzędny, a po nim zawarte w
nim pliki. Kolejność tę można odwrócić metodą childFirst().
Symbole wieloznaczne
Maska może zawierać kilka znaków specjalnych:
*– dowolna liczba znaków z wyjątkiem separatora/(pozostaje na jednym poziomie katalogów)**– dowolna liczba znaków, łącznie z/(obejmuje poziomy katalogów, zobacz niżej)?– dokładnie jeden znak z wyjątkiem/[a-z]– jeden znak z zakresu albo zbioru w nawiasach[!a-z]– jeden znak spoza nawiasów
Rzecz kluczowa i łatwa do przeoczenia: ** pasuje do zera lub więcej poziomów katalogów. Nie
oznacza “co najmniej jeden podkatalog”. Dlatego src/**/*.php pasuje zarówno do pliku leżącego bezpośrednio w
src, jak i do zakopanego kilka poziomów głębiej. Rozważ takie drzewo:
src/
├── app.php
├── Model/
│ ├── User.php
│ └── Repository/
│ └── UserRepository.php
└── Control/
└── SignForm.php
Poniższa tabela pokazuje, do czego pasują poszczególne maski, zarówno dla plików, jak i katalogów:
| Maska | Pasuje do |
|---|---|
src/*.php |
tylko src/app.php (bezpośrednio w src) |
src/**/*.php |
src/app.php, src/Model/User.php, src/Model/Repository/UserRepository.php (wszystkie
poziomy, w tym bezpośrednio w src) |
src/* |
bezpośrednie dzieci src: app.php, Model, Control |
src/** |
wszystko pod src, pliki i katalogi (skrót dla src/**/*) |
src/*/ |
bezpośrednie podkatalogi src: Model, Control |
src/**/ |
wszystkie podkatalogi na dowolnej głębokości: Model,
Model/Repository, Control |
Warto zapamiętać dwa skróty:
**, po którym nie następuje bezpośrednio/, zachowuje się jak**/plus*. Zatemsrc/**to skrót dlasrc/**/*, a**.phpdla**/*.php.- Ukośnik na końcu ogranicza maskę wyłącznie do katalogów. Zatem
find('log/')zwraca katalogi o nazwielog, ale nigdy pliku o tej nazwie. (findFiles()odrzuca końcowy ukośnik, bo szukanie pliku “katalogu” nie ma sensu.)
Kolejne przykłady użycia:
img/?.png– pliki o jednoliterowej nazwie, jak0.png,1.png,x.pnglogs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log– pliki logów w formacieYYYY-MM-DDdocs/**/*.md– wszystkie pliki z rozszerzeniem.mdwdocsi wszystkich jego podkatalogach
Wykluczanie
Metody exclude() używa się do usunięcia plików i katalogów z wyniku. Argumentem jest maska, do której
element nie może pasować. Tutaj szukamy plików *.txt z wyjątkiem tych, które zawierają w nazwie literę
X:
Finder::findFiles('*.txt')
->exclude('*X*');
Maska wykluczająca używa dokładnie tej samej gramatyki co maski wyszukiwania – tych samych symboli wieloznacznych, kotwiczenia ./ i skrótu **. O zakresie
wykluczenia decyduje jej końcowa część:
| Maska | Wyklucza |
|---|---|
temp |
dowolny plik albo katalog o nazwie temp, na dowolnej głębokości |
temp/ |
tylko katalog temp (i jego zawartość); plik o nazwie temp zostaje zachowany |
temp/* |
zawartość temp, ale zachowuje sam katalog temp |
temp/** |
to samo co temp/* |
Do wykluczonego katalogu Finder w ogóle nie wchodzi podczas przechodzenia, więc wykluczanie całych poddrzew również przyspiesza wyszukiwanie. W ten sposób pomijasz konkretne podkatalogi:
Finder::findFiles('*.php')
->from($dir)
->exclude('temp', '.git');
Filtrowanie
Finder oferuje kilka metod do filtrowania wyników (czyli ich zawężania). Można je łączyć i wywoływać wielokrotnie.
Za pomocą size() filtrujemy według rozmiaru pliku. W ten sposób znajdziemy pliki o rozmiarze z zakresu od
100 do 200 bajtów:
Finder::findFiles('*.php')
->size('>=', 100)
->size('<=', 200);
Metoda date() filtruje według daty ostatniej modyfikacji pliku. Wartościami mogą być daty bezwzględne albo
względne wobec bieżącej daty i czasu. Na przykład to znajdzie pliki zmodyfikowane w ciągu ostatnich dwóch tygodni:
Finder::findFiles('*.php')
->date('>', '-2 weeks')
->from($dir)
Obie metody rozumieją operatory >, >=, <, <=, =,
!=, <>.
Finder pozwala też filtrować wyniki własnymi callbackami. Callback otrzymuje jako parametr obiekt
Nette\Utils\FileInfo i musi zwrócić true, aby plik trafił do wyniku.
Przykład: wyszukiwanie plików PHP zawierających string 'Nette' (bez rozróżniania wielkości liter):
Finder::findFiles('*.php')
->filter(fn($file) => str_contains(strtolower($file->read()), 'nette'));
Filtrowanie według głębokości
Przy wyszukiwaniu rekurencyjnym możesz ustawić maksymalną głębokość przechodzenia metodą limitDepth().
Ustawienie limitDepth(1) przechodzi tylko przez pierwszy poziom podkatalogów, limitDepth(0) całkowicie
wyłącza schodzenie w głąb, a wartość –1 usuwa ograniczenie głębokości.
Finder pozwala używać własnych callbacków do decydowania, do których katalogów wchodzić podczas przechodzenia. Callback
otrzymuje obiekt Nette\Utils\FileInfo reprezentujący katalog i musi zwrócić true, aby do
niego wejść:
Finder::findFiles('*.php')
->descentFilter(fn($file) => $file->getBasename() !== 'temp');
Katalogi nie do odczytu
Domyślnie Finder pomija katalogi, których nie potrafi odczytać (na przykład z powodu niewystarczających uprawnień).
Jeśli wolisz, aby w takich przypadkach zgłaszał wyjątek, wywołaj ignoreUnreadableDirs(false).
Finder::findFiles('*.php')
->from($dir)
->ignoreUnreadableDirs(false);
Sortowanie
Finder oferuje również kilka metod do sortowania wyników.
Metoda sortByName() sortuje wyniki według nazwy pliku. Sortowanie jest naturalne, czyli poprawnie radzi sobie
z liczbami w nazwach i zwraca np. foo1.txt przed foo10.txt.
Finder pozwala też sortować własnym callbackiem. Otrzymuje on jako parametry dwa obiekty Nette\Utils\FileInfo
i musi zwrócić wynik porównania operatorem <=> (czyli -1, 0 albo 1).
Tak na przykład posortujemy pliki według rozmiaru:
$finder->sortBy(fn($a, $b) => $a->getSize() <=> $b->getSize());
Kilka różnych wyszukiwań
Jeśli potrzebujesz znaleźć kilka zestawów plików w różnych miejscach albo spełniających różne kryteria, użyj metody
append(). Zwraca ona nowy obiekt Finder, dzięki czemu możesz łańcuchowo wywoływać metody
dołączonego wyszukiwania:
($finder = new Finder) // pierwszy Finder zapisz do zmiennej $finder!
->files('*.php') // szuka plików *.php w src/
->from('src')
->append()
->files('*.md') // w docs/ szuka plików *.md
->from('docs')
->append()
->files('*.json'); // w bieżącym katalogu szuka plików *.json
Alternatywnie metody append() można użyć do dodania konkretnego pliku (albo tablicy plików). W tym przypadku
zwraca ten sam obiekt Finder:
$finder = Finder::findFiles('*.txt')
->append(__FILE__);
FileInfo
Nette\Utils\FileInfo to klasa reprezentująca plik albo katalog znaleziony w wyniku wyszukiwania. Rozszerza klasę SplFileInfo i udostępnia informacje takie jak rozmiar pliku, data ostatniej modyfikacji, nazwa, ścieżka itd.
Ponadto udostępnia metody zwracające ścieżkę względną, co przydaje się przy przechodzeniu rekurencyjnym:
foreach (Finder::findFiles('*.jpg')->from('.') as $file) {
$absoluteFilePath = $file->getRealPath();
$relativeFilePath = $file->getRelativePathname();
}
Dostępne są też metody do odczytu i zapisu zawartości pliku:
foreach ($finder as $file) {
$contents = $file->read();
// ...
$file->write($contents);
}
Zwracanie wyników jako tablicy
Jak widać w przykładach, Finder implementuje interfejs IteratorAggregate, więc do przejścia po wynikach
możesz użyć foreach. Zaprojektowano go tak, aby wyniki wczytywały się dopiero podczas iteracji, co oznacza, że
przy dużej liczbie plików nie czeka z góry na odczytanie ich wszystkich.
Wyniki możesz też pobrać jako tablicę obiektów Nette\Utils\FileInfo metodą collect(). Tablica
jest indeksowana liczbowo, a nie asocjacyjnie.
$array = Finder::findFiles('*.php')->collect();