SPL (Standard PHP Library) предоставляет богатую коллекцию итераторов, позволяющих обрабатывать данные лениво, эффективно и с минимальным потреблением памяти. Итераторы SPL реализуют паттерн Iterator и могут комбинироваться друг с другом, образуя мощные конвейеры обработки данных.
Иерархия итераторов SPL
Все SPL-итераторы реализуют интерфейс Iterator или OuterIterator. Вот ключевые группы:
- Фильтрующие:
FilterIterator,CallbackFilterIterator,RegexIterator - Ограничивающие:
LimitIterator,NoRewindIterator,InfiniteIterator - Рекурсивные:
RecursiveIteratorIterator,RecursiveDirectoryIterator - Агрегирующие:
AppendIterator,MultipleIterator - Кэширующие:
CachingIterator,RecursiveCachingIterator - Файловые:
DirectoryIterator,FilesystemIterator,GlobIterator
FilterIterator -- фильтрация элементов
FilterIterator -- абстрактный класс. Необходимо реализовать метод accept(), который возвращает true для элементов, проходящих фильтр.
<?php
declare(strict_types=1);
// Filter even numbers from an array
class EvenFilter extends FilterIterator
{
public function accept(): bool
{
return $this->current() % 2 === 0;
}
}
$numbers = new ArrayIterator([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
$even = new EvenFilter($numbers);
foreach ($even as $number) {
echo $number . ' '; // 2 4 6 8 10
}
Практический пример: фильтрация файлов по расширению
<?php
declare(strict_types=1);
class ExtensionFilter extends FilterIterator
{
/** @param list<string> $extensions */
public function __construct(
Iterator $iterator,
private readonly array $extensions,
) {
parent::__construct($iterator);
}
public function accept(): bool
{
$file = $this->current();
if ($file instanceof SplFileInfo) {
return in_array(
strtolower($file->getExtension()),
$this->extensions,
true,
);
}
return false;
}
}
$directory = new DirectoryIterator('/path/to/project');
$phpFiles = new ExtensionFilter($directory, ['php', 'phtml']);
foreach ($phpFiles as $file) {
echo $file->getPathname() . PHP_EOL;
}
CallbackFilterIterator -- фильтрация замыканием
Начиная с PHP 5.4, вместо создания отдельного класса можно использовать CallbackFilterIterator с анонимной функцией.
<?php
declare(strict_types=1);
$data = new ArrayIterator([
['name' => 'Alice', 'age' => 30, 'active' => true],
['name' => 'Bob', 'age' => 17, 'active' => true],
['name' => 'Charlie', 'age' => 25, 'active' => false],
['name' => 'Diana', 'age' => 22, 'active' => true],
]);
// Filter active adults
$filtered = new CallbackFilterIterator($data, function (array $item): bool {
return $item['active'] && $item['age'] >= 18;
});
foreach ($filtered as $person) {
echo $person['name'] . PHP_EOL; // Alice, Diana
}
Callback получает три аргумента
<?php
declare(strict_types=1);
$items = new ArrayIterator(['apple', 'banana', 'avocado', 'blueberry']);
// callback(current, key, iterator)
$startsWithA = new CallbackFilterIterator(
$items,
function (string $value, int $key, Iterator $iterator): bool {
return str_starts_with($value, 'a');
},
);
foreach ($startsWithA as $fruit) {
echo $fruit . PHP_EOL; // apple, avocado
}
LimitIterator -- пагинация и ограничение
LimitIterator извлекает подмножество элементов из итератора, аналогично LIMIT и OFFSET в SQL.
<?php
declare(strict_types=1);
$all = new ArrayIterator(range(1, 100));
// LimitIterator(iterator, offset, count)
$page = new LimitIterator($all, 20, 10); // Skip 20, take 10
foreach ($page as $item) {
echo $item . ' '; // 21 22 23 24 25 26 27 28 29 30
}
Реализация пагинации
<?php
declare(strict_types=1);
function paginate(Iterator $data, int $page, int $perPage): LimitIterator
{
$offset = ($page - 1) * $perPage;
return new LimitIterator($data, $offset, $perPage);
}
$items = new ArrayIterator(range(1, 50));
// Page 3, 10 items per page
$page3 = paginate($items, 3, 10);
foreach ($page3 as $key => $value) {
echo "[$key] => $value" . PHP_EOL;
// [20] => 21, [21] => 22, ..., [29] => 30
}
CachingIterator -- кэширование и предпросмотр
CachingIterator обёртывает итератор и позволяет заглядывать вперёд (lookahead) и кэшировать строковое представление текущего элемента.
<?php
declare(strict_types=1);
$data = new ArrayIterator(['first', 'second', 'third', 'last']);
$caching = new CachingIterator($data, CachingIterator::FULL_CACHE);
foreach ($caching as $item) {
// hasNext() checks if there is a next element
if ($caching->hasNext()) {
echo $item . ', ';
} else {
echo $item . '.'; // No comma for last element
}
}
// Output: first, second, third, last.
Флаги CachingIterator
<?php
declare(strict_types=1);
// CALL_TOSTRING — call __toString() on current element and cache
// TOSTRING_USE_KEY — __toString returns the key
// TOSTRING_USE_CURRENT — __toString returns the current value
// TOSTRING_USE_INNER — __toString returns inner iterator's __toString
// FULL_CACHE — cache all visited elements (accessible via getCache())
$items = new ArrayIterator(['a', 'b', 'c', 'd']);
$cached = new CachingIterator($items, CachingIterator::FULL_CACHE);
// Must iterate to fill cache
foreach ($cached as $item) {
// processing...
}
// Access full cache after iteration
$cache = $cached->getCache();
print_r($cache); // [0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd']
RecursiveIteratorIterator -- обход деревьев
RecursiveIteratorIterator -- один из самых мощных итераторов SPL. Он «расплющивает» рекурсивную структуру в плоскую последовательность.
Режимы обхода
<?php
declare(strict_types=1);
$tree = new RecursiveArrayIterator([
'fruits' => ['apple', 'banana'],
'vegetables' => [
'root' => ['carrot', 'beet'],
'leaf' => ['spinach'],
],
'grains' => ['rice'],
]);
// LEAVES_ONLY (default) — only leaf elements
$leaves = new RecursiveIteratorIterator(
$tree,
RecursiveIteratorIterator::LEAVES_ONLY,
);
foreach ($leaves as $leaf) {
echo $leaf . ' ';
}
// apple banana carrot beet spinach rice
// SELF_FIRST — parent before children
$selfFirst = new RecursiveIteratorIterator(
$tree,
RecursiveIteratorIterator::SELF_FIRST,
);
foreach ($selfFirst as $key => $value) {
$depth = $selfFirst->getDepth();
$indent = str_repeat(' ', $depth);
if (is_array($value)) {
echo "{$indent}{$key}:" . PHP_EOL;
} else {
echo "{$indent}{$key}: {$value}" . PHP_EOL;
}
}
// CHILD_FIRST — children before parent
$childFirst = new RecursiveIteratorIterator(
$tree,
RecursiveIteratorIterator::CHILD_FIRST,
);
Обход файловой системы
<?php
declare(strict_types=1);
// Walk entire directory tree
$directory = new RecursiveDirectoryIterator(
'/path/to/project/src',
RecursiveDirectoryIterator::SKIP_DOTS,
);
$files = new RecursiveIteratorIterator(
$directory,
RecursiveIteratorIterator::SELF_FIRST,
);
foreach ($files as $file) {
/** @var SplFileInfo $file */
$depth = $files->getDepth();
$indent = str_repeat(' ', $depth);
$type = $file->isDir() ? '[DIR]' : '[FILE]';
echo "{$indent}{$type} {$file->getFilename()}" . PHP_EOL;
}
Поиск PHP-файлов рекурсивно
<?php
declare(strict_types=1);
function findPhpFiles(string $directory): Generator
{
$dir = new RecursiveDirectoryIterator(
$directory,
RecursiveDirectoryIterator::SKIP_DOTS,
);
$iterator = new RecursiveIteratorIterator($dir);
$regex = new RegexIterator(
$iterator,
'/\.php$/i',
RegexIterator::MATCH,
);
foreach ($regex as $file) {
yield $file->getPathname();
}
}
foreach (findPhpFiles('/path/to/src') as $phpFile) {
echo $phpFile . PHP_EOL;
}
AppendIterator -- объединение итераторов
AppendIterator последовательно склеивает несколько итераторов в один.
<?php
declare(strict_types=1);
$first = new ArrayIterator([1, 2, 3]);
$second = new ArrayIterator([4, 5, 6]);
$third = new ArrayIterator([7, 8, 9]);
$combined = new AppendIterator();
$combined->append($first);
$combined->append($second);
$combined->append($third);
foreach ($combined as $value) {
echo $value . ' '; // 1 2 3 4 5 6 7 8 9
}
Практический пример: объединение источников данных
<?php
declare(strict_types=1);
// Combine data from multiple CSV files
function readCsvFiles(array $filePaths): AppendIterator
{
$combined = new AppendIterator();
foreach ($filePaths as $path) {
$file = new SplFileObject($path);
$file->setFlags(
SplFileObject::READ_CSV
| SplFileObject::SKIP_EMPTY
| SplFileObject::DROP_NEW_LINE,
);
$combined->append($file);
}
return $combined;
}
$allRows = readCsvFiles([
'/data/january.csv',
'/data/february.csv',
'/data/march.csv',
]);
foreach ($allRows as $row) {
// Process each row from all files
echo implode(' | ', $row) . PHP_EOL;
}
MultipleIterator -- параллельная итерация
MultipleIterator позволяет итерироваться по нескольким итераторам одновременно (как zip в Python).
<?php
declare(strict_types=1);
$names = new ArrayIterator(['Alice', 'Bob', 'Charlie']);
$ages = new ArrayIterator([30, 25, 35]);
$cities = new ArrayIterator(['Moscow', 'London', 'Berlin']);
$multi = new MultipleIterator(MultipleIterator::MIT_KEYS_ASSOC);
$multi->attachIterator($names, 'name');
$multi->attachIterator($ages, 'age');
$multi->attachIterator($cities, 'city');
foreach ($multi as $data) {
echo sprintf(
'%s, %d years old, from %s',
$data['name'],
$data['age'],
$data['city'],
) . PHP_EOL;
}
// Alice, 30 years old, from Moscow
// Bob, 25 years old, from London
// Charlie, 35 years old, from Berlin
Флаги MultipleIterator
<?php
declare(strict_types=1);
// MIT_NEED_ALL (default) — stop when ANY iterator ends
// MIT_NEED_ANY — continue until ALL iterators end
// MIT_KEYS_NUMERIC — keys as [0, 1, 2, ...]
// MIT_KEYS_ASSOC — keys as attached names
$short = new ArrayIterator([1, 2]);
$long = new ArrayIterator([10, 20, 30, 40]);
// MIT_NEED_ALL: stops at the shortest
$multi = new MultipleIterator(MultipleIterator::MIT_NEED_ALL);
$multi->attachIterator($short);
$multi->attachIterator($long);
foreach ($multi as $pair) {
echo $pair[0] . '-' . $pair[1] . ' '; // 1-10 2-20
}
NoRewindIterator -- запрет перемотки
NoRewindIterator предотвращает вызов rewind() на внутреннем итераторе. Полезно для итераторов, которые нельзя перемотать (потоки, генераторы).
<?php
declare(strict_types=1);
function generateNumbers(): Generator
{
yield 1;
yield 2;
yield 3;
}
// Generators cannot be rewound — NoRewindIterator makes this explicit
$noRewind = new NoRewindIterator(generateNumbers());
foreach ($noRewind as $number) {
echo $number . ' '; // 1 2 3
}
// Second iteration produces nothing (no rewind)
foreach ($noRewind as $number) {
echo $number; // Never reached
}
InfiniteIterator -- циклическая итерация
InfiniteIterator бесконечно повторяет внутренний итератор. Обязательно используйте с LimitIterator или break.
<?php
declare(strict_types=1);
$colors = new ArrayIterator(['red', 'green', 'blue']);
$infinite = new InfiniteIterator($colors);
// Without limit — infinite loop!
// Use LimitIterator to constrain
$limited = new LimitIterator($infinite, 0, 7);
foreach ($limited as $color) {
echo $color . ' ';
}
// red green blue red green blue red
Round-robin распределение задач
<?php
declare(strict_types=1);
$workers = new ArrayIterator(['Worker-A', 'Worker-B', 'Worker-C']);
$roundRobin = new InfiniteIterator($workers);
$tasks = ['task1', 'task2', 'task3', 'task4', 'task5', 'task6', 'task7'];
$roundRobin->rewind();
foreach ($tasks as $task) {
$worker = $roundRobin->current();
echo "{$worker} => {$task}" . PHP_EOL;
$roundRobin->next();
}
// Worker-A => task1
// Worker-B => task2
// Worker-C => task3
// Worker-A => task4
// Worker-B => task5
// Worker-C => task6
// Worker-A => task7
RegexIterator -- фильтрация по регулярным выражениям
<?php
declare(strict_types=1);
$logs = new ArrayIterator([
'[ERROR] Database connection failed',
'[INFO] User logged in',
'[ERROR] File not found',
'[WARNING] Low memory',
'[INFO] Request completed',
]);
// Match only ERROR lines
$errors = new RegexIterator($logs, '/^\[ERROR\]/');
foreach ($errors as $line) {
echo $line . PHP_EOL;
}
// [ERROR] Database connection failed
// [ERROR] File not found
Режимы RegexIterator
<?php
declare(strict_types=1);
$data = new ArrayIterator(['foo123', 'bar456', 'baz789', 'hello']);
// MATCH (default) — filter elements matching the pattern
$matched = new RegexIterator($data, '/\d+/', RegexIterator::MATCH);
// foo123, bar456, baz789
// GET_MATCH — return only the matched part
$getMatch = new RegexIterator($data, '/(\d+)/', RegexIterator::GET_MATCH);
foreach ($getMatch as $match) {
echo $match[0] . ' '; // 123 456 789
}
// REPLACE — replace matched part
$replace = new RegexIterator($data, '/\d+/', RegexIterator::REPLACE);
$replace->replacement = 'NUM';
foreach ($replace as $item) {
echo $item . ' '; // fooNUM barNUM bazNUM hello
}
// ALL_MATCHES — return all matches per element
// SPLIT — split each element by pattern
DirectoryIterator vs FilesystemIterator
Два похожих итератора для работы с каталогами, но с важными различиями.
<?php
declare(strict_types=1);
// DirectoryIterator — basic directory listing
$dir = new DirectoryIterator('/path/to/dir');
foreach ($dir as $fileInfo) {
// Includes . and ..
if ($fileInfo->isDot()) {
continue;
}
echo $fileInfo->getFilename() . PHP_EOL;
}
// FilesystemIterator — enhanced version
// Automatically skips . and ..
// Keys are pathnames (not sequential integers)
$fs = new FilesystemIterator('/path/to/dir');
foreach ($fs as $path => $fileInfo) {
echo "{$path} => {$fileInfo->getFilename()}" . PHP_EOL;
}
Ключевые различия
| Характеристика | DirectoryIterator |
FilesystemIterator |
|---|---|---|
Пропуск . и .. |
Нет (вручную) | Да (SKIP_DOTS по умолчанию) |
| Ключи при итерации | Числовые (0, 1, 2...) | Полный путь к файлу |
current() возвращает |
Тот же объект (!) | Новый SplFileInfo |
| Флаги | Нет | Множество (KEY_AS_PATHNAME, CURRENT_AS_FILEINFO и др.) |
Внимание!
DirectoryIteratorвозвращает один и тот же объект при каждой итерации, просто перемещая внутренний указатель. Если вы сохраняете элементы в массив, все элементы будут указывать на последний файл. ИспользуйтеcloneилиFilesystemIterator.
GlobIterator -- поиск по паттерну
<?php
declare(strict_types=1);
// Find all PHP files in a directory
$glob = new GlobIterator('/path/to/src/*.php');
echo "Found {$glob->count()} PHP files" . PHP_EOL;
foreach ($glob as $file) {
/** @var SplFileInfo $file */
echo $file->getFilename() . ' (' . $file->getSize() . ' bytes)' . PHP_EOL;
}
Практические примеры
Сканер файлов проекта
<?php
declare(strict_types=1);
final class ProjectScanner
{
/** @param list<string> $excludeDirs */
public function __construct(
private readonly string $rootPath,
private readonly array $excludeDirs = ['vendor', 'node_modules', '.git'],
) {}
/** @return Generator<string, SplFileInfo> */
public function findFiles(string $pattern = '/\.php$/i'): Generator
{
$directory = new RecursiveDirectoryIterator(
$this->rootPath,
RecursiveDirectoryIterator::SKIP_DOTS,
);
// Filter out excluded directories
$filtered = new RecursiveCallbackFilterIterator(
$directory,
function (SplFileInfo $file, string $key, RecursiveDirectoryIterator $iterator): bool {
if ($file->isDir()) {
return !in_array($file->getFilename(), $this->excludeDirs, true);
}
return true;
},
);
$flattened = new RecursiveIteratorIterator($filtered);
$matched = new RegexIterator($flattened, $pattern);
foreach ($matched as $file) {
yield $file->getPathname() => $file;
}
}
/** @return array{files: int, dirs: int, totalSize: int} */
public function getStats(): array
{
$files = 0;
$dirs = 0;
$totalSize = 0;
foreach ($this->findFiles('/./') as $file) {
if ($file->isFile()) {
$files++;
$totalSize += $file->getSize();
} else {
$dirs++;
}
}
return compact('files', 'dirs', 'totalSize');
}
}
Парсер лог-файлов
<?php
declare(strict_types=1);
final class LogParser
{
public function __construct(
private readonly string $logPath,
) {}
/** @return Generator<int, array{level: string, message: string, timestamp: string}> */
public function parse(string $levelFilter = 'ERROR'): Generator
{
$file = new SplFileObject($this->logPath);
$file->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::SKIP_EMPTY);
// Filter by log level using RegexIterator
$pattern = '/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\] \[(' . $levelFilter . ')\] (.+)$/';
$filtered = new RegexIterator($file, $pattern, RegexIterator::GET_MATCH);
foreach ($filtered as $match) {
yield [
'timestamp' => $match[1],
'level' => $match[2],
'message' => $match[3],
];
}
}
/** @return Generator<int, array<string>> */
public function getLastLines(int $count): Generator
{
$file = new SplFileObject($this->logPath);
$file->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::SKIP_EMPTY);
// Seek to end, then count back
$file->seek(PHP_INT_MAX);
$totalLines = $file->key();
$offset = max(0, $totalLines - $count);
$limited = new LimitIterator($file, $offset, $count);
foreach ($limited as $line) {
yield $line;
}
}
}
CSV-процессор с конвейером итераторов
<?php
declare(strict_types=1);
final class CsvPipeline
{
/**
* Read CSV, filter rows, limit results — all lazy.
*
* @return Iterator<int, array<string>>
*/
public static function process(
string $filePath,
callable $filter,
int $offset = 0,
int $limit = 100,
): Iterator {
// Step 1: Read CSV file lazily
$file = new SplFileObject($filePath);
$file->setFlags(
SplFileObject::READ_CSV
| SplFileObject::SKIP_EMPTY
| SplFileObject::DROP_NEW_LINE
| SplFileObject::READ_AHEAD,
);
// Step 2: Filter rows
$filtered = new CallbackFilterIterator($file, $filter);
// Step 3: Paginate
return new LimitIterator($filtered, $offset, $limit);
}
}
// Usage: process 10M-row CSV with constant memory
$results = CsvPipeline::process(
'/data/huge-dataset.csv',
fn(array $row): bool => isset($row[2]) && (float) $row[2] > 1000.0,
offset: 0,
limit: 50,
);
foreach ($results as $row) {
echo implode(', ', $row) . PHP_EOL;
}
Запомни: SPL-итераторы работают лениво -- данные обрабатываются по одному элементу, без загрузки всей коллекции в память. Это делает их идеальными для обработки больших файлов и потоков данных. Комбинируя итераторы (фильтр + лимит + regex), вы создаёте эффективные конвейеры обработки данных.