SPL предоставляет объектно-ориентированный интерфейс для работы с файлами и каталогами. Вместо процедурных функций (fopen, fread, fgets) можно использовать классы SplFileInfo и SplFileObject, которые инкапсулируют все операции с файлами.
SplFileInfo -- информация о файле
SplFileInfo -- это объектная обёртка над информацией о файле или каталоге. Она не открывает файл для чтения/записи, а только предоставляет метаданные.
<?php
declare(strict_types=1);
$info = new SplFileInfo('/var/www/app/src/Controller/UserController.php');
// Basic information
echo $info->getFilename(); // UserController.php
echo $info->getBasename(); // UserController.php
echo $info->getBasename('.php'); // UserController (without extension)
echo $info->getExtension(); // php
// Path information
echo $info->getPath(); // /var/www/app/src/Controller
echo $info->getPathname(); // /var/www/app/src/Controller/UserController.php
echo $info->getRealPath(); // /var/www/app/src/Controller/UserController.php (resolved symlinks)
// Type checks
var_dump($info->isFile()); // true
var_dump($info->isDir()); // false
var_dump($info->isLink()); // false (not a symlink)
var_dump($info->isReadable()); // true
var_dump($info->isWritable()); // true
var_dump($info->isExecutable()); // false
// Size and time
echo $info->getSize(); // 2048 (bytes)
echo $info->getMTime(); // 1708531200 (Unix timestamp of last modification)
echo $info->getATime(); // 1708531500 (last access time)
echo $info->getCTime(); // 1708531200 (inode change time on Unix)
// Type and permissions
echo $info->getType(); // "file" or "dir" or "link"
echo $info->getPerms(); // 33188 (octal 0100644)
echo decoct($info->getPerms()); // 100644
// Owner
echo $info->getOwner(); // 1000 (UID)
echo $info->getGroup(); // 1000 (GID)
SplFileInfo при работе с несуществующими файлами
<?php
declare(strict_types=1);
$info = new SplFileInfo('/path/to/nonexistent.txt');
// These work without the file existing:
echo $info->getFilename(); // nonexistent.txt
echo $info->getExtension(); // txt
echo $info->getPath(); // /path/to
// These will fail or return false:
var_dump($info->isFile()); // false
var_dump($info->isDir()); // false
var_dump($info->getRealPath()); // false (file doesn't exist)
// $info->getSize(); // RuntimeException: SplFileInfo::getSize(): stat failed
Важно:
SplFileInfoне проверяет существование файла при создании. Методы, требующие доступа к файловой системе (getSize(),getMTime()), бросятRuntimeExceptionдля несуществующих файлов.
SplFileObject -- чтение и запись файлов
SplFileObject расширяет SplFileInfo и предоставляет полноценный интерфейс для работы с содержимым файлов. Он реализует Iterator, что позволяет использовать файл в foreach.
Построчное чтение
<?php
declare(strict_types=1);
$file = new SplFileObject('/var/log/app.log');
// Read line by line with foreach
foreach ($file as $lineNumber => $line) {
echo sprintf('[%d] %s', $lineNumber + 1, $line);
}
// Read single line
$file->rewind();
$firstLine = $file->current();
echo $firstLine;
// Move to specific line
$file->seek(9); // Go to line 10 (0-indexed)
echo $file->current();
Флаги SplFileObject
Флаги управляют поведением при чтении:
<?php
declare(strict_types=1);
$file = new SplFileObject('/path/to/data.txt');
// DROP_NEW_LINE — strip newline characters from end of line
$file->setFlags(SplFileObject::DROP_NEW_LINE);
// SKIP_EMPTY — skip empty lines during iteration
$file->setFlags(SplFileObject::SKIP_EMPTY);
// READ_AHEAD — read on rewind/next to make hasChildren() work
$file->setFlags(SplFileObject::READ_AHEAD);
// Combine flags with bitwise OR
$file->setFlags(
SplFileObject::DROP_NEW_LINE
| SplFileObject::SKIP_EMPTY
| SplFileObject::READ_AHEAD,
);
// Now iteration skips empty lines and strips newlines
foreach ($file as $line) {
echo $line . PHP_EOL; // No trailing \n, no empty lines
}
Чтение CSV-файлов
<?php
declare(strict_types=1);
// Read CSV with SplFileObject
$csv = new SplFileObject('/data/users.csv');
$csv->setFlags(
SplFileObject::READ_CSV
| SplFileObject::SKIP_EMPTY
| SplFileObject::DROP_NEW_LINE,
);
// Default delimiter is comma, but can be changed:
$csv->setCsvControl(
separator: ';', // Delimiter
enclosure: '"', // Enclosure character
escape: '\\', // Escape character
);
// Read header
$csv->rewind();
$headers = $csv->current();
$csv->next();
// Read data rows
foreach ($csv as $row) {
if ($row === [null]) {
continue; // Skip malformed lines
}
// Map headers to values
$record = array_combine($headers, $row);
echo sprintf(
'%s <%s>',
$record['name'],
$record['email'],
) . PHP_EOL;
}
Запись файлов
<?php
declare(strict_types=1);
// Write text file
$file = new SplFileObject('/tmp/output.txt', 'w');
$file->fwrite("First line\n");
$file->fwrite("Second line\n");
$file->fwrite("Third line\n");
// Write CSV
$csvOut = new SplFileObject('/tmp/export.csv', 'w');
// Write header
$csvOut->fputcsv(['id', 'name', 'email']);
// Write data
$users = [
[1, 'Alice', '[email protected]'],
[2, 'Bob', '[email protected]'],
[3, 'Charlie', '[email protected]'],
];
foreach ($users as $user) {
$csvOut->fputcsv($user);
}
Блокировка файлов (flock)
<?php
declare(strict_types=1);
$file = new SplFileObject('/tmp/counter.txt', 'c+');
// Acquire exclusive lock (blocking)
if ($file->flock(LOCK_EX)) {
// Read current value
$file->rewind();
$counter = (int) $file->fgets();
// Increment
$counter++;
// Write new value
$file->rewind();
$file->ftruncate(0);
$file->fwrite((string) $counter);
// Release lock
$file->flock(LOCK_UN);
echo "Counter: {$counter}" . PHP_EOL;
} else {
echo 'Could not acquire lock' . PHP_EOL;
}
// Non-blocking lock attempt
$file2 = new SplFileObject('/tmp/resource.lock', 'c+');
if ($file2->flock(LOCK_EX | LOCK_NB)) {
echo 'Lock acquired, processing...' . PHP_EOL;
// ... work ...
$file2->flock(LOCK_UN);
} else {
echo 'Resource is busy, try later' . PHP_EOL;
}
Позиционирование в файле
<?php
declare(strict_types=1);
$file = new SplFileObject('/path/to/data.bin', 'r');
// Seek to byte position
$file->fseek(100); // Go to byte 100
$file->fseek(50, SEEK_CUR); // Forward 50 bytes from current position
$file->fseek(-20, SEEK_END); // 20 bytes before end
// Read specific number of bytes
$data = $file->fread(1024); // Read 1024 bytes
// Get current byte position
$position = $file->ftell();
// Go to specific line (for text files)
$file->seek(5); // Go to line 6 (0-indexed)
SplTempFileObject -- временные файлы
SplTempFileObject создаёт временный файл, который удаляется при уничтожении объекта. Можно указать максимальный размер в памяти -- файлы меньше этого размера хранятся в RAM.
<?php
declare(strict_types=1);
// Create temp file in memory (up to 5MB, then spills to disk)
$temp = new SplTempFileObject(5 * 1024 * 1024);
// Use like regular SplFileObject
$temp->fwrite("Temporary data line 1\n");
$temp->fwrite("Temporary data line 2\n");
$temp->fwrite("Temporary data line 3\n");
// Read back
$temp->rewind();
foreach ($temp as $line) {
echo $line;
}
// When $temp goes out of scope, the file is deleted
Использование для промежуточных данных
<?php
declare(strict_types=1);
final class CsvTransformer
{
/**
* Transform CSV data and return as SplTempFileObject.
* Keeps everything in memory for small datasets.
*/
public function transform(SplFileObject $input, callable $rowMapper): SplTempFileObject
{
$output = new SplTempFileObject(10 * 1024 * 1024); // 10MB in-memory limit
$input->setFlags(
SplFileObject::READ_CSV
| SplFileObject::SKIP_EMPTY
| SplFileObject::DROP_NEW_LINE,
);
foreach ($input as $row) {
if ($row === [null]) {
continue;
}
$transformed = $rowMapper($row);
if ($transformed !== null) {
$output->fputcsv($transformed);
}
}
$output->rewind();
return $output;
}
}
$transformer = new CsvTransformer();
$input = new SplFileObject('/data/raw.csv');
$result = $transformer->transform($input, function (array $row): ?array {
// Skip rows with empty name
if (empty($row[1])) {
return null;
}
// Uppercase the name (column 1)
$row[1] = strtoupper($row[1]);
return $row;
});
// Read transformed data
foreach ($result as $row) {
echo implode(', ', $row) . PHP_EOL;
}
DirectoryIterator -- перебор каталога
<?php
declare(strict_types=1);
$dir = new DirectoryIterator('/var/www/app/src');
foreach ($dir as $fileInfo) {
/** @var DirectoryIterator $fileInfo */
// Skip . and ..
if ($fileInfo->isDot()) {
continue;
}
$type = $fileInfo->isDir() ? 'DIR ' : 'FILE';
$size = $fileInfo->isFile() ? $fileInfo->getSize() : 0;
echo sprintf(
'[%s] %-30s %s',
$type,
$fileInfo->getFilename(),
$fileInfo->isFile() ? formatSize($size) : '',
) . PHP_EOL;
}
function formatSize(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$power = $bytes > 0 ? (int) floor(log($bytes, 1024)) : 0;
return sprintf('%.2f %s', $bytes / (1024 ** $power), $units[$power]);
}
Предупреждение:
DirectoryIteratorвозвращает один и тот же объект при каждой итерации. Если вы собираете файлы в массив, используйте$fileInfo->getFileInfo()илиclone $fileInfo:
<?php
declare(strict_types=1);
$dir = new DirectoryIterator('/tmp');
// WRONG — all entries will reference the last file
$wrong = [];
foreach ($dir as $item) {
$wrong[] = $item; // Same object!
}
// CORRECT — clone each entry
$correct = [];
foreach ($dir as $item) {
if (!$item->isDot()) {
$correct[] = clone $item;
}
}
RecursiveDirectoryIterator -- рекурсивный обход
<?php
declare(strict_types=1);
$dir = new RecursiveDirectoryIterator(
'/var/www/app/src',
RecursiveDirectoryIterator::SKIP_DOTS
| RecursiveDirectoryIterator::UNIX_PATHS,
);
$iterator = new RecursiveIteratorIterator(
$dir,
RecursiveIteratorIterator::SELF_FIRST,
);
foreach ($iterator as $path => $fileInfo) {
/** @var SplFileInfo $fileInfo */
$depth = $iterator->getDepth();
$indent = str_repeat(' ', $depth);
if ($fileInfo->isDir()) {
echo "{$indent}[{$fileInfo->getFilename()}]" . PHP_EOL;
} else {
echo "{$indent}{$fileInfo->getFilename()} ({$fileInfo->getSize()} bytes)" . PHP_EOL;
}
}
Ограничение глубины рекурсии
<?php
declare(strict_types=1);
$dir = new RecursiveDirectoryIterator('/var/www/app', RecursiveDirectoryIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($dir);
// Limit recursion depth to 2 levels
$iterator->setMaxDepth(2);
foreach ($iterator as $file) {
echo $file->getPathname() . PHP_EOL;
}
Практические примеры
CSV-импортёр с валидацией
<?php
declare(strict_types=1);
final class CsvImporter
{
/** @var list<string> */
private array $errors = [];
private int $imported = 0;
/**
* @param array<string, int> $columnMap Column name => CSV index
* @param list<callable> $validators List of validators (row => bool)
*/
public function __construct(
private readonly string $filePath,
private readonly array $columnMap,
private readonly array $validators = [],
private readonly bool $hasHeader = true,
) {}
/**
* Import CSV and yield valid rows as associative arrays.
*
* @return Generator<int, array<string, string>>
*/
public function import(): Generator
{
$file = new SplFileObject($this->filePath);
$file->setFlags(
SplFileObject::READ_CSV
| SplFileObject::SKIP_EMPTY
| SplFileObject::DROP_NEW_LINE
| SplFileObject::READ_AHEAD,
);
$lineNumber = 0;
foreach ($file as $row) {
$lineNumber++;
// Skip header row
if ($this->hasHeader && $lineNumber === 1) {
continue;
}
// Skip malformed rows
if ($row === [null] || count($row) < count($this->columnMap)) {
$this->errors[] = "Line {$lineNumber}: insufficient columns";
continue;
}
// Map columns to named keys
$mapped = [];
foreach ($this->columnMap as $name => $index) {
$mapped[$name] = trim($row[$index] ?? '');
}
// Validate
$valid = true;
foreach ($this->validators as $validator) {
if (!$validator($mapped, $lineNumber)) {
$valid = false;
break;
}
}
if ($valid) {
$this->imported++;
yield $lineNumber => $mapped;
}
}
}
public function getImportedCount(): int
{
return $this->imported;
}
/** @return list<string> */
public function getErrors(): array
{
return $this->errors;
}
}
// Usage
$importer = new CsvImporter(
filePath: '/data/users.csv',
columnMap: ['name' => 0, 'email' => 1, 'age' => 2],
validators: [
function (array $row, int $line) use (&$importer): bool {
if (empty($row['email']) || !filter_var($row['email'], FILTER_VALIDATE_EMAIL)) {
return false;
}
return true;
},
function (array $row, int $line): bool {
$age = (int) $row['age'];
return $age >= 0 && $age <= 150;
},
],
);
foreach ($importer->import() as $lineNumber => $user) {
echo sprintf(
'Line %d: %s <%s>, age %s',
$lineNumber,
$user['name'],
$user['email'],
$user['age'],
) . PHP_EOL;
}
echo sprintf(
'Imported: %d, Errors: %d',
$importer->getImportedCount(),
count($importer->getErrors()),
) . PHP_EOL;
Парсер лог-файлов
<?php
declare(strict_types=1);
final readonly class LogEntry
{
public function __construct(
public \DateTimeImmutable $timestamp,
public string $level,
public string $channel,
public string $message,
public array $context = [],
) {}
}
final class LogFileParser
{
private const string PATTERN = '/^\[(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}[^\]]*)\] (\w+)\.(\w+): (.+)$/';
/**
* Parse log file lazily.
*
* @return Generator<int, LogEntry>
*/
public function parse(string $filePath): Generator
{
$file = new SplFileObject($filePath);
$file->setFlags(
SplFileObject::DROP_NEW_LINE
| SplFileObject::SKIP_EMPTY
| SplFileObject::READ_AHEAD,
);
$lineNumber = 0;
foreach ($file as $line) {
$lineNumber++;
if (!preg_match(self::PATTERN, $line, $matches)) {
continue;
}
try {
$timestamp = new \DateTimeImmutable($matches[1]);
} catch (\Exception) {
continue;
}
yield $lineNumber => new LogEntry(
timestamp: $timestamp,
channel: $matches[2],
level: $matches[3],
message: $matches[4],
);
}
}
/**
* Get only ERROR entries from the last N lines.
*
* @return Generator<int, LogEntry>
*/
public function getRecentErrors(string $filePath, int $lastLines = 1000): Generator
{
$file = new SplFileObject($filePath);
$file->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::SKIP_EMPTY);
// Seek to end to find total line count
$file->seek(PHP_INT_MAX);
$totalLines = $file->key();
$offset = max(0, $totalLines - $lastLines);
$limited = new LimitIterator($file, $offset);
foreach ($limited as $line) {
if (!preg_match(self::PATTERN, $line, $matches)) {
continue;
}
if (strtoupper($matches[3]) !== 'ERROR') {
continue;
}
try {
yield new LogEntry(
timestamp: new \DateTimeImmutable($matches[1]),
channel: $matches[2],
level: $matches[3],
message: $matches[4],
);
} catch (\Exception) {
continue;
}
}
}
}
$parser = new LogFileParser();
// Parse entire file lazily
foreach ($parser->parse('/var/log/app/app.log') as $lineNum => $entry) {
if ($entry->level === 'ERROR') {
echo sprintf(
'[%s] %s: %s',
$entry->timestamp->format('Y-m-d H:i:s'),
$entry->channel,
$entry->message,
) . PHP_EOL;
}
}
Утилита резервного копирования файлов
<?php
declare(strict_types=1);
final class FileBackup
{
/** @var list<string> */
private array $log = [];
/**
* @param list<string> $extensions Only backup files with these extensions
* @param list<string> $excludeDirs Skip these directory names
*/
public function __construct(
private readonly string $sourceDir,
private readonly string $backupDir,
private readonly array $extensions = [],
private readonly array $excludeDirs = ['.git', 'vendor', 'node_modules'],
) {}
/**
* @return array{copied: int, skipped: int, errors: int, totalSize: int}
*/
public function backup(): array
{
$stats = ['copied' => 0, 'skipped' => 0, 'errors' => 0, 'totalSize' => 0];
$source = new RecursiveDirectoryIterator(
$this->sourceDir,
RecursiveDirectoryIterator::SKIP_DOTS,
);
// Filter out excluded directories
$filtered = new RecursiveCallbackFilterIterator(
$source,
function (SplFileInfo $file): bool {
if ($file->isDir()) {
return !in_array($file->getFilename(), $this->excludeDirs, true);
}
if ($this->extensions !== []) {
return in_array(
strtolower($file->getExtension()),
$this->extensions,
true,
);
}
return true;
},
);
$iterator = new RecursiveIteratorIterator(
$filtered,
RecursiveIteratorIterator::SELF_FIRST,
);
foreach ($iterator as $item) {
/** @var SplFileInfo $item */
$relativePath = substr($item->getPathname(), strlen($this->sourceDir));
$targetPath = $this->backupDir . $relativePath;
if ($item->isDir()) {
if (!is_dir($targetPath)) {
mkdir($targetPath, 0755, true);
}
continue;
}
// Skip if backup is newer or same
if (file_exists($targetPath) && filemtime($targetPath) >= $item->getMTime()) {
$stats['skipped']++;
continue;
}
// Ensure target directory exists
$targetDir = dirname($targetPath);
if (!is_dir($targetDir)) {
mkdir($targetDir, 0755, true);
}
if (copy($item->getPathname(), $targetPath)) {
$stats['copied']++;
$stats['totalSize'] += $item->getSize();
$this->log[] = "Copied: {$relativePath}";
} else {
$stats['errors']++;
$this->log[] = "Error: {$relativePath}";
}
}
return $stats;
}
/** @return list<string> */
public function getLog(): array
{
return $this->log;
}
}
$backup = new FileBackup(
sourceDir: '/var/www/app/src',
backupDir: '/backups/app-' . date('Y-m-d'),
extensions: ['php', 'yaml', 'twig'],
);
$stats = $backup->backup();
echo sprintf(
'Backup complete: %d copied, %d skipped, %d errors, %.2f MB',
$stats['copied'],
$stats['skipped'],
$stats['errors'],
$stats['totalSize'] / 1048576,
) . PHP_EOL;
Чтение больших файлов порциями
<?php
declare(strict_types=1);
/**
* Read large file in chunks without loading it entirely into memory.
*
* @return Generator<int, string> Yields chunks of specified size
*/
function readChunks(string $filePath, int $chunkSize = 8192): Generator
{
$file = new SplFileObject($filePath, 'r');
while (!$file->eof()) {
$chunk = $file->fread($chunkSize);
if ($chunk !== false && $chunk !== '') {
yield $chunk;
}
}
}
// Calculate file hash without loading entire file
$hashContext = hash_init('sha256');
foreach (readChunks('/data/large-file.dat', 65536) as $chunk) {
hash_update($hashContext, $chunk);
}
$hash = hash_final($hashContext);
echo "SHA-256: {$hash}" . PHP_EOL;
Запомни:
SplFileObjectреализует интерфейсIterator, что позволяет использовать файл вforeach,LimitIterator,RegexIteratorи других SPL-итераторах. ФлагиREAD_CSV,SKIP_EMPTY,DROP_NEW_LINEиREAD_AHEADуправляют поведением при чтении. Для построчного чтения больших файловSplFileObjectпотребляет минимум памяти -- в отличие отfile(), которая загружает весь файл.