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]
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]
SPL удобнее воспринимать не как один класс, а как несколько семейств: итераторы, объектная работа с файлами и специализированные структуры данных.
Quick recall
Почему SPL не стоит воспринимать как «замену массивам» в PHP?

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 даёт аккуратную точку, где объект решает, как именно он обходится.

Quick recall
Когда для собственного класса лучше выбрать `IteratorAggregate`, а не ручную реализацию `Iterator`?

Готовые итераторы: фильтровать, ограничивать, обходить директории

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() и ручным склеиванием путей. Но правила безопасности не исчезают: пользовательский путь всё равно нужно валидировать, а доступ к файлам — ограничивать разрешёнными директориями.

Quick recall
Какой SPL-набор обычно заменяет ручной рекурсивный обход директории через `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.

См. также

Sources

  1. PHP Manual: Standard PHP Library (SPL)
  2. PHP Manual: SPL Datastructures
  3. PHP Manual: Iterator
  4. PHP Manual: IteratorAggregate
  5. PHP Manual: SplFileInfo
  6. PHP Manual: ArrayObject
  7. PHP Manual: SplQueue
  8. PHP Manual: SplPriorityQueue
  9. PHP Manual: RecursiveDirectoryIterator