Nette Documentation Preview

syntax
Finder: поиск файлов
********************

.[perex]
Нужно найти файлы, отвечающие определённой маске? Finder вам поможет. Это универсальный и быстрый инструмент для обхода структуры каталогов.


Установка:

```shell
composer require nette/utils
```

В примерах предполагается, что создан такой псевдоним класса:

```php
use Nette\Utils\Finder;
```


Использование
-------------

Сначала посмотрим, как с помощью [api:Nette\Utils\Finder] вывести имена файлов с расширениями `.txt` и `.md` в текущем каталоге:

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

Каталогом поиска по умолчанию служит текущий каталог, но вы можете изменить его методами [in() или from() |#Где искать?]. Переменная `$file` - экземпляр класса [#FileInfo], а `$name` - строка с путём к файлу.

Путь возвращается в том виде, в каком вы его написали, с сохранением разделителей платформы; в Windows поэтому в результате могут смешиваться `/` и `\`. Вызовите `FileSystem::unixSlashes()`, если вам нужна единообразная форма.


Что искать?
-----------

Кроме метода `findFiles()` есть `findDirectories()`, который ищет только каталоги, и `find()`, который ищет и то, и другое. Эти методы статические, поэтому их можно вызывать без создания экземпляра. Аргумент с маской необязателен; если его опустить, подходит всё.

```php
foreach (Finder::find() as $file) {
	echo $file; // теперь выводятся все файлы и каталоги
}
```

С помощью методов `files()` и `directories()` вы можете указать дополнительные элементы для поиска. Методы можно вызывать многократно, а в аргументе можно передать и массив масок:

```php
Finder::findDirectories('vendor') // все каталоги
	->files(['*.php', '*.phpt']); // плюс все файлы PHP
```

Альтернатива статическим методам - создание экземпляра через `new Finder` (такой новый объект изначально ничего не ищет) и указание искомого через `files()` и `directories()`:

```php
(new Finder)
	->directories()      // все каталоги
	->files('*.php');    // плюс все файлы PHP
```

В маске можно использовать [подстановочные знаки |#Подстановочные знаки], такие как `*`, `**`, `?` и `[...]`. Можно указывать даже каталоги: например, `src/*.php` находит все файлы PHP в каталоге `src`. Символьные ссылки тоже трактуются как каталоги или файлы.


Где искать?
-----------

Каталогом поиска по умолчанию служит текущий каталог. Изменить его можно методами `in()` и `from()`:

```php
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`.

Относительные пути отсчитываются от текущего каталога, но можно использовать и абсолютные:

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

В пути можно использовать подстановочные знаки `*`, `**` и `?`, но **не** `[...]`, который там воспринимается буквально. Это предотвращает неожиданное поведение, когда вы, например, ищете `in(__DIR__)`, а путь случайно содержит символы `[]`. Например, `src/*/*.php` ищет все файлы PHP в каталогах второго уровня внутри `src`.

При рекурсивном поиске файлов и каталогов (в глубину) сначала возвращается родительский каталог, а затем содержащиеся в нём файлы. Этот порядок можно перевернуть методом `childFirst()`.


Подстановочные знаки
--------------------

Маска может содержать несколько особых символов:

- `*` - любое число символов, кроме разделителя `/` (остаётся в пределах одного уровня каталогов)
- `**` - любое число символов, **включая** `/` (проходит сквозь уровни каталогов, см. ниже)
- `?` - ровно один символ, кроме `/`
- `[a-z]` - один символ из диапазона или набора внутри скобок
- `[!a-z]` - один символ, которого *нет* в скобках

Принципиально важный и легко упускаемый момент: `**` соответствует **нулю или более** уровней каталогов. Это *не* означает "хотя бы один подкаталог". Поэтому `src/**/*.php` подходит и файлу, лежащему прямо в `src`, и файлу, спрятанному на несколько уровней глубже. Рассмотрим такое дерево:

/--pre
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.png`
- `logs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log` - файлы логов в формате `YYYY-MM-DD`
- `docs/**/*.md` - все файлы с расширением `.md` в `docs` и всех его подкаталогах


Исключение
----------

Метод `exclude()` убирает файлы и каталоги из результатов. Аргументом служит маска, которой элемент **не** должен соответствовать. Здесь мы ищем файлы `*.txt`, кроме тех, в имени которых есть буква `X`:

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

Маска исключения использует ту же грамматику, что и маски поиска: те же [подстановочные знаки |#Подстановочные знаки], привязку `./` и сокращение `**`. Её завершающая часть определяет область исключения:

|-------------------------------------------------------------------------------
| Маска | Исключает
|-------------------------------------------------------------------------------
| `temp` | любой файл или каталог с именем `temp`, на любой глубине
| `temp/` | только каталог `temp` (и его содержимое); файл с именем `temp` сохраняется
| `temp/*` | содержимое `temp`, но сохраняет сам каталог `temp`
| `temp/**` | то же самое, что `temp/*`

В исключённый каталог обход даже не заходит, поэтому исключение целых поддеревьев ещё и ускоряет поиск. Вот как пропустить определённые подкаталоги:

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


Фильтрация
----------

Finder предлагает несколько методов фильтрации результатов (то есть их сокращения). Их можно сочетать и вызывать многократно.

С помощью `size()` мы фильтруем по размеру файла. Так мы найдём файлы размером от 100 до 200 байт:

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

Метод `date()` фильтрует по дате последнего изменения файла. Значения могут быть абсолютными датами или относительными к текущим дате и времени. Например, так находятся файлы, изменённые за последние две недели:

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

Оба метода понимают операторы `>`, `>=`, `<`, `<=`, `=`, `!=`, `<>`.

Finder позволяет также фильтровать результаты собственными callback-функциями. Callback получает параметром объект `Nette\Utils\FileInfo` и должен вернуть `true`, чтобы файл попал в результаты.

Пример: поиск файлов PHP, содержащих строку `'Nette'` (без учёта регистра):

```php
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`, чтобы зайти в него:

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


Нечитаемые каталоги
-------------------

По умолчанию Finder пропускает каталоги, которые не может прочитать (например, из-за недостатка прав). Если вы предпочитаете, чтобы в таких случаях он выбрасывал исключение, вызовите `ignoreUnreadableDirs(false)`.

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


Сортировка
----------

Finder предлагает и несколько методов сортировки результатов.

Метод `sortByName()` сортирует результаты по имени файла. Сортировка естественная, то есть правильно обрабатывает числа в именах и возвращает, например, `foo1.txt` раньше `foo10.txt`.

Finder позволяет сортировать и собственной callback-функцией. Она получает параметрами два объекта `Nette\Utils\FileInfo` и должна вернуть результат сравнения оператором `<=>` (то есть `-1`, `0` или `1`). Например, вот так мы сортируем файлы по размеру:

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


Несколько разных поисков
------------------------

Если вам нужно найти несколько наборов файлов в разных местах или по разным критериям, используйте метод `append()`. Он возвращает новый объект `Finder`, позволяя выстроить цепочку вызовов для добавленного поиска:


```php
($finder = new Finder) // первый Finder сохраните в переменную $finder!
	->files('*.php')   // ищем файлы *.php в src/
	->from('src')
	->append()
	->files('*.md')    // в docs/ ищем файлы *.md
	->from('docs')
	->append()
	->files('*.json'); // в текущей папке ищем файлы *.json
```

Кроме того, метод `append()` можно использовать, чтобы добавить конкретный файл (или массив файлов). В этом случае он возвращает тот же объект `Finder`:

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


FileInfo
--------

[api:Nette\Utils\FileInfo] - класс, представляющий файл или каталог, найденный в результатах поиска. Он расширяет класс [php:SplFileInfo] и предоставляет такие сведения, как размер файла, дата последнего изменения, имя, путь и так далее.

Кроме того, он предоставляет методы, возвращающие относительный путь, что полезно при рекурсивном обходе:

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

Также доступны методы для чтения и записи содержимого файла:

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


Получение результатов в виде массива
------------------------------------

Как видно из примеров, Finder реализует интерфейс `IteratorAggregate`, поэтому вы можете обходить результаты через `foreach`. Он устроен так, что результаты загружаются только во время обхода, то есть при большом количестве файлов он не ждёт, пока все они будут прочитаны заранее.

Вы можете также получить результаты в виде массива объектов `Nette\Utils\FileInfo` методом `collect()`. Массив индексируется числами, а не ассоциативно.

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

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, ModelControl
src/** всё внутри src, файлы и каталоги (сокращение для src/**/*)
src/*/ прямые подкаталоги src: ModelControl
src/**/ все подкаталоги любой глубины: Model, Model/RepositoryControl

Стоит запомнить два сокращения:

  • **, за которым не следует сразу /, ведёт себя как **/ плюс *. Так что src/** – сокращение для src/**/*, а **.php – для **/*.php.
  • Завершающий слеш ограничивает маску только каталогами. Так что find('log/') возвращает каталоги с именем log, но никогда файл с таким именем. (findFiles() завершающий слеш отклоняет, потому что искать файл „каталог“ бессмысленно.)

Другие примеры использования:

  • img/?.png – файлы с однобуквенным именем вроде 0.png, 1.pngx.png
  • logs/[0-9][0-9][0-9][0-9]-[01][0-9]-[0-3][0-9].log – файлы логов в формате YYYY-MM-DD
  • docs/**/*.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();