Finder: поиск файлов
Нужно найти файлы, отвечающие определённой маске? Finder вам поможет. Это универсальный и быстрый инструмент для обхода структуры каталогов.
Установка:
composer require nette/utils
В примерах предполагается, что создан такой псевдоним класса:
use Nette\Utils\Finder;
Использование
Сначала посмотрим, как с помощью Nette\Utils\Finder вывести имена файлов с
расширениями .txt и .md в текущем каталоге:
foreach (Finder::findFiles(['*.txt', '*.md']) as $name => $file) {
echo $file;
}
Каталогом поиска по умолчанию служит текущий каталог, но вы можете
изменить его методами in() или from(). Переменная
$file – экземпляр класса FileInfo, а $name –
строка с путём к файлу.
Путь возвращается в том виде, в каком вы его написали, с сохранением
разделителей платформы; в Windows поэтому в результате могут смешиваться
/ и \. Вызовите FileSystem::unixSlashes(), если вам нужна
единообразная форма.
Что искать?
Кроме метода findFiles() есть findDirectories(), который ищет только
каталоги, и find(), который ищет и то, и другое. Эти методы
статические, поэтому их можно вызывать без создания экземпляра.
Аргумент с маской необязателен; если его опустить, подходит всё.
foreach (Finder::find() as $file) {
echo $file; // теперь выводятся все файлы и каталоги
}
С помощью методов files() и directories() вы можете указать
дополнительные элементы для поиска. Методы можно вызывать
многократно, а в аргументе можно передать и массив масок:
Finder::findDirectories('vendor') // все каталоги
->files(['*.php', '*.phpt']); // плюс все файлы PHP
Альтернатива статическим методам – создание экземпляра через
new Finder (такой новый объект изначально ничего не ищет) и указание
искомого через files() и directories():
(new Finder)
->directories() // все каталоги
->files('*.php'); // плюс все файлы PHP
В маске можно использовать подстановочные
знаки, такие как *, **, ? и [...]. Можно
указывать даже каталоги: например, src/*.php находит все файлы PHP в
каталоге src. Символьные ссылки тоже трактуются как каталоги
или файлы.
Где искать?
Каталогом поиска по умолчанию служит текущий каталог. Изменить его
можно методами in() и from():
Finder::findFiles('*.php')
->in(['src', 'tests']) // ищет прямо в src/ и tests/
->from('vendor'); // ищет и в подкаталогах vendor/
Эти два метода различаются глубиной: in() ищет только внутри
заданного каталога, а from() спускается и в его подкаталоги
(рекурсивно). Чтобы рекурсивно обойти текущий каталог, используйте
from('.').
Впрочем, рекурсию задаёт не только from(): ею управляет и
подстановочный знак ** в маске, так что
findFiles('**/*.php')->in('src') тоже ищет рекурсивно. Иначе говоря,
from('src') – лишь сокращение для in('src') с рекурсивной маской.
См. Подстановочные знаки.
Эти методы можно вызывать многократно или передавать несколько
путей массивом; тогда файлы будут искаться во всех указанных
каталогах. Если какого-то из каталогов не существует, выбрасывается
Nette\InvalidStateException.
Относительные пути отсчитываются от текущего каталога, но можно использовать и абсолютные:
Finder::findFiles('*.php')
->in('/var/www/html');
В пути можно использовать подстановочные знаки *, ** и
?, но не [...], который там воспринимается буквально. Это
предотвращает неожиданное поведение, когда вы, например, ищете
in(__DIR__), а путь случайно содержит символы []. Например,
src/*/*.php ищет все файлы PHP в каталогах второго уровня внутри
src.
При рекурсивном поиске файлов и каталогов (в глубину) сначала
возвращается родительский каталог, а затем содержащиеся в нём файлы.
Этот порядок можно перевернуть методом childFirst().
Подстановочные знаки
Маска может содержать несколько особых символов:
*– любое число символов, кроме разделителя/(остаётся в пределах одного уровня каталогов)**– любое число символов, включая/(проходит сквозь уровни каталогов, см. ниже)?– ровно один символ, кроме/[a-z]– один символ из диапазона или набора внутри скобок[!a-z]– один символ, которого нет в скобках
Принципиально важный и легко упускаемый момент: **
соответствует нулю или более уровней каталогов. Это не
означает “хотя бы один подкаталог”. Поэтому src/**/*.php подходит и
файлу, лежащему прямо в src, и файлу, спрятанному на несколько
уровней глубже. Рассмотрим такое дерево:
src/
├── app.php
├── Model/
│ ├── User.php
│ └── Repository/
│ └── UserRepository.php
└── Control/
└── SignForm.php
Следующая таблица показывает, чему соответствуют отдельные маски, для файлов и каталогов:
| Маска | Соответствие |
|---|---|
src/*.php |
только src/app.php (прямо в src) |
src/**/*.php |
src/app.php, src/Model/User.php, src/Model/Repository/UserRepository.php (все
уровни, включая непосредственно src) |
src/* |
прямые потомки src: app.php, Model, Control |
src/** |
всё внутри src, файлы и каталоги (сокращение для src/**/*) |
src/*/ |
прямые подкаталоги src: Model, Control |
src/**/ |
все подкаталоги любой глубины: Model,
Model/Repository, Control |
Стоит запомнить два сокращения:
**, за которым не следует сразу/, ведёт себя как**/плюс*. Так чтоsrc/**– сокращение дляsrc/**/*, а**.php– для**/*.php.- Завершающий слеш ограничивает маску только каталогами. Так что
find('log/')возвращает каталоги с именемlog, но никогда файл с таким именем. (findFiles()завершающий слеш отклоняет, потому что искать файл “каталог” бессмысленно.)
Другие примеры использования:
img/?.png– файлы с однобуквенным именем вроде0.png,1.png,x.pnglogs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log– файлы логов в форматеYYYY-MM-DDdocs/**/*.md– все файлы с расширением.mdвdocsи всех его подкаталогах
Исключение
Метод exclude() убирает файлы и каталоги из результатов.
Аргументом служит маска, которой элемент не должен
соответствовать. Здесь мы ищем файлы *.txt, кроме тех, в имени
которых есть буква X:
Finder::findFiles('*.txt')
->exclude('*X*');
Маска исключения использует ту же грамматику, что и маски поиска: те
же подстановочные знаки, привязку ./ и
сокращение **. Её завершающая часть определяет область
исключения:
| Маска | Исключает |
|---|---|
temp |
любой файл или каталог с именем temp, на любой глубине |
temp/ |
только каталог temp (и его содержимое); файл с именем temp
сохраняется |
temp/* |
содержимое temp, но сохраняет сам каталог temp |
temp/** |
то же самое, что temp/* |
В исключённый каталог обход даже не заходит, поэтому исключение целых поддеревьев ещё и ускоряет поиск. Вот как пропустить определённые подкаталоги:
Finder::findFiles('*.php')
->from($dir)
->exclude('temp', '.git');
Фильтрация
Finder предлагает несколько методов фильтрации результатов (то есть их сокращения). Их можно сочетать и вызывать многократно.
С помощью size() мы фильтруем по размеру файла. Так мы найдём
файлы размером от 100 до 200 байт:
Finder::findFiles('*.php')
->size('>=', 100)
->size('<=', 200);
Метод date() фильтрует по дате последнего изменения файла.
Значения могут быть абсолютными датами или относительными к текущим
дате и времени. Например, так находятся файлы, изменённые за последние
две недели:
Finder::findFiles('*.php')
->date('>', '-2 weeks')
->from($dir)
Оба метода понимают операторы >, >=, <,
<=, =, !=, <>.
Finder позволяет также фильтровать результаты собственными
callback-функциями. Callback получает параметром объект Nette\Utils\FileInfo и
должен вернуть true, чтобы файл попал в результаты.
Пример: поиск файлов PHP, содержащих строку 'Nette' (без учёта
регистра):
Finder::findFiles('*.php')
->filter(fn($file) => str_contains(strtolower($file->read()), 'nette'));
Фильтрация по глубине
При рекурсивном поиске вы можете задать максимальную глубину обхода
методом limitDepth(). Значение limitDepth(1) обходит только первый
уровень подкаталогов, limitDepth(0) полностью отключает спуск вглубь,
а значение –1 снимает ограничение глубины.
Finder позволяет с помощью собственных callback-функций решать, в какие
каталоги заходить при обходе. Callback получает объект Nette\Utils\FileInfo,
представляющий каталог, и должен вернуть true, чтобы зайти
в него:
Finder::findFiles('*.php')
->descentFilter(fn($file) => $file->getBasename() !== 'temp');
Нечитаемые каталоги
По умолчанию Finder пропускает каталоги, которые не может прочитать
(например, из-за недостатка прав). Если вы предпочитаете, чтобы в таких
случаях он выбрасывал исключение, вызовите ignoreUnreadableDirs(false).
Finder::findFiles('*.php')
->from($dir)
->ignoreUnreadableDirs(false);
Сортировка
Finder предлагает и несколько методов сортировки результатов.
Метод sortByName() сортирует результаты по имени файла. Сортировка
естественная, то есть правильно обрабатывает числа в именах и
возвращает, например, foo1.txt раньше foo10.txt.
Finder позволяет сортировать и собственной callback-функцией. Она получает
параметрами два объекта Nette\Utils\FileInfo и должна вернуть результат
сравнения оператором <=> (то есть -1, 0 или
1). Например, вот так мы сортируем файлы по размеру:
$finder->sortBy(fn($a, $b) => $a->getSize() <=> $b->getSize());
Несколько разных поисков
Если вам нужно найти несколько наборов файлов в разных местах или по
разным критериям, используйте метод append(). Он возвращает новый
объект Finder, позволяя выстроить цепочку вызовов для
добавленного поиска:
($finder = new Finder) // первый Finder сохраните в переменную $finder!
->files('*.php') // ищем файлы *.php в src/
->from('src')
->append()
->files('*.md') // в docs/ ищем файлы *.md
->from('docs')
->append()
->files('*.json'); // в текущей папке ищем файлы *.json
Кроме того, метод append() можно использовать, чтобы добавить
конкретный файл (или массив файлов). В этом случае он возвращает тот же
объект Finder:
$finder = Finder::findFiles('*.txt')
->append(__FILE__);
FileInfo
Nette\Utils\FileInfo – класс, представляющий файл или каталог, найденный в результатах поиска. Он расширяет класс SplFileInfo и предоставляет такие сведения, как размер файла, дата последнего изменения, имя, путь и так далее.
Кроме того, он предоставляет методы, возвращающие относительный путь, что полезно при рекурсивном обходе:
foreach (Finder::findFiles('*.jpg')->from('.') as $file) {
$absoluteFilePath = $file->getRealPath();
$relativeFilePath = $file->getRelativePathname();
}
Также доступны методы для чтения и записи содержимого файла:
foreach ($finder as $file) {
$contents = $file->read();
// ...
$file->write($contents);
}
Получение результатов в виде массива
Как видно из примеров, Finder реализует интерфейс IteratorAggregate,
поэтому вы можете обходить результаты через foreach. Он устроен
так, что результаты загружаются только во время обхода, то есть при
большом количестве файлов он не ждёт, пока все они будут прочитаны
заранее.
Вы можете также получить результаты в виде массива объектов
Nette\Utils\FileInfo методом collect(). Массив индексируется числами,
а не ассоциативно.
$array = Finder::findFiles('*.php')->collect();