Информация о каталоге в PHP: обзор инструментов для разработчиков
Основные способы получения информации о каталоге в PHP
Как получить полную информацию о файлах внутри каталога, включая размер, дату модификации и права доступа, используя современный объектно-ориентированный подход?
Наиболее эффективным решением является использование класса DirectoryIterator. Он предоставляет удобный интерфейс для итерации по записям каталога, а каждый объект итератора реализует SplFileInfo, что даёт доступ к метаданным файла без дополнительных вызовов stat().
<?php
$path = '/var/www/project';
$iterator = new DirectoryIterator($path);
foreach ($iterator as $fileinfo) {
if ($fileinfo->isDot()) continue; // пропустить . и ..
$name = $fileinfo->getFilename();
$size = $fileinfo->getSize();
$perms = substr(sprintf('%o', $fileinfo->getPerms()), -4);
$mtime = date('Y-m-d H:i:s', $fileinfo->getMTime());
echo "$name | $size bytes | $perms | $mtime\n";
}
?>
Возможные проблемы: Если путь не существует или нет прав на чтение, выбрасывается исключение UnexpectedValueException. Следует оборачивать код в try-catch. Также следует помнить, что DirectoryIterator включает записи . и .., которые необходимо отфильтровывать.
Типичная ошибка: Использование $fileinfo->getPathname() как строки без проверки isDot(). Это может привести к ложным результатам при попытке открыть '.' в качестве файла.
Случаи использования: Когда требуется не только список файлов, но и их свойства для последующей обработки (например, построение файлового менеджера или индексация). DirectoryIterator работает быстро и потребляет мало памяти, так как не загружает все данные сразу.
Как просто получить массив имён файлов и папок без дополнительных атрибутов?
Функция scandir() возвращает отсортированный массив записей каталога. Это самый быстрый способ получить плоский список.
<?php
$files = scandir('/path/to/dir');
$files = array_diff($files, array('.', '..'));
print_r($files);
?>
Проблемы: scandir() не предоставляет никакой информации о файлах, кроме имени. Порядок сортировки по умолчанию алфавитный; для другого порядка нужно передавать второй параметр. Ошибка при отсутствии каталога возвращает false, а не исключение.
Случай использования: Простое получение списка для последующей передачи в другой обработчик, или когда нужно только перечислить содержимое.
Как отфильтровать файлы по шаблону, например, все изображения .jpg?
Функция glob() возвращает массив путей, соответствующих заданному паттерну. Поддерживает регулярные выражения в стиле оболочки.
<?php
$images = glob('/path/to/photos/*.{jpg,jpeg,png}', GLOB_BRACE);
foreach ($images as $img) {
echo basename($img) . " - " . filesize($img) . " bytes\n";
}
?>
Проблемы: glob() не рекурсивна по умолчанию. Для обхода подкаталогов требуется GLOB_RECURSIVE (доступен с PHP 8.1) или ручной вызов. Также при большом количестве файлов может быть медленнее итераторов.
Типичная ошибка: Ожидание, что GLOB_BRACE работает во всех системах – на Windows может потребоваться явное указание расширений.
Случай использования: Когда нужно быстро собрать файлы определённого типа для пакетной обработки (например, все изображения, которые нужно ресайзить).
Как получить информацию о файлах с помощью низкоуровневых функций opendir/readdir?
Классический подход: opendir(), затем readdir() в цикле, и для каждого элемента вызывать stat() или fileinfo. Это даёт полный контроль, но требует больше кода.
<?php
$dh = opendir('/var/www');
if ($dh) {
while (($file = readdir($dh)) !== false) {
if ($file == '.' || $file == '..') continue;
$path = '/var/www/' . $file;
$stat = stat($path);
echo $file . ' - size: ' . $stat['size'] . ', mode: ' . substr(sprintf('%o', $stat['mode']), -4) . "\n";
}
closedir($dh);
}
?>
Проблемы: Необходимо вручную управлять ресурсом и закрывать дескриптор. Ошибка открытия каталога не генерирует исключение, а возвращает false. Для каждого файла выполняется отдельный системный вызов stat(), что при больших каталогах может быть медленнее DirectoryIterator.
Случаи использования: Когда требуется совместимость с очень старыми версиями PHP или необходимо очень специфическое управление потоком чтения.
Как рекурсивно получить информацию обо всех файлах внутри вложенных каталогов?
Класс RecursiveDirectoryIterator в комбинации с RecursiveIteratorIterator позволяет обойти всё дерево каталогов.
<?php
$path = '/var/www';
$rdi = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($rdi, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iterator as $fileinfo) {
if ($fileinfo->isFile()) {
echo $fileinfo->getPathname() . "\n";
}
}
?>
Проблемы: При большом количестве вложенных папок может потребоваться много памяти, так как итератор строит всё дерево в памяти. Используйте флаг CURRENT_AS_SELF для экономии. Также возможно превышение лимита вложенности операционной системы.
Типичная ошибка: Забывание установить SKIP_DOTS приводит к появлению '.' и '..'.
Случаи использования: Поиск всех файлов определённого типа, подсчёт общего размера каталога, создание резервных копий.
Расширенные практические примеры
Пример 1: Подсчёт общего размера каталога (рекурсивный)
Используем RecursiveDirectoryIterator для суммирования размеров всех файлов.
<?php
function dirSize($path) {
$totalSize = 0;
$rdi = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($rdi, RecursiveIteratorIterator::LEAVES_ONLY);
foreach ($iterator as $fileinfo) {
if ($fileinfo->isFile()) {
$totalSize += $fileinfo->getSize();
}
}
return $totalSize;
}
echo 'Размер /var/www: ' . dirSize('/var/www') . ' bytes';
?>
Размер /var/www: 128937459 bytes
Пояснение: Флаг LEAVES_ONLY гарантирует, что мы обращаемся только к файлам, пропуская каталоги. Такой подход точнее, чем суммирование по всем элементам.
Пример 2: Вывод дерева каталогов с отступами
Рекурсивно обходим структуру и форматируем вывод с учётом уровня вложенности.
<?php
function listDirTree($path, $indent = '') {
$dir = new DirectoryIterator($path);
foreach ($dir as $fileinfo) {
if ($fileinfo->isDot()) continue;
$name = $fileinfo->getFilename();
$size = $fileinfo->isFile() ? ' (' . $fileinfo->getSize() . ' B)' : '';
echo $indent . ($fileinfo->isDir() ? '[DIR] ' : '[FILE] ') . $name . $size . "\n";
if ($fileinfo->isDir() && !$fileinfo->isLink()) {
listDirTree($fileinfo->getPathname(), $indent . ' ');
}
}
}
listDirTree('/var/www');
?>
[FILE] index.php (2048 B)
[DIR] public
[FILE] style.css (4096 B)
[DIR] images
[FILE] logo.png (10240 B)
Пояснение: Использован рекурсивный вызов для подкаталогов. Важно учитывать символические ссылки, чтобы избежать бесконечных циклов – в примере проверка !$fileinfo->isLink().
Пример 3: Поиск всех файлов с определённым расширением с помощью итератора
Используем RegexIterator для фильтрации.
<?php
$path = '/var/www';
$rdi = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS);
$rit = new RecursiveIteratorIterator($rdi, RecursiveIteratorIterator::SELF_FIRST);
$regex = new RegexIterator($rit, '/\.php$/i', RecursiveRegexIterator::GET_MATCH);
foreach ($regex as $pathname => $matches) {
echo $pathname . "\n";
}
?>
/var/www/index.php
/var/www/includes/functions.php
/var/www/admin/config.php
Пояснение: RegexIterator применяет регулярное выражение к текущему пути. Параметр GET_MATCH возвращает совпадение. Это удобная альтернатива glob() для рекурсивного поиска.
Пример 4: Получение информации о файлах в формате JSON
Собираем метаданные и сериализуем в JSON для API.
<?php
function getDirInfoJson($path) {
$result = [];
$iterator = new DirectoryIterator($path);
foreach ($iterator as $info) {
if ($info->isDot()) continue;
$result[] = [
'name' => $info->getFilename(),
'type' => $info->isDir() ? 'dir' : 'file',
'size' => $info->getSize(),
'mtime' => $info->getMTime(),
'perms' => $info->getPerms()
];
}
return json_encode($result, JSON_PRETTY_PRINT);
}
echo getDirInfoJson('/var/www');
?>
[
{
"name": "index.php",
"type": "file",
"size": 2048,
"mtime": 1680000000,
"perms": 33188
},
{
"name": "public",
"type": "dir",
"size": 4096,
"mtime": 1680000100,
"perms": 16877
}
]
Пояснение: Для вывода прав доступа числовой код можно преобразовать в строку, но в JSON удобнее оставить числом. Абсолютные метки времени можно потом преобразовать на клиенте.