SPL: стандартная библиотека, которую часто забывают
SPL, Standard PHP Library, — это набор встроенных интерфейсов и классов для задач, которые в PHP встречаются постоянно: обход последовательностей, работа с файлами как с объектами, очереди, стеки, кучи, map по объектам, стандартные исключения и несколько служебных функций. Это не отдельный пакет Composer и не фреймворк: SPL входит в PHP и доступна в обычном коде без установки зависимостей.
Главная идея SPL — не «заменить массивы», а дать более точную модель там, где обычный array из Массивы как ordered map начинает скрывать намерение. Если у вас просто список настроек или результат SQL-запроса, массив нормален. Если у вас поток записей, очередь задач, обход директории или коллекция объектов с инвариантами, SPL часто делает код честнее.
flowchart TD
SPL[SPL: Standard PHP Library]
SPL --> I[Итерация]
SPL --> F[Файлы]
SPL --> D[Структуры данных]
SPL --> E[Исключения и функции]
I --> Traversable[Traversable]
I --> Iterator[Iterator]
I --> IteratorAggregate[IteratorAggregate]
I --> Ready[ArrayIterator, FilterIterator, RecursiveIteratorIterator]
F --> SplFileInfo[SplFileInfo]
F --> SplFileObject[SplFileObject]
F --> RecursiveDirectoryIterator[RecursiveDirectoryIterator]
D --> Queue[SplQueue]
D --> Stack[SplStack]
D --> Heap[SplMinHeap / SplMaxHeap]
D --> Priority[SplPriorityQueue]
D --> Objects[SplObjectStorage]
D --> ArrayObject[ArrayObject]Traversable, Iterator и IteratorAggregate
foreach в PHP умеет обходить массивы и объекты, которые являются Traversable. Сам Traversable нельзя реализовать напрямую в пользовательском классе; обычно выбирают один из двух путей: реализовать Iterator или IteratorAggregate.
Iterator — низкоуровневый контракт. Класс сам хранит позицию и реализует пять методов: rewind(), valid(), current(), key(), next(). Это полезно, когда объект действительно сам является курсором: например, читает внешний источник порциями или должен контролировать перемещение.
<?php
final class Lines implements Iterator
{
private int $position = 0;
public function __construct(private array $lines) {}
public function rewind(): void
{
$this->position = 0;
}
public function valid(): bool
{
return array_key_exists($this->position, $this->lines);
}
public function current(): string
{
return $this->lines[$this->position];
}
public function key(): int
{
return $this->position;
}
public function next(): void
{
$this->position++;
}
}
foreach (new Lines(['first', 'second']) as $i => $line) {
echo $i . ': ' . $line . PHP_EOL;
}Но в прикладном коде чаще удобнее IteratorAggregate. Класс не притворяется курсором, а отдаёт внешний итератор через getIterator(). Это хорошо ложится на объектные коллекции: внутри можно хранить массив, но наружу отдавать только безопасный обход.
<?php
final class Users implements IteratorAggregate, Countable
{
/** @param list<string> $emails */
public function __construct(private array $emails) {}
public function getIterator(): Traversable
{
foreach ($this->emails as $email) {
yield strtolower($email);
}
}
public function count(): int
{
return count($this->emails);
}
}
$users = new Users(['Admin@Example.test', 'Editor@Example.test']);
foreach ($users as $email) {
echo $email . PHP_EOL;
}Здесь yield связывает SPL с Генераторы и Fiber: генератор уже является Traversable, поэтому его можно вернуть из getIterator(). Это не значит, что каждая коллекция должна быть генератором. Просто IteratorAggregate даёт аккуратную точку, где объект решает, как именно он обходится.
Готовые итераторы: фильтровать, ограничивать, обходить директории
SPL содержит готовые итераторы: ArrayIterator, LimitIterator, CallbackFilterIterator, RegexIterator, FilesystemIterator, RecursiveDirectoryIterator, RecursiveIteratorIterator и другие. Их удобно комбинировать, когда данные уже выглядят как последовательность.
Например, в статье Файловая система и stream wrappers файл рассматривался как поток байтов. SPL добавляет объектный обход файловой системы: директория становится итератором, элементы — объектами с методами.
<?php
$directory = new RecursiveDirectoryIterator(
__DIR__ . '/src',
FilesystemIterator::SKIP_DOTS
);
$files = new RecursiveIteratorIterator($directory);
foreach ($files as $file) {
/** @var SplFileInfo $file */
if ($file->isFile() && $file->getExtension() === 'php') {
echo $file->getPathname() . PHP_EOL;
}
}Такой код читается лучше, чем рекурсивная функция с opendir(), readdir() и ручным склеиванием путей. Но правила безопасности не исчезают: пользовательский путь всё равно нужно валидировать, а доступ к файлам — ограничивать разрешёнными директориями.
SplFileInfo и SplFileObject
SplFileInfo представляет один файл или директорию. Он не читает содержимое сам по себе, а отвечает на вопросы про объект файловой системы: имя, расширение, размер, тип, абсолютный путь, права, readable/writable, является ли элемент файлом или директорией.
<?php
$file = new SplFileInfo(__DIR__ . '/storage/report.csv');
if (!$file->isFile() || !$file->isReadable()) {
throw new RuntimeException('Отчёт недоступен');
}
echo $file->getFilename() . ': ' . $file->getSize() . ' bytes' . PHP_EOL;Если нужно читать строки или CSV, SplFileInfo::openFile() возвращает SplFileObject. Это тот же мост, который уже был в Файловая система и stream wrappers: вместо загрузки файла целиком в память вы обходите его как последовательность строк.
ArrayObject: массив с объектной оболочкой
ArrayObject позволяет объекту вести себя как массив: доступ через $items['key'], count(), сортировки, getIterator(), getArrayCopy(). Он полезен для адаптеров и легаси-кода, где API ожидает массивоподобный объект.
<?php
$config = new ArrayObject([
'debug' => false,
'locale' => 'ru',
]);
$config['debug'] = true;
$config->ksort();
foreach ($config as $key => $value) {
echo $key . '=' . var_export($value, true) . PHP_EOL;
}Но ArrayObject не стоит автоматически использовать как «лучшую коллекцию». Для доменной коллекции обычно яснее собственный класс с IteratorAggregate, Countable и методами вроде active(), byEmail(), add(). ArrayObject::ARRAY_AS_PROPS, где элементы доступны как свойства, может выглядеть удобно, но часто размывает границу между данными и поведением объекта. Если вам важны типы и инварианты, лучше явно описать методы класса; это ближе к темам Классы, объекты и видимость и PHPDoc, generics и статический анализ.
Очереди, стеки, кучи и priority queue
PHP-массив умеет всё понемногу: [], array_pop(), array_shift(), сортировки. SPL-структуры полезны, когда важна именно операция.
SplQueue — FIFO-очередь: первым пришёл, первым вышел. Это естественная модель для локальной очереди задач, обхода графа в ширину, буфера событий.
<?php
$queue = new SplQueue();
$queue->enqueue('resize-image');
$queue->enqueue('send-email');
while (!$queue->isEmpty()) {
echo $queue->dequeue() . PHP_EOL;
}SplStack — LIFO-стек: последним положили, первым забрали. Он подходит для undo-истории, парсинга вложенных структур, обхода дерева без рекурсии.
SplMinHeap, SplMaxHeap и SplPriorityQueue нужны, когда постоянно требуется забирать элемент с минимальным, максимальным или пользовательским приоритетом. SplPriorityQueue реализована на max heap: большее значение приоритета выходит раньше. Важный нюанс: порядок элементов с одинаковым приоритетом не определён. Если порядок равных задач важен, добавляйте в приоритет второй компонент, например монотонный счётчик.
<?php
$jobs = new SplPriorityQueue();
$jobs->insert('send-alert', 100);
$jobs->insert('refresh-cache', 10);
$jobs->insert('write-log', 1);
while (!$jobs->isEmpty()) {
echo $jobs->extract() . PHP_EOL;
}Для реального background processing в веб-приложении обычно берут Redis, RabbitMQ, SQS или фреймворковую очередь; это уже область Очереди, фоновые задачи и воркеры. SPL-очередь живёт в памяти текущего процесса и исчезает после завершения скрипта.
SplObjectStorage: map, где ключ — объект
Обычный PHP-массив не может использовать объект как ключ. SplObjectStorage решает именно эту задачу: хранит набор объектов и при необходимости связанные с ними данные. Это бывает удобно для графов, подписчиков, visited-set при обходе объектов, registry внутри процесса.
<?php
$seen = new SplObjectStorage();
$a = new stdClass();
$b = new stdClass();
$seen[$a] = 'root';
$seen[$b] = 'child';
foreach ($seen as $object) {
echo $seen[$object] . PHP_EOL;
}Если ключи — строки или числа, используйте обычный массив. Если ключи — объекты и важна идентичность конкретного экземпляра, SplObjectStorage выражает это прямо.
Когда SPL лучше массива
SPL стоит брать не ради «более умного PHP», а когда структура данных совпадает с задачей. Очередь честнее массива с array_shift(). IteratorAggregate честнее публичного массива внутри объекта. SplFileInfo честнее строки пути, когда код постоянно спрашивает у файла имя, расширение и размер. SplPriorityQueue честнее ручной сортировки массива после каждого добавления задачи.
Обратная сторона тоже есть. SPL-классы менее привычны многим PHP-разработчикам, а статический анализ лучше понимает простые массивы с хорошими PHPDoc-аннотациями, чем произвольные мутируемые контейнеры. Поэтому практичный критерий простой: если массив ясно передаёт смысл — оставьте массив. Если вокруг массива появились комментарии «это очередь», «это стек», «здесь ключ — объект», «обход ленивый» — скорее всего, пора посмотреть на SPL.
См. также
- Массивы как ordered map — почему обычный
arrayв PHP уже является мощной структурой, но не всегда лучшей моделью. - Файловая система и stream wrappers —
SplFileObject, чтение по частям и безопасная работа с путями. - Генераторы и Fiber —
yieldкак компактный способ вернутьTraversableизIteratorAggregate. - Классы, объекты и видимость — когда коллекцию лучше оформить как доменный объект.
- PHPDoc, generics и статический анализ — как описывать типы элементов в коллекциях.
- Очереди, фоновые задачи и воркеры — где заканчиваются in-memory структуры SPL и начинаются долговечные очереди задач.