Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. Интерфейсы коллекций: foreach, скобки, count и строковое представление
  2. Зачем объекту уметь работать с foreach, count и квадратными скобками
  3. Traversable: объект можно передать как последовательность
  4. Iterator: одна закладка и пять методов обхода
  5. IteratorAggregate: коллекция выдаёт отдельную закладку каждому обходу
  6. Countable: определить, что именно считает интерфейс
  7. ArrayAccess: четыре входа для квадратных скобок
  8. Вложенное изменение: почему offsetSet иногда не вызывается
  9. InternalIterator: курсор, который выдаёт встроенный объект
  10. Serializable: прочитать старые данные и перейти к современному договору
  11. Stringable: понятная строка из объекта без потери самого объекта
  12. Выбрать договор под потребность и не обещать лишнего
  13. Функции и методы этой темы
  14. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 69 · От понятного договора к реализации

Интерфейсы коллекций: foreach, скобки, count и строковое представление

Все методы Traversable, Iterator, IteratorAggregate, InternalIterator, Countable, ArrayAccess, Serializable и Stringable: от первой операции до правил собственного контейнера.

Зачем объекту уметь работать с foreach, count и квадратными скобками

Наглядная модель

Как count($basket) попадает в ваш класс

Корзина хранит две позиции. Интерфейс Countable связывает обычную функцию с вашим методом.

  1. echo count($basket)

    Прикладной код запрашивает количество.

  2. Basket implements Countable

    PHP узнаёт, что у объекта есть договор подсчёта.

  3. $basket->count()

    Вызывается написанный вами метод корзины.

  4. return count($this->items)

    Внутренний массив содержит Книгу и Лампу: результат 2.

  5. echo 2

    Результат возвращается в место исходного вызова.

Countable не добавляет обход и доступ по ключу. Для них существуют независимые IteratorAggregate и ArrayAccess.

На сайте есть список товаров. Шаблону нужно вывести их, показать количество, иногда получить товар по артикулу. Пока данные лежат в массиве, подходят foreach, count и квадратные скобки. Когда вы переносите хранение и правила в класс, удобный внешний код хочется сохранить. Предопределённые интерфейсы позволяют договориться с PHP: «при такой операции вызови такие методы моего объекта».

Интерфейс — набор обязательных методов с согласованными параметрами и результатами. Он сам не хранит товары и не содержит вашу бизнес-логику. Слово implements после имени класса объявляет этот договор. public function объявляет доступный метод; $this обозначает текущий объект. private array $items — свойство с массивом, доступное только внутри класса; : int после скобок метода обещает целый результат, который return отдаёт вызывающему коду. Если это пока новые слова, сначала прочитайте класс и экземпляр и интерфейс на примере сервиса. Здесь каждый следующий договор изучаем отдельно.

Одна привычная операция — вызов вашего метода
  1. count($basket) → PHP видит Countable
  2. Вызывает $basket->count()
  3. Метод считает выбранные вами элементы
  4. Возвращённое число попадает в место count($basket)
Показать в корзине полезную сводку позицийСамостоятельный пример
<?php
declare(strict_types=1);

class Basket implements Countable
{
    private array $items = ['Чай', 'Кофе'];
    public function count(): int { return count($this->items); }
}
$basket = new Basket();
echo 'В корзине ', count($basket), " позиции. Можно перейти к оформлению.\n";
Ожидаемый результат
В корзине 2 позиции. Можно перейти к оформлению.

count($basket) обращается к count() класса, потому что он выполняет договор Countable. Возвращается число позиций, а не число единиц товара: следующая часть сравнивает эти два смысла. Сам count не проверяет остаток и возможность оплаты.

Traversable: объект можно передать как последовательность

Сервис экспорта хочет принимать и готовый массив, и объект, который выдаёт записи постепенно. Тип iterable разрешает массив или Traversable. Сам Traversable — маркер без методов. Пользовательский конкретный класс реализует Iterator или IteratorAggregate, которые уже наследуют этот маркер. Одного implements Traversable для такого класса недостаточно.

Вид значенияПоддерживаемый договор
Массивiterable, но не объект Traversable
Iterator / IteratorAggregate / GeneratorОбъект Traversable и iterable
Обычный объект с публичными свойствамиforeach возможен по доступным свойствам; это не обещает iterable
Выбрать вход для общей печати товарных названийСамостоятельный пример
<?php
declare(strict_types=1);

function printNames(iterable $names): void
{
    foreach ($names as $name) { echo 'Этикетка: ', $name, "\n"; }
}
printNames(['Чай']);
printNames(new ArrayIterator(['Кофе']));
$record = (object) ['title' => 'Сахар'];
try { printNames($record); }
catch (TypeError $error) { echo "Обычная запись объекта не выполняет договор iterable\n"; }
foreach ($record as $name => $value) { echo 'Доступное поле ', $name, ': ', $value, "\n"; }
Ожидаемый результат
Этикетка: Чай
Этикетка: Кофе
Обычная запись объекта не выполняет договор iterable
Доступное поле title: Сахар

iterable принимает массив или Traversable. Обычный stdClass допускает специальный foreach по доступным свойствам, но не становится от этого Traversable. Различие видно на реальном типизированном входе printNames. Напрямую пользовательский класс обычно реализует Iterator либо IteratorAggregate, а не один маркер Traversable.

В следующем классе __construct(private array $items) — сокращённое объявление свойства через параметр конструктора: при new ProductList([...]) PHP сохраняет переданный массив в закрытом $this->items. Это называется продвижением свойства; развёрнутая форма объясняется в главе о конструкторах. getIterator(): Traversable обещает вернуть объект обхода.

Один обработчик принимает массив и коллекциюСамостоятельный пример
<?php
declare(strict_types=1);

class ProductList implements IteratorAggregate {
    public function __construct(private array $items) {}
    public function getIterator(): Traversable {
        return new ArrayIterator($this->items);
    }
}

function showNames(iterable $names): void {
    foreach ($names as $name) { echo $name, "\n"; }
}
showNames(['Книга']);
showNames(new ProductList(['Лампа']));
Ожидаемый результат
Книга
Лампа

getIterator передаёт PHP объект обхода. Параметр iterable не требует именно ProductList и не загружает все записи заранее. Но он также не гарантирует повторный обход, известное количество или допустимый тип каждой записи: эти условия описывает ваш API.

С PHP 8.0 абстрактному классу разрешено объявить implements Traversable, но его конкретный потомок всё равно должен реализовать Iterator или IteratorAggregate. До PHP 8.0 это запрещалось и абстрактным пользовательским классам. Встроенные классы PHP могут иметь собственную реализацию Traversable без этих двух интерфейсов. Маркер не имеет конструктора и не создаёт экземпляр через new.

Iterator: одна закладка и пять методов обхода

На одной записи Книга
  1. rewind ставит позицию на начало
  2. valid проверяет существование позиции, не значение элемента
  3. current и key дают товар и ключ
  4. Тело foreach использует запись
  5. next сдвигает позицию; valid решает, продолжать ли

Итератор можно представить как список с закладкой текущей позиции. current читает значение у закладки, key читает ключ, next передвигает её, valid проверяет существование позиции, rewind возвращает к началу. Аналогия не означает, что весь источник уже лежит в памяти: реальный итератор может читать файл, страницу API или вычислять значения.

Начнём со списка в памяти. $position = 0 хранит позицию, array_values убирает исходные ключи и делает последовательные номера. Поэтому эта конкретная реализация намеренно не сохраняет артикулы в ключах. array_key_exists проверяет наличие элемента даже со значением null. Оператор ?? выбирает запасной null при отсутствующем значении; это не проверка окончания обхода.

Выполнение foreach ($cursor as $key => $value)
  1. rewind() → позиция 0
  2. valid() → есть элемент?
  3. Да: current() и key() → переменные цикла
  4. Выполнить тело → next() → снова valid()
  5. Нет: закончить цикл
Полный простой итератор со значением nullСамостоятельный пример
<?php
declare(strict_types=1);

class ProductCursor implements Iterator {
    private int $position = 0;
    private array $items;
    public function __construct(array $items) {
        $this->items = array_values($items);
    }
    public function current(): mixed {
        return $this->items[$this->position] ?? null;
    }
    public function key(): mixed {
        return $this->valid() ? $this->position : null;
    }
    public function next(): void { ++$this->position; }
    public function rewind(): void { $this->position = 0; }
    public function valid(): bool {
        return array_key_exists($this->position, $this->items);
    }
}

$cursor = new ProductCursor(['Книга', null, 'Лампа']);
foreach ($cursor as $key => $value) {
    echo $key, ':', $value ?? '(нет подписи)', "\n";
}
Ожидаемый результат
0:Книга
1:(нет подписи)
2:Лампа

Пять методов вызываются PHP автоматически. Если valid использовал бы isset для внутреннего массива, null ошибочно закончил бы обход на позиции 1. Наш current возвращает null и для существующего null, и вне списка, поэтому завершение определяет именно valid.

Увидеть порядок вызовов на одной записиСамостоятельный пример
<?php
class TracedCursor implements Iterator {
    private int $position = 0;
    public function rewind(): void { echo "rewind\n"; $this->position = 0; }
    public function valid(): bool { echo "valid\n"; return $this->position === 0; }
    public function current(): mixed { echo "current\n"; return 'Книга'; }
    public function key(): mixed { echo "key\n"; return 'A'; }
    public function next(): void { echo "next\n"; ++$this->position; }
}
foreach (new TracedCursor() as $key => $value) {
    echo $key, ':', $value, "\n";
}
Ожидаемый результат
rewind
valid
current
key
A:Книга
next
valid

Здесь обе переменные цикла используются, поэтому видны current и key. Диагностический echo нужен только для учебной трассировки; реальный курсор не должен загрязнять HTTP-ответ служебными строками. break заканчивает обход раньше, без обычного перехода next после прерванного тела.

break оставляет закладку на текущей записиСамостоятельный пример
<?php
declare(strict_types=1);

class ProductCursor implements Iterator {
    private int $position = 0;
    private array $items;
    public function __construct(array $items) {
        $this->items = array_values($items);
    }
    public function current(): mixed {
        return $this->items[$this->position] ?? null;
    }
    public function key(): mixed {
        return $this->valid() ? $this->position : null;
    }
    public function next(): void { ++$this->position; }
    public function rewind(): void { $this->position = 0; }
    public function valid(): bool {
        return array_key_exists($this->position, $this->items);
    }
}

$cursor = new ProductCursor(['Книга', 'Лампа']);
foreach ($cursor as $value) {
    echo $value, "\n";
    break;
}
echo $cursor->current(), "\n";
$cursor->next();
echo $cursor->current(), "\n";
Ожидаемый результат
Книга
Книга
Лампа

Iterator не содержит события «цикл прервали». Освобождение внешних ресурсов нельзя завязывать только на next после последней записи. Используйте явное закрытие и finally, а для потоковых последовательностей изучите Generator и его время жизни.

IteratorAggregate: коллекция выдаёт отдельную закладку каждому обходу

Два вложенных цикла по одному курсору используют одну позицию: внутренний цикл сдвигает её у внешнего. Коллекции обычно удобнее реализовать IteratorAggregate: она хранит данные и при каждом getIterator создаёт новый курсор. Сам интерфейс не обещает новую закладку — это решение реализации, поэтому ниже явно стоит new ArrayIterator.

Две независимые итерации одного каталога
  1. Внешний foreach → getIterator() → курсор A
  2. Внутренний foreach → getIterator() → курсор B
  3. Курсор B дошёл до конца → курсор A остаётся на своей позиции
  4. Внешний цикл продолжает следующий товар
Все сочетания товаров без потери позицииСамостоятельный пример
<?php
declare(strict_types=1);

class ProductList implements IteratorAggregate {
    public function __construct(private array $items) {}
    public function getIterator(): Traversable {
        return new ArrayIterator($this->items);
    }
}

$products = new ProductList(['A', 'B']);
foreach ($products as $left) {
    foreach ($products as $right) {
        echo $left, '+', $right, "\n";
    }
}
Ожидаемый результат
A+A
A+B
B+A
B+B

Четыре результата подтверждают независимые позиции. Повторное возвращение одного сохранённого Iterator из getIterator этой гарантии не дало бы. Сами объекты-товары при этом могут оставаться общими: отдельный курсор не означает глубокое клонирование содержимого.

Почему один курсор неудобен для вложенного обходаСамостоятельный пример
<?php
declare(strict_types=1);

class ProductCursor implements Iterator {
    private int $position = 0;
    private array $items;
    public function __construct(array $items) {
        $this->items = array_values($items);
    }
    public function current(): mixed {
        return $this->items[$this->position] ?? null;
    }
    public function key(): mixed {
        return $this->valid() ? $this->position : null;
    }
    public function next(): void { ++$this->position; }
    public function rewind(): void { $this->position = 0; }
    public function valid(): bool {
        return array_key_exists($this->position, $this->items);
    }
}

$cursor = new ProductCursor(['A', 'B']);
foreach ($cursor as $left) {
    foreach ($cursor as $right) { echo $left, '+', $right, "\n"; }
}
Ожидаемый результат
A+A
A+B

Внутренний foreach исчерпал ту же позицию. Внешний уже не попал на B. Это наблюдаемое поведение конкретного конечного курсора, а не универсальный рецепт: другие вложенные схемы с break способны зациклиться. Возвращайте свежий итератор или осознанно организуйте независимое состояние.

getIterator возвращает Traversable, а не массив. Можно вернуть ArrayIterator или Generator, который получается у метода с yield. Прямой return $this->items с типом Traversable вызовет TypeError. Интерфейс появился в PHP 5; современные возвращаемые типы в примерах соответствуют PHP 8. В старых библиотеках возможен атрибут ReturnTypeWillChange для переходного периода — собственный новый код лучше сразу объявлять с подходящими типами.

Countable: определить, что именно считает интерфейс

В корзине две позиции, но пять единиц товара. Оба числа полезны, поэтому договор нужно назвать и описать. count($basket) обычно считает позиции; totalQuantity() отдельно суммирует количество. Countable требует только count(): int и не устанавливает, насколько быстро он выполняется.

Различить позиции и единицы товараСамостоятельный пример
<?php
class Basket implements Countable {
    private array $quantities = [2, 3];
    public function count(): int { return count($this->quantities); }
    public function totalQuantity(): int { return array_sum($this->quantities); }
}
$basket = new Basket();
echo 'Позиций: ', count($basket), "\n";
echo 'Единиц: ', $basket->totalQuantity(), "\n";
Ожидаемый результат
Позиций: 2
Единиц: 5

Два метода отвечают на разные вопросы. count с COUNT_RECURSIVE не передаёт режим вашему count(): параметров у интерфейсного метода нет. Он не заставляет пользовательский объект автоматически считать вложенные элементы.

Если count делает SQL-запрос или проходит весь генератор, его повторение в условии цикла может быть дорогим или менять состояние. Лучше явный метод получения количества, сохранённое число при неизменном наборе либо серверный COUNT по нужному фильтру. iterator_count проходит источник и может исчерпать его; наличие Traversable само по себе не означает Countable.

Перебираемость не обещает быстрый или доступный подсчётСамостоятельный пример
<?php
declare(strict_types=1);

class ProductList implements IteratorAggregate {
    public function __construct(private array $items) {}
    public function getIterator(): Traversable {
        return new ArrayIterator($this->items);
    }
}

$products = new ProductList(['Книга', 'Лампа']);
foreach ($products as $name) { echo "Товар: ", $name, "\n"; }
echo is_countable($products) ? "Есть договор подсчёта\n" : "Отдельного договора подсчёта нет\n";
try { count($products); }
catch (TypeError $error) { echo "Нужен Countable или массив\n"; }
Ожидаемый результат
Товар: Книга
Товар: Лампа
Отдельного договора подсчёта нет
Нужен Countable или массив

Коллекция умеет выдавать элементы, но не реализует Countable. Поэтому count вызывает TypeError, хотя foreach уже сработал. Для потока подсчёт через полный проход может выполнить всю дорогую работу и исчерпать источник; не добавляйте его незаметно ради бейджа.

ArrayAccess: четыре входа для квадратных скобок

Начните с привычного массива: $settings["theme"] = "dark" сохраняет настройку, чтение получает её, isset спрашивает наличие, unset удаляет. ArrayAccess позволяет объекту перехватить эти четыре операции и задать собственные проверки. Квадратные скобки сами не дают foreach или count — для них нужны другие договоры.

Запись вызывающегоКакой метод PHP вызовет
$settings["theme"]offsetGet("theme")
$settings["theme"] = "dark"offsetSet("theme", "dark")
isset($settings["theme"])offsetExists("theme")
unset($settings["theme"])offsetUnset("theme")

Контейнер настроек может дать привычное $settings["theme"], при этом проверить допустимость ключей и сообщить о пропущенной настройке. ArrayAccess предоставляет четыре входа. В полном примере ниже private $items хранит данные, requireKey проверяет int|string, throw прекращает ошибочный вызов с исключением. null у offsetSet означает добавление без явно заданного ключа.

Запись в прикладном кодеМетод объектаЧто передаёт PHP
$settings["theme"]offsetGetКлюч theme
$settings["theme"] = "dark"offsetSetКлюч theme и значение dark
isset($settings["theme"])offsetExistsКлюч theme
unset($settings["theme"])offsetUnsetКлюч theme
Включить и удалить настройку темыСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$this->requireKey($offset)]);
    }
    public function offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings['theme'] = 'dark';
echo "Тема: ", $settings['theme'], "\n";
echo isset($settings['theme']) ? "Тема задана\n" : "Будет использована тема по умолчанию\n";
unset($settings['theme']);
echo isset($settings['theme']) ? "Тема задана\n" : "Будет использована тема по умолчанию\n";
Ожидаемый результат
Тема: dark
Тема задана
Будет использована тема по умолчанию

Класс показывает полный договор четырёх операций. offsetSet получает null для записи через пустые скобки [] и добавляет следующий элемент; для именованной настройки принимает int/string. В этом варианте offsetExists использует isset, поэтому значение null считается отсутствующим. Такая политика должна быть явной.

empty сначала вызывает offsetExists; если тот вернул false, результат empty сразу true. Если true — PHP вызывает offsetGet и проверяет полученное значение на пустоту. Поэтому 0, "0", false и null требуют ясного контракта. В нашем контейнере isset внутреннего массива делает null «не установленным», но прямое чтение null разрешено через array_key_exists.

Проследить empty и issetСамостоятельный пример
<?php
class Probe implements ArrayAccess {
    public function offsetExists(mixed $offset): bool {
        echo "exists:$offset\n"; return $offset === 'zero';
    }
    public function offsetGet(mixed $offset): mixed { echo "get:$offset\n"; return 0; }
    public function offsetSet(mixed $offset, mixed $value): void {}
    public function offsetUnset(mixed $offset): void {}
}
$probe = new Probe();
var_dump(isset($probe['zero']));
var_dump(empty($probe['zero']));
var_dump(empty($probe['missing']));
Ожидаемый результат
exists:zero
bool(true)
exists:zero
get:zero
bool(true)
exists:missing
bool(true)

isset не прочитал значение через get. empty прочитал только существующее zero. Пустые методы set/unset в этом диагностическом классе не сохраняют ничего и не подходят как готовый контейнер приложения; рабочая реализация находится выше.

Вложенное изменение: почему offsetSet иногда не вызывается

Запись $settings["mail"] = [...] полностью заменяет элемент и вызывает offsetSet. Запись $settings["mail"]["enabled"] = true сначала получает внутреннее значение через offsetGet, а затем пытается изменить его часть. При обычном возврате массива по значению изменение не дойдёт до хранилища и PHP сообщит об indirect modification. То же касается ++/-- и получения элемента по ссылке.

СпособПуть измененияГде проверяется запись
Явная обратная записьПрочитать → изменить локальный массив → записать весь элементЧерез offsetSet
Getter по ссылкеПолучить ссылку на внутренний элемент → изменить частьoffsetSet не участвует; это часть договора контейнера
Включить почтовые уведомления с явной записью изменённой настройкиСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$this->requireKey($offset)]);
    }
    public function offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings['mail'] = ['enabled' => false];
$mail = $settings['mail'];
$mail['enabled'] = true;
$settings['mail'] = $mail;
echo $settings['mail']['enabled'] ? "Почтовые уведомления включены\n" : "Почтовые уведомления выключены\n";
Ожидаемый результат
Почтовые уведомления включены

offsetGet вернул массив по значению: $mail — изменяемая локальная копия. Последняя запись $settings["mail"] = $mail вызывает offsetSet и сохраняет результат. Это ясная граница проверки всего нового значения. Прямая вложенная запись не обязана вызвать offsetSet.

Разрешить прямое вложенное изменение и понимать ценуСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$this->requireKey($offset)]);
    }
    public function &offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings['mail'] = ['enabled' => false];
$settings['mail']['enabled'] = true;
echo $settings['mail']['enabled'] ? "Почтовые уведомления включены напрямую\n" : "Почтовые уведомления выключены\n";
Ожидаемый результат
Почтовые уведомления включены напрямую

&offsetGet возвращает связь с хранимым элементом, поэтому вложенная запись изменяет его на месте. Она обходит целостную проверку в offsetSet. Это осознанный более открытый договор; для настроек с инвариантами предпочтительнее явный метод enableMail() или прочитать → изменить → записать обратно.

Возврат ссылки из offsetGet совместим с интерфейсом в современных PHP; старые обсуждения до PHP 5.3.4 содержат уже неактуальные ограничения. Ссылка на отсутствующий элемент может незаметно создать его, если просто вернуть $this->items[$key]. Наш вариант сначала проверяет наличие и выбрасывает исключение. Подробнее: семантика ссылок.

InternalIterator: курсор, который выдаёт встроенный объект

Некоторые встроенные классы предоставляют обход через final InternalIterator. Создавать его вручную не нужно и нельзя: конструктор private. Получите его у объекта-владельца. В примере WeakMap хранит подписи по объектам-ключам, а getIterator возвращает готовый курсор. Переменные $first/$second намеренно сохраняют ключи живыми.

Получить встроенный курсор картыСамостоятельный пример
<?php
declare(strict_types=1);

$first = new stdClass();
$second = new stdClass();
$map = new WeakMap();
$map[$first] = 'Первый';
$map[$second] = 'Второй';
$cursor = $map->getIterator();

echo get_class($cursor), "\n";
$cursor->rewind();
while ($cursor->valid()) {
    echo $cursor->current(), "\n";
    $cursor->next();
}
Ожидаемый результат
InternalIterator
Первый
Второй

Методы знакомы по Iterator. key этого курсора возвращает объект, потому что источник — WeakMap, а не массив. Не преобразуйте такой обход в массив с сохранением ключей: объекты не могут быть ключами обычного массива. Поведение и изменение источника определяет сам встроенный контейнер.

Класс доступен с PHP 8.0 и не предназначен для наследования. current и key возвращают mixed; next и rewind ничего не возвращают, valid — bool. Проверяйте valid перед чтением позиции: не опирайтесь на случайное значение вне диапазона. Общая оболочка не обещает кэширование, сортировку, независимые копии или пригодность для многократного чтения любого источника.

Serializable: прочитать старые данные и перейти к современному договору

Сериализация превращает состояние объекта в строку, чтобы сохранить его и позже восстановить. Она не равна выводу подписи пользователю и не заменяет JSON-контракт внешнего API. Исторический интерфейс Serializable использует методы serialize(): ?string и unserialize(string $data): void. Современный новый класс обычно использует магические __serialize(): array и __unserialize(array $data): void, доступные с PHP 7.4.

С PHP 8.1 класс, реализующий Serializable без обоих современных магических методов, получает уведомление deprecated. Если объявлены все четыре метода, новая serialize($object) предпочитает __serialize. Но старый сохранённый формат C по-прежнему требует Serializable::unserialize. Современный формат O направляется в __unserialize. Поэтому при миграции недостаточно просто удалить интерфейс, если в базе ещё лежат старые строки.

Операция в классе с четырьмя методамиКакой путь выбирает PHP
Новая serialize($task)__serialize() → современный формат O
Чтение формата O__unserialize(array) → проверка и восстановление
Чтение старого формата CИнтерфейсный unserialize(string) → проверка и восстановление
Новые записи используют __serializeСамостоятельный пример
<?php
declare(strict_types=1);

class SavedTask implements Serializable {
    public static int $constructed = 0;
    public function __construct(public string $label) {
        ++self::$constructed;
    }
    public function __serialize(): array {
        return ['label' => $this->label];
    }
    public function __unserialize(array $data): void {
        if (!isset($data['label']) || !is_string($data['label'])) {
            throw new UnexpectedValueException('Нет строковой метки');
        }
        $this->label = $data['label'];
    }
    public function serialize(): ?string {
        return json_encode($this->__serialize(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
    }
    public function unserialize(string $data): void {
        $decoded = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($decoded)) {
            throw new UnexpectedValueException('Ожидался объект данных');
        }
        $this->__unserialize($decoded);
    }
}

$task = new SavedTask('Заказ');
$payload = serialize($task);
echo substr($payload, 0, 2), "\n";
$restored = unserialize($payload, ['allowed_classes' => [SavedTask::class]]);
echo $restored->label, "\n";
echo SavedTask::$constructed, "\n";
Ожидаемый результат
O:
Заказ
1

Конструктор вызван только у исходного new. Восстановление проверяет и присваивает поля через __unserialize, не вызывает __construct автоматически. Пример работает только с собственной строкой, созданной выше. allowed_classes ограничивает классы, но не превращает произвольный внешний unserialize в безопасный JSON-парсер.

Существующие старые записи проходят отдельный путьСамостоятельный пример
<?php
declare(strict_types=1);

class SavedTask implements Serializable {
    public static int $constructed = 0;
    public function __construct(public string $label) {
        ++self::$constructed;
    }
    public function __serialize(): array {
        return ['label' => $this->label];
    }
    public function __unserialize(array $data): void {
        if (!isset($data['label']) || !is_string($data['label'])) {
            throw new UnexpectedValueException('Нет строковой метки');
        }
        $this->label = $data['label'];
    }
    public function serialize(): ?string {
        return json_encode($this->__serialize(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
    }
    public function unserialize(string $data): void {
        $decoded = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($decoded)) {
            throw new UnexpectedValueException('Ожидался объект данных');
        }
        $this->__unserialize($decoded);
    }
}

$body = '{"label":"Архив"}';
$name = SavedTask::class;
$payload = 'C:' . strlen($name) . ':"' . $name . '":' . strlen($body) . ':{' . $body . '}';
$restored = unserialize($payload, ['allowed_classes' => [SavedTask::class]]);
echo $restored->label, "\n";
echo SavedTask::$constructed, "\n";
Ожидаемый результат
Архив
0

Строка C специально построена из контролируемых локальных данных для демонстрации совместимости; размеры измеряются в байтах strlen. В прикладном коде не собирайте формат PHP вручную. Интерфейсный unserialize действует как восстановитель объекта; обычный конструктор не запускается.

Serializable заменяет __sleep/__wakeup старым собственным протоколом. Сериализация сама по себе не вызывает деструктор; дополнительные действия происходят только если ваш код их выполняет. Метод serialize должен вернуть string или null. Метод unserialize получает строковое содержимое и его возвращаемое значение не используется. Магические методы не имеют отдельного интерфейса: класс только с __serialize не становится Serializable. Для нового API лучше явный собственный контракт, а не проверка устаревшего интерфейса.

Stringable: понятная строка из объекта без потери самого объекта

Номер заказа можно хранить объектом с правилами, но выводить как ORD-42. Метод __toString возвращает эту строку. Stringable появился в PHP 8.0; класс с __toString автоматически его реализует, хотя явное implements Stringable помогает читателю увидеть договор. Это не выгрузка всех полей и не HTML-экранирование.

Преобразование номера заказа в подпись
  1. Объект OrderNumber хранит число 42
  2. echo или (string) требует строку
  3. __toString() возвращает "ORD-42"
  4. Исходный объект остаётся объектом
Номер заказа можно передать в текстовую подписьСамостоятельный пример
<?php
declare(strict_types=1);

class OrderNumber implements Stringable
{
    public function __construct(private int $number) {}
    public function __toString(): string { return 'ORD-' . $this->number; }
}
function receiptTitle(string|Stringable $number): string
{
    return 'Чек для ' . (string) $number;
}
$number = new OrderNumber(42);
echo receiptTitle($number), "\n";
echo receiptTitle('Внешний заказ B-7'), "\n";
Ожидаемый результат
Чек для ORD-42
Чек для Внешний заказ B-7

string|Stringable принимает готовый текст или объект со строковым представлением. Явное implements здесь документирует намерение; PHP также связывает подходящий __toString со Stringable. В strict_types чистый параметр string не обязан принимать объект автоматически — это следующий отдельный опыт.

При strict_types строковый параметр не принимает объект автоматическиСамостоятельный пример
<?php
declare(strict_types=1);

class OrderNumber implements Stringable {
    public function __construct(private int $number) {}
    public function __toString(): string {
        return 'ORD-' . $this->number;
    }
}

function showText(string $text): void { echo $text, "\n"; }
function showLabel(string|Stringable $text): void { echo (string) $text, "\n"; }
$number = new OrderNumber(42);
try { showText($number); }
catch (TypeError $error) { echo "Нужно явное преобразование\n"; }
showText((string) $number);
showLabel($number);
Ожидаемый результат
Нужно явное преобразование
ORD-42
ORD-42

Этот самостоятельный пример начинается с declare(strict_types=1). string|Stringable явно разрешает обе формы. В нестрогом вызове допустимо скалярное преобразование объекта с __toString к string; тип Stringable сам по себе при этом не принимает обычную строку, потому что она не объект.

Не выполняйте скрытые HTTP-запросы или тяжёлые вычисления в __toString: случайный вывод объекта тогда становится дорогой операцией. С PHP 7.4 __toString может выбросить исключение; в PHP 8 неправильный результат приводит к ошибке типа при строгом объявлении. Держите представление предсказуемым и отдельно предоставляйте сложную загрузку/форматирование.

Выбрать договор под потребность и не обещать лишнего

ЗадачаДоговорЧего он сам не добавляет
Передать последовательность из массива или объектаiterable / TraversableКоличество, повторяемость, доступ по ключу
Управлять одной текущей позициейIteratorНезависимые вложенные обходы
Выдавать отдельный обход коллекцииIteratorAggregateОбязательное создание свежего курсора
Получить количество объектов/позицийCountableДешевизну подсчёта, foreach
Работать с объектом через квадратные скобкиArrayAccessОстальные функции массива
Читать исторический PHP-формат CSerializable + современные magic-методыБезопасность произвольных внешних данных
Получить строковую подпись объектаStringableJSON/HTML/полный снимок состояния

Один класс может реализовать несколько независимых интерфейсов. Для каталога часто достаточно IteratorAggregate и Countable, а изменение лучше оставить явным addProduct(), если требуются проверки. Для простой последовательности расчёта генератор короче собственного Iterator. Для массива без собственных правил дополнительный класс может вообще не потребоваться.

Коллекция для шаблона: список и число без открытой записиСамостоятельный пример
<?php
class Catalog implements IteratorAggregate, Countable {
    public function __construct(private array $names) {}
    public function getIterator(): Traversable { return new ArrayIterator($this->names); }
    public function count(): int { return count($this->names); }
}
$catalog = new Catalog(['Книга', 'Лампа']);
echo 'Найдено: ', count($catalog), "\n";
foreach ($catalog as $name) { echo '- ', $name, "\n"; }
Ожидаемый результат
Найдено: 2
- Книга
- Лампа

В шаблоне остаются знакомые операции, а объект хранит данные закрыто. В реальном HTML дополнительно экранируйте $name. Этот учебный контейнер не проверяет тип каждого элемента входного массива; проверка каталожных записей — отдельный шаг, не свойство интерфейса.

Дальше: проектирование интерфейсов, потоковая обработка, WeakMap, исследование методов и интерфейсов объекта.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

Iterator::currentПрочитать значение у текущей закладки, не передвигая её.

PHP 5+ · Core

public function current(): mixed

Прочитать значение у текущей закладки, не передвигая её.

Часть договораЧто означает
АргументыНет.
РезультатЗначение любого типа; завершение проверяйте отдельно через valid.
ПоложениеЧтение не должно само переходить на следующую запись.
Одно положение — повторное чтение
  1. Позиция 0 → current() → Книга
  2. Снова current() → всё ещё Книга
  3. next() → позиция 1 → current() → Лампа
Прочитать текущий товар, не двигая курсорСамостоятельный пример
<?php
declare(strict_types=1);

$cursor = new ArrayIterator(['Чай', 'Кофе']);

echo 'На этикетке: ', $cursor->current(), "\n";
echo 'Повторное чтение: ', $cursor->current(), "\n";
$cursor->next();
echo 'Следующий товар: ', $cursor->current(), "\n";
Ожидаемый результат
На этикетке: Чай
Повторное чтение: Чай
Следующий товар: Кофе

current читает значение текущей позиции; повторное чтение не двигает позицию. next выполняет отдельный переход. ArrayIterator — готовая реализация Iterator; собственная реализация пяти методов разобрана в главе.

null является допустимой записьюСамостоятельный пример
<?php
declare(strict_types=1);

class ProductCursor implements Iterator {
    private int $position = 0;
    private array $items;
    public function __construct(array $items) {
        $this->items = array_values($items);
    }
    public function current(): mixed {
        return $this->items[$this->position] ?? null;
    }
    public function key(): mixed {
        return $this->valid() ? $this->position : null;
    }
    public function next(): void { ++$this->position; }
    public function rewind(): void { $this->position = 0; }
    public function valid(): bool {
        return array_key_exists($this->position, $this->items);
    }
}

$cursor = new ProductCursor([null]);
var_dump($cursor->valid(), $cursor->current());
$cursor->next();
var_dump($cursor->valid(), $cursor->current());
Ожидаемый результат
bool(true)
NULL
bool(false)
NULL

Одинаковый результат current не различает элемент null и конец. Нельзя писать while ($cursor->current()) для обхода данных, где допустимы null, 0, false или пустая строка. Поведение чтения вне позиции выбирает реализация, здесь она возвращает null.

Связанные методы: Iterator::valid, Iterator::next, Iterator::key. Карта интерфейсов.

Iterator::keyПрочитать идентификатор текущего элемента.

PHP 5+ · Core

public function key(): mixed

Прочитать идентификатор текущего элемента.

Часть договораЧто означает
АргументыНет.
РезультатКлюч текущей записи. Для обычной совместимости с массивами выбирайте int|string.
Вне позицииВ справке описан null и возможный E_NOTICE при неудаче; конкретная реализация определяет поведение. Проверяйте valid.
Ключ и значение отвечают на разные вопросы
  1. key() → где/под каким именем
  2. current() → какие данные
  3. next() меняет текущую пару
Получить подпись и номер позиции каталогаСамостоятельный пример
<?php
declare(strict_types=1);

$cursor = new ArrayIterator(['Чай', 'Кофе']);

echo 'Позиция ', $cursor->key(), ': ', $cursor->current(), "\n";
$cursor->next();
echo 'Позиция ', $cursor->key(), ': ', $cursor->current(), "\n";
Ожидаемый результат
Позиция 0: Чай
Позиция 1: Кофе

key возвращает ключ текущего элемента. В этом списке ключи 0 и 1; в карте артикулов они могли бы быть строками. Интерфейс не обязывает ключ быть целым; следующий пример показывает артикул как ключ.

Артикул как ключ готового итератораСамостоятельный пример
<?php
$cursor = new ArrayIterator(['A-10' => 'Книга', 'B-20' => 'Лампа']);
$cursor->rewind();
while ($cursor->valid()) {
    echo $cursor->key(), ':', $cursor->current(), "\n";
    $cursor->next();
}
Ожидаемый результат
A-10:Книга
B-20:Лампа

Тип mixed в интерфейсе шире int|string. Некоторые источники, например WeakMap, дают объект-ключ через InternalIterator; его нельзя автоматически сохранить как ключ обычного массива. Ожидаемый тип ключа включайте в описание своей коллекции.

Связанные методы: Iterator::current, Iterator::valid, InternalIterator::key. Карта интерфейсов.

Iterator::nextПередвинуть закладку на следующую запись.

PHP 5+ · Core

public function next(): void

Передвинуть закладку на следующую запись.

Часть договораЧто означает
АргументыНет.
РезультатЗначения нет; это действие, а не чтение следующего элемента.
В foreachВызывается после завершённого тела итерации, затем valid. При break обычный следующий шаг не выполняется.
Следующий элемент требует двух действий
  1. next() → перемещение
  2. valid() → существует ли позиция
  3. current() → прочитать данные
Пропустить уже обработанный товарСамостоятельный пример
<?php
declare(strict_types=1);

$cursor = new ArrayIterator(['Чай', 'Кофе']);

echo 'Уже обработан: ', $cursor->current(), "\n";
$cursor->next();
if ($cursor->valid()) { echo 'Теперь обработать: ', $cursor->current(), "\n"; }
Ожидаемый результат
Уже обработан: Чай
Теперь обработать: Кофе

next меняет позицию и возвращает void. Проверка valid перед чтением предотвращает попытку использовать несуществующий элемент после конца. Для обычного полного прохода эти действия делает foreach.

Ручной цикл по известному договоруСамостоятельный пример
<?php
declare(strict_types=1);

class ProductCursor implements Iterator {
    private int $position = 0;
    private array $items;
    public function __construct(array $items) {
        $this->items = array_values($items);
    }
    public function current(): mixed {
        return $this->items[$this->position] ?? null;
    }
    public function key(): mixed {
        return $this->valid() ? $this->position : null;
    }
    public function next(): void { ++$this->position; }
    public function rewind(): void { $this->position = 0; }
    public function valid(): bool {
        return array_key_exists($this->position, $this->items);
    }
}

$cursor = new ProductCursor(['A', 'B']);
$cursor->rewind();
while ($cursor->valid()) {
    echo $cursor->current(), "\n";
    $cursor->next();
}
var_dump($cursor->valid());
Ожидаемый результат
A
B
bool(false)

Забытый next сделал бы этот цикл бесконечным. Обычному выводу списка лучше подходит foreach; ручное управление полезно при пошаговой обработке, объединении потоков и отладке курсора.

Связанные методы: Iterator::current, Iterator::valid, Iterator::rewind. Карта интерфейсов.

Iterator::rewindУстановить начало обхода.

PHP 5+ · Core

public function rewind(): void

Установить начало обхода.

Часть договораЧто означает
АргументыНет.
РезультатЗначения нет.
Автоматический вызовforeach вызывает rewind перед началом, а не после окончания. Ранее выбранная вручную позиция обычно сбрасывается.
Начало нового foreach
  1. Курсор был на позиции 1
  2. foreach → rewind() → позиция 0
  3. Первым снова читается A
Начать новый полный проход с первой позицииСамостоятельный пример
<?php
declare(strict_types=1);

$cursor = new ArrayIterator(['Чай', 'Кофе']);

$cursor->next();
echo 'До прохода: ', $cursor->current(), "\n";
foreach ($cursor as $title) { echo 'Этикетка: ', $title, "\n"; }
Ожидаемый результат
До прохода: Кофе
Этикетка: Чай
Этикетка: Кофе

foreach вызывает rewind перед обходом, поэтому печать началась с Чая. Конкретный Iterator обязан определить допустимость перемотки; Generator нельзя считать произвольно перематываемым массивом. NoRewindIterator в следующем примере сохраняет текущую позицию.

Обойти остаток без автоматического сбросаСамостоятельный пример
<?php
declare(strict_types=1);

class ProductCursor implements Iterator {
    private int $position = 0;
    private array $items;
    public function __construct(array $items) {
        $this->items = array_values($items);
    }
    public function current(): mixed {
        return $this->items[$this->position] ?? null;
    }
    public function key(): mixed {
        return $this->valid() ? $this->position : null;
    }
    public function next(): void { ++$this->position; }
    public function rewind(): void { $this->position = 0; }
    public function valid(): bool {
        return array_key_exists($this->position, $this->items);
    }
}

$cursor = new ProductCursor(['A', 'B', 'C']);
$cursor->next();
foreach (new NoRewindIterator($cursor) as $value) {
    echo $value, "\n";
}
Ожидаемый результат
B
C

NoRewindIterator из SPL подавляет сброс при начале foreach. Это полезно при продолжении уже начатого чтения. Он не клонирует источник: исходный курсор тоже продвигается.

Связанные методы: Iterator::next, Iterator::valid, IteratorAggregate::getIterator. Карта интерфейсов.

Iterator::validПроверить, есть ли элемент в текущей позиции.

PHP 5+ · Core

public function valid(): bool

Проверить, есть ли элемент в текущей позиции.

Часть договораЧто означает
АргументыНет.
Результатtrue — позиция существует, false — закончить обход.
ВызовПосле rewind и каждого next. Истинность самого значения не определяет существование позиции.
Наличие отличается от содержимого
  1. Позиция существует → valid true
  2. Значение равно null или 0 → это всё ещё элемент
  3. Позиция за концом → valid false
Не потерять нули и falseСамостоятельный пример
<?php
declare(strict_types=1);

class ProductCursor implements Iterator {
    private int $position = 0;
    private array $items;
    public function __construct(array $items) {
        $this->items = array_values($items);
    }
    public function current(): mixed {
        return $this->items[$this->position] ?? null;
    }
    public function key(): mixed {
        return $this->valid() ? $this->position : null;
    }
    public function next(): void { ++$this->position; }
    public function rewind(): void { $this->position = 0; }
    public function valid(): bool {
        return array_key_exists($this->position, $this->items);
    }
}

$cursor = new ProductCursor([0, false, null]);
foreach ($cursor as $value) { var_dump($value); }
Ожидаемый результат
int(0)
bool(false)
NULL

array_key_exists в реализации valid сохраняет все три позиции. isset вернул бы false для null, а проверка current на истинность отсеяла бы ещё и ноль/false.

Пустой каталог не должен выдавать выдуманный товарСамостоятельный пример
<?php
declare(strict_types=1);

$cursor = new ArrayIterator([]);
$cursor->rewind();
echo $cursor->valid() ? "Есть товар\n" : "Каталог пока пуст\n";
foreach ($cursor as $title) { echo 'Товар: ', $title, "\n"; }
echo "Можно предложить добавить первый товар\n";
Ожидаемый результат
Каталог пока пуст
Можно предложить добавить первый товар

valid сообщает существование текущей позиции. Для пустого источника foreach не выполнит тело ни разу. Это отличается от существующего элемента 0, false или null; они показаны рядом, чтобы не подменять valid проверкой значения.

Связанные методы: Iterator::current, Iterator::next, Iterator::rewind. Карта интерфейсов.

Countable::countВернуть выбранное коллекцией количество.

PHP 5.1+ · Core

public function count(): int

Вернуть выбранное коллекцией количество.

Часть договораЧто означает
АргументыНет, включая параметр режима.
РезультатЦелое количество по договору класса.
Автоматический вызовcount($object) вызывает этот метод при реализации Countable.
Показать число строк каталога
  1. count($catalog)
  2. Catalog::count() → count внутреннего массива
  3. Вернуть число в подпись интерфейса
Один договор считает ожидающие задания двумя способамиСамостоятельный пример
<?php
declare(strict_types=1);

class Queue implements Countable
{
    private array $jobs = ['Письмо по заказу 42', 'Отчёт о продажах'];
    public function count(): int { return count($this->jobs); }
}
$queue = new Queue();
echo 'Метод очереди: ', $queue->count(), " задания\n";
echo 'Функция PHP: ', count($queue), " задания\n";
Ожидаемый результат
Метод очереди: 2 задания
Функция PHP: 2 задания

Вызов count($queue) направляется в Countable::count реализации Queue. Оба пути дают количество ожидающих элементов. Это осмысленный счётчик интерфейса, но он не оценивает длительность работы и не сохраняет очередь после завершения процесса.

COUNT_RECURSIVE не передаётся внутрь методаСамостоятельный пример
<?php
class Sections implements Countable {
    private array $items = [['A', 'B'], ['C']];
    public function count(): int { return count($this->items); }
}
$sections = new Sections();
echo count($sections), "\n";
echo count($sections, COUNT_RECURSIVE), "\n";
Ожидаемый результат
2
2

Обе операции считают две верхние секции: метод не принимает режим и не стал рекурсивным. Если приложению нужен итог по вложенным товарам, создайте явно названный метод. Стоимость count у пользовательского класса зависит от реализации, в отличие от простого чтения количества элементов массива.

Связанные методы: IteratorAggregate::getIterator, Iterator::valid. Карта интерфейсов.

ArrayAccess::offsetExistsРешить, считается ли ключ установленным для isset и empty.

PHP 5+ · Core

public function offsetExists(mixed $offset): bool

Решить, считается ли ключ установленным для isset и empty.

Часть договораЧто означает
offsetПроверяемый ключ; допустимые типы определяет контейнер.
Результатbool. Для нетипизированных старых реализаций результат приводился к bool.
emptyЕсли false, get не вызывается; если true, empty дополнительно читает offsetGet.
Проверка пустоты состоит из двух решений
  1. offsetExists(key)
  2. false → empty сразу true
  3. true → offsetGet(key) → проверить значение
null, ноль и отсутствующий ключСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$this->requireKey($offset)]);
    }
    public function offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings['null'] = null;
$settings['zero'] = 0;
var_dump(isset($settings['null']), isset($settings['zero']), isset($settings['missing']));
var_dump(empty($settings['zero']));
Ожидаемый результат
bool(false)
bool(true)
bool(false)
bool(true)

Реализация через isset внутреннего массива отличает ноль от отсутствия, но считает null не установленным. empty нуля true. Это не обязательно семантика другого ArrayAccess-класса.

Контейнер может считать ключ с null существующимСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return array_key_exists($this->requireKey($offset), $this->items);
    }
    public function offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings['optional'] = null;
var_dump(isset($settings['optional']));
var_dump(empty($settings['optional']));
Ожидаемый результат
bool(true)
bool(true)

Здесь isset объекта отражает наш offsetExists через array_key_exists, поэтому возвращает true даже для null. empty читает null и возвращает true. Такой выбор нужно явно документировать, чтобы читатель не перенёс привычную семантику массивов без проверки.

Связанные методы: ArrayAccess::offsetGet, ArrayAccess::offsetSet. Карта интерфейсов.

ArrayAccess::offsetGetПолучить значение по ключу или сообщить о недопустимом чтении.

PHP 5+ · Core

public function offsetGet(mixed $offset): mixed

Получить значение по ключу или сообщить о недопустимом чтении.

Часть договораЧто означает
offsetКлюч читаемого элемента.
РезультатЛюбой тип значения. Правило отсутствующего ключа задаёт реализация.
Непрямое изменениеДля изменения массива внутри элемента, ++/-- или получения ссылки метод должен возвращать реальную ссылку.
Чтение настройки
  1. $settings["theme"]
  2. offsetGet("theme") проверяет наличие
  3. Возвращает строку или выбрасывает оговорённое исключение
Отличить установленный null от отсутствияСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$this->requireKey($offset)]);
    }
    public function offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings['optional'] = null;
var_dump($settings['optional']);
try { $settings['missing']; }
catch (OutOfBoundsException $error) { echo "Настройка не найдена\n"; }
Ожидаемый результат
NULL
Настройка не найдена

array_key_exists в getter позволяет хранить null, даже если offsetExists использует семантику isset. Возвращать null при любом пропуске тоже возможно, но тогда приложению труднее отличить два случая.

Увеличить значение через getter по ссылкеСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$this->requireKey($offset)]);
    }
    public function &offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings['attempts'] = 0;
++ $settings['attempts'];
echo $settings['attempts'], "\n";
Ожидаемый результат
1

Префиксный ++ изменяет значение, полученное по ссылке; setter здесь не выполняет проверку новой величины. Для ограниченного числа попыток лучше явный incrementAttempts, который не позволит обойти правило. Возврат ссылки — возможность, не обязательное улучшение.

Связанные методы: ArrayAccess::offsetExists, ArrayAccess::offsetSet, ArrayAccess::offsetUnset. Карта интерфейсов.

ArrayAccess::offsetSetЗаписать целое значение по ключу или обработать добавление без ключа.

PHP 5+ · Core

public function offsetSet(mixed $offset, mixed $value): void

Записать целое значение по ключу или обработать добавление без ключа.

Часть договораЧто означает
offsetКлюч, либо null при записи $object[] = value.
valueНовое значение произвольного типа; проверка входит в ответственность класса.
РезультатЗначения нет. Непрямое изменение подэлемента вызывает getter, не этот метод.
Добавление и замена
  1. $settings["theme"] = "dark" → offsetSet("theme", "dark")
  2. $settings[] = "extra" → offsetSet(null, "extra")
  3. Класс решает: добавить, заменить или отклонить
Добавить значение без указанного ключаСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$this->requireKey($offset)]);
    }
    public function offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings[] = 'Первая';
$settings[] = 'Вторая';
echo $settings[0], ':', $settings[1], "\n";
Ожидаемый результат
Первая:Вторая

Автоматические числовые ключи создаёт наш внутренний массив в ветке offset === null. Сам интерфейс не обязан поддерживать добавление: словарь фиксированных настроек может выбрасывать исключение.

Отклонить неподходящий ключ до измененияСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$this->requireKey($offset)]);
    }
    public function offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings['theme'] = 'light';
try { $settings[new stdClass()] = 'dark'; }
catch (InvalidArgumentException $error) { echo "Неверный ключ\n"; }
echo $settings['theme'], "\n";
Ожидаемый результат
Неверный ключ
light

Наш контейнер принимает только int|string, объект-ключ отклонён. Явный $settings[null] тоже попадёт в ветку добавления и неотличим по параметру от []. Если вам нужно различать такие операции, используйте явные методы API.

Связанные методы: ArrayAccess::offsetGet, ArrayAccess::offsetExists, ArrayAccess::offsetUnset. Карта интерфейсов.

ArrayAccess::offsetUnsetУдалить запись через unset($object[$key]).

PHP 5+ · Core

public function offsetUnset(mixed $offset): void

Удалить запись через unset($object[$key]).

Часть договораЧто означает
offsetКлюч удаляемой записи.
РезультатЗначения нет; правило отсутствия определяет класс.
ГраницыУдаляет элемент, не сам контейнер. unset переменной объекта не вызывает этот метод.
Удаление конкретного ключа
  1. unset($settings["theme"])
  2. offsetUnset("theme")
  3. Внутреннее хранилище больше не содержит запись
Удалить и проверить отсутствиеСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$this->requireKey($offset)]);
    }
    public function offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings['theme'] = 'dark';
unset($settings['theme']);
var_dump(isset($settings['theme']));
unset($settings['missing']);
echo "Повторное удаление допустимо\n";
Ожидаемый результат
bool(false)
Повторное удаление допустимо

В этой реализации удаление отсутствующего корректного ключа безвредно. Другой контейнер может требовать ошибку или регистрировать событие удаления; согласуйте это с остальными методами.

Удаление переменной не очищает общий объектСамостоятельный пример
<?php
declare(strict_types=1);

class Settings implements ArrayAccess {
    private array $items = [];
    private function requireKey(mixed $offset): int|string {
        if (!is_int($offset) && !is_string($offset)) {
            throw new InvalidArgumentException('Ключ должен быть int или string');
        }
        return $offset;
    }
    public function offsetExists(mixed $offset): bool {
        return isset($this->items[$this->requireKey($offset)]);
    }
    public function offsetGet(mixed $offset): mixed {
        $key = $this->requireKey($offset);
        if (!array_key_exists($key, $this->items)) {
            throw new OutOfBoundsException('Настройка не найдена');
        }
        return $this->items[$key];
    }
    public function offsetSet(mixed $offset, mixed $value): void {
        if ($offset === null) {
            $this->items[] = $value;
            return;
        }
        $this->items[$this->requireKey($offset)] = $value;
    }
    public function offsetUnset(mixed $offset): void {
        unset($this->items[$this->requireKey($offset)]);
    }
}

$settings = new Settings();
$settings['theme'] = 'dark';
$also = $settings;
unset($settings);
echo $also['theme'], "\n";
Ожидаемый результат
dark

$also по-прежнему держит тот же экземпляр. offsetUnset вызывается для элемента через скобки, а не при уничтожении имени переменной. Историческое приведение (unset) также не вызывало его; само (unset) удалено в PHP 8 и не является современным способом очистки.

Связанные методы: ArrayAccess::offsetSet, ArrayAccess::offsetExists, ArrayAccess::offsetGet. Карта интерфейсов.

InternalIterator::__constructЗакрытый конструктор запрещает создавать встроенный курсор напрямую.

PHP 8.0+ · Core

private function __construct()

Закрытый конструктор запрещает создавать встроенный курсор напрямую.

Часть договораЧто означает
АргументыНет.
Доступprivate: new InternalIterator() из приложения вызовет Error.
ПолучениеЗапросите итератор у поддерживающего встроенного объекта. Класс final, наследование запрещено.
Источник создаёт подходящий курсор
  1. Создать WeakMap
  2. WeakMap::getIterator()
  3. Получить InternalIterator, связанный с этим источником
Прямое создание не разрешеноСамостоятельный пример
<?php
try { new InternalIterator(); }
catch (Error $error) { echo "Конструктор закрыт\n"; }
Ожидаемый результат
Конструктор закрыт

Error перехвачен только для демонстрации ограничения. В рабочем коде просто не вызывайте закрытый конструктор; он не принимает массив или источник в качестве обходного параметра.

Получить курсор WeakMap и прочитать подписи товаровСамостоятельный пример
<?php
declare(strict_types=1);

$tea = (object) ['title' => 'Чай'];
$coffee = (object) ['title' => 'Кофе'];
$labels = new WeakMap();
$labels[$tea] = 'Готов к публикации';
$labels[$coffee] = 'Нужна фотография';
$cursor = $labels->getIterator();
echo 'Тип курсора: ', get_class($cursor), "\n";
foreach ($cursor as $product => $label) { echo $product->title, ': ', $label, "\n"; }
Ожидаемый результат
Тип курсора: InternalIterator
Чай: Готов к публикации
Кофе: Нужна фотография

InternalIterator создаёт встроенный контейнер, здесь WeakMap. Пользовательский код получает готовый курсор через getIterator, а не new InternalIterator. Карта использует объект как ключ, поэтому в foreach product — сам объект товара.

Связанные методы: InternalIterator::rewind, IteratorAggregate::getIterator. Карта интерфейсов.

InternalIterator::currentПрочитать текущий элемент встроенного источника.

PHP 8.0+ · Core

public function current(): mixed

Прочитать текущий элемент встроенного источника.

Часть договораЧто означает
АргументыНет.
РезультатТекущее значение любого типа.
ГраницаСначала проверяйте valid; поведение вне позиции не является универсальным результатом для всех источников.
Чтение встроенного курсора
  1. rewind()
  2. valid() подтверждает позицию
  3. current() возвращает значение WeakMap
Текущее значение картыСамостоятельный пример
<?php
declare(strict_types=1);

$first = new stdClass();
$second = new stdClass();
$map = new WeakMap();
$map[$first] = 'Первый';
$map[$second] = 'Второй';
$cursor = $map->getIterator();

$cursor->rewind();
if ($cursor->valid()) { echo $cursor->current(), "\n"; }
Ожидаемый результат
Первый

В примере порядок задаётся вставкой двух записей в WeakMap. Метод не отдаёт ключ и не меняет позицию.

null не равен окончанию обходаСамостоятельный пример
<?php
$key = new stdClass();
$map = new WeakMap();
$map[$key] = null;
$cursor = $map->getIterator();
$cursor->rewind();
var_dump($cursor->valid(), $cursor->current());
Ожидаемый результат
bool(true)
NULL

В карте существует запись со значением null. Проверка через current на истинность потеряла бы её, а valid корректно сообщает наличие.

Связанные методы: InternalIterator::key, InternalIterator::next, InternalIterator::valid. Карта интерфейсов.

InternalIterator::keyПолучить ключ текущего элемента встроенного источника.

PHP 8.0+ · Core

public function key(): mixed

Получить ключ текущего элемента встроенного источника.

Часть договораЧто означает
АргументыНет.
РезультатКлюч в модели данного источника, не обязательно int|string.
Для WeakMapВозвращается сам объект-ключ, не его идентификатор и не копия.
Ключ карты объектов
  1. WeakMap хранит запись по $first
  2. key() возвращает тот же объект
  3. === подтверждает идентичность
Сравнить объект-ключ с исходнымСамостоятельный пример
<?php
declare(strict_types=1);

$first = new stdClass();
$second = new stdClass();
$map = new WeakMap();
$map[$first] = 'Первый';
$map[$second] = 'Второй';
$cursor = $map->getIterator();

$cursor->rewind();
var_dump($cursor->key() === $first);
echo $cursor->current(), "\n";
Ожидаемый результат
bool(true)
Первый

Идентичность ключа полезна для поиска служебных сведений об объекте. Это не числовая позиция массива.

Сохранённый ключ сам удерживает объектСамостоятельный пример
<?php
$key = new stdClass();
$map = new WeakMap();
$map[$key] = 'Подпись';
$cursor = $map->getIterator();
$cursor->rewind();
$savedKey = $cursor->key();
unset($cursor, $key);
echo count($map), "\n";
unset($savedKey);
echo count($map), "\n";
Ожидаемый результат
1
0

Полученный объект-ключ стал обычной сильной ссылкой в $savedKey. Диагностический сбор ключей способен продлить жизнь объектов и изменить ожидаемую очистку WeakMap.

Связанные методы: InternalIterator::current, InternalIterator::valid. Карта интерфейсов.

InternalIterator::nextПередвинуть встроенный курсор к следующей записи.

PHP 8.0+ · Core

public function next(): void

Передвинуть встроенный курсор к следующей записи.

Часть договораЧто означает
АргументыНет.
РезультатЗначения нет.
После вызоваПроверьте valid, затем читайте current/key.
Шаг по карте
  1. Текущая запись Первая
  2. next()
  3. Вторая запись или конец
Пропустить первую записьСамостоятельный пример
<?php
declare(strict_types=1);

$first = new stdClass();
$second = new stdClass();
$map = new WeakMap();
$map[$first] = 'Первый';
$map[$second] = 'Второй';
$cursor = $map->getIterator();

$cursor->rewind();
$cursor->next();
if ($cursor->valid()) { echo $cursor->current(), "\n"; }
Ожидаемый результат
Второй

next управляет позицией того же курсора; он не возвращает второй элемент и не создаёт копию коллекции.

Обнаружить конец после двух переходовСамостоятельный пример
<?php
declare(strict_types=1);

$first = new stdClass();
$second = new stdClass();
$map = new WeakMap();
$map[$first] = 'Первый';
$map[$second] = 'Второй';
$cursor = $map->getIterator();

$cursor->rewind();
$cursor->next();
var_dump($cursor->valid());
$cursor->next();
var_dump($cursor->valid());
Ожидаемый результат
bool(true)
bool(false)

Не обращаемся к current после false. В производственном источнике ошибки чтения должны обрабатываться по договору контейнера, а не предположением, что любой next всегда успешен.

Связанные методы: InternalIterator::valid, InternalIterator::current, InternalIterator::rewind. Карта интерфейсов.

InternalIterator::rewindВернуть встроенный курсор в начало.

PHP 8.0+ · Core

public function rewind(): void

Вернуть встроенный курсор в начало.

Часть договораЧто означает
АргументыНет.
РезультатЗначения нет.
НазначениеПодготовить начало обхода данного источника.
Повторное чтение начала
  1. next() сдвинул позицию
  2. rewind() возвращает в начало
  3. valid/current читают первую запись
Вернуться к первой подписиСамостоятельный пример
<?php
declare(strict_types=1);

$first = new stdClass();
$second = new stdClass();
$map = new WeakMap();
$map[$first] = 'Первый';
$map[$second] = 'Второй';
$cursor = $map->getIterator();

$cursor->rewind();
$cursor->next();
echo $cursor->current(), "\n";
$cursor->rewind();
echo $cursor->current(), "\n";
Ожидаемый результат
Второй
Первый

Работает для этой карты в памяти. Не переносите стоимость и повторяемость такого чтения на каждый потоковый источник.

Новый foreach вызывает возврат к началуСамостоятельный пример
<?php
declare(strict_types=1);

$first = new stdClass();
$second = new stdClass();
$map = new WeakMap();
$map[$first] = 'Первый';
$map[$second] = 'Второй';
$cursor = $map->getIterator();

$cursor->rewind();
$cursor->next();
foreach ($cursor as $value) { echo $value, "\n"; }
Ожидаемый результат
Первый
Второй

Сдвиг перед foreach не заставил цикл начать со второй записи. Для продолжения позиции используйте ручной цикл или подходящую оболочку NoRewindIterator.

Связанные методы: InternalIterator::next, InternalIterator::valid, Iterator::rewind. Карта интерфейсов.

InternalIterator::validПроверить существование текущей позиции встроенного курсора.

PHP 8.0+ · Core

public function valid(): bool

Проверить существование текущей позиции встроенного курсора.

Часть договораЧто означает
АргументыНет.
Результатtrue для существующей позиции, false при отсутствии.
ПрименениеПеред чтением key/current и после next/rewind.
Контроль конца
  1. rewind()
  2. valid false → тело не выполняется
  3. valid true → прочитать и перейти дальше
Пустая карта сразу заканчиваетсяСамостоятельный пример
<?php
$map = new WeakMap();
$cursor = $map->getIterator();
$cursor->rewind();
var_dump($cursor->valid());
Ожидаемый результат
bool(false)

Пустой источник не является ошибкой; интерфейс сообщает о нём обычным false.

Ручной проход обрабатывает существующие подписиСамостоятельный пример
<?php
declare(strict_types=1);

$tea = (object) ['title' => 'Чай'];
$coffee = (object) ['title' => 'Кофе'];
$labels = new WeakMap();
$labels[$tea] = 'Готов';
$labels[$coffee] = 'Черновик';
$cursor = $labels->getIterator();
$cursor->rewind();
while ($cursor->valid()) {
    echo $cursor->key()->title, ': ', $cursor->current(), "\n";
    $cursor->next();
}
echo "Все подписи просмотрены\n";
Ожидаемый результат
Чай: Готов
Кофе: Черновик
Все подписи просмотрены

valid проверяется перед каждым чтением. key возвращает объект товара, current — связанную подпись, next продвигает курсор. Важно не забыть next: иначе цикл будет повторять одну запись. Для простого вывода foreach короче.

Связанные методы: InternalIterator::current, InternalIterator::next, Countable::count. Карта интерфейсов.

Serializable::serializeИсторический метод возвращает собственную строку состояния или null.

PHP 5.1+ · Core

public function serialize(): ?string

Исторический метод возвращает собственную строку состояния или null.

Часть договораЧто означает
АргументыНет.
Результатstring или null, не массив. Для __serialize современного протокола результат, напротив, array.
Ошибки/версииВ справке исторического протокола описано Exception для неподходящего результата. При явном ?string и strict_types неверный return даёт TypeError. Без обоих magic-методов класс deprecated с PHP 8.1.
Старое содержимое и новый путь различаются
  1. Прямой $task->serialize() → наша JSON-строка
  2. Глобальная serialize($task) при __serialize → формат O
  3. Не путать метод интерфейса и глобальную функцию
Увидеть содержимое старого методаСамостоятельный пример
<?php
declare(strict_types=1);

class SavedTask implements Serializable {
    public static int $constructed = 0;
    public function __construct(public string $label) {
        ++self::$constructed;
    }
    public function __serialize(): array {
        return ['label' => $this->label];
    }
    public function __unserialize(array $data): void {
        if (!isset($data['label']) || !is_string($data['label'])) {
            throw new UnexpectedValueException('Нет строковой метки');
        }
        $this->label = $data['label'];
    }
    public function serialize(): ?string {
        return json_encode($this->__serialize(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
    }
    public function unserialize(string $data): void {
        $decoded = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($decoded)) {
            throw new UnexpectedValueException('Ожидался объект данных');
        }
        $this->__unserialize($decoded);
    }
}

$task = new SavedTask('Заказ');
echo $task->serialize(), "\n";
echo substr(serialize($task), 0, 2), "\n";
Ожидаемый результат
{"label":"Заказ"}
O:

Прямой метод всё ещё вызывается обычным способом. Глобальная функция предпочла __serialize, поэтому современная запись не содержит историческую C-оболочку. Сам интерфейс не требует JSON внутри: это выбранное нами содержимое.

null допустим как результат старого методаСамостоятельный пример
<?php
class OptionalSnapshot implements Serializable {
    public function serialize(): ?string { return null; }
    public function unserialize(string $data): void {}
    public function __serialize(): array { return []; }
    public function __unserialize(array $data): void {}
}
$snapshot = new OptionalSnapshot();
var_dump($snapshot->serialize());
echo substr(serialize($snapshot), 0, 2), "\n";
Ожидаемый результат
NULL
O:

Этот диагностический класс показывает тип результата и приоритет magic-метода, не механизм хранения полезных данных. В новом коде оформляйте снимок через __serialize; наличие Serializable здесь нужно только для разбора исторического договора.

Связанные методы: Serializable::unserialize, Stringable::__toString. Карта интерфейсов.

Serializable::unserializeВосстановить объект из содержимого исторической C-записи.

PHP 5.1+ · Core

public function unserialize(string $data): void

Восстановить объект из содержимого исторической C-записи.

Часть договораЧто означает
dataСтроковое содержимое, ранее созданное интерфейсным serialize.
РезультатВозвращаемое значение не используется; метод восстанавливает состояние экземпляра.
Жизненный циклОбычный __construct не вызывается автоматически. Можно вызвать его явно, только если это осознанно соответствует восстановлению.
Данные входят в восстановитель
  1. Строка состояния
  2. Декодирование + проверка формы/типов
  3. Присвоение допустимых полей объекта
Прямой вызов восстанавливает существующий объектСамостоятельный пример
<?php
declare(strict_types=1);

class SavedTask implements Serializable {
    public static int $constructed = 0;
    public function __construct(public string $label) {
        ++self::$constructed;
    }
    public function __serialize(): array {
        return ['label' => $this->label];
    }
    public function __unserialize(array $data): void {
        if (!isset($data['label']) || !is_string($data['label'])) {
            throw new UnexpectedValueException('Нет строковой метки');
        }
        $this->label = $data['label'];
    }
    public function serialize(): ?string {
        return json_encode($this->__serialize(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
    }
    public function unserialize(string $data): void {
        $decoded = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($decoded)) {
            throw new UnexpectedValueException('Ожидался объект данных');
        }
        $this->__unserialize($decoded);
    }
}

$task = new SavedTask('Старая метка');
$task->unserialize('{"label":"Новая метка"}');
echo $task->label, "\n";
echo SavedTask::$constructed, "\n";
Ожидаемый результат
Новая метка
1

Прямой метод не создаёт другой экземпляр. При обработке C-формата PHP выделяет объект и вызывает этот метод без обычного конструктора. В обоих случаях проверка данных остаётся вашей обязанностью.

Не присвоить неверное состояние из снимкаСамостоятельный пример
<?php
declare(strict_types=1);

class SavedTask implements Serializable {
    public static int $constructed = 0;
    public function __construct(public string $label) {
        ++self::$constructed;
    }
    public function __serialize(): array {
        return ['label' => $this->label];
    }
    public function __unserialize(array $data): void {
        if (!isset($data['label']) || !is_string($data['label'])) {
            throw new UnexpectedValueException('Нет строковой метки');
        }
        $this->label = $data['label'];
    }
    public function serialize(): ?string {
        return json_encode($this->__serialize(), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
    }
    public function unserialize(string $data): void {
        $decoded = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
        if (!is_array($decoded)) {
            throw new UnexpectedValueException('Ожидался объект данных');
        }
        $this->__unserialize($decoded);
    }
}

$task = new SavedTask('Сохранить');
try { $task->unserialize('{"label":42}'); }
catch (UnexpectedValueException $error) { echo "Неверный формат метки\n"; }
echo $task->label, "\n";
Ожидаемый результат
Неверный формат метки
Сохранить

Проверка идёт до изменения свойства, поэтому старое состояние осталось. JSON_THROW_ON_ERROR отдельно отличает сломанный JSON от корректного JSON неправильной формы. Этот код не оправдывает unserialize внешнего пользовательского PHP-payload.

Связанные методы: Serializable::serialize. Карта интерфейсов.

Stringable::__toStringВернуть удобное строковое представление объекта.

PHP 8.0+ · Core

public function __toString(): string

Вернуть удобное строковое представление объекта.

Часть договораЧто означает
АргументыНет.
РезультатСтрока для echo, конкатенации и явного (string).
ТипКласс с этим методом в PHP 8 автоматически Stringable. Обычная строка Stringable не является; совместный параметр — string|Stringable.
Из доменного значения в подпись
  1. OrderNumber(42)
  2. __toString()
  3. Строка ORD-42 в выводе или логе
Интерфейс определяется даже без implementsСамостоятельный пример
<?php
class ShipmentCode {
    public function __toString(): string { return 'SHIP-7'; }
}
$code = new ShipmentCode();
var_dump($code instanceof Stringable);
echo 'Отправление: ', $code, "\n";
Ожидаемый результат
bool(true)
Отправление: SHIP-7

Явное implements Stringable всё равно полезно как документация намерения. Метод не должен возвращать массив сведений: для него нужен другой протокол, например JsonSerializable.

Строгий метод не возвращает число вместо строкиСамостоятельный пример
<?php
declare(strict_types=1);
class InvalidLabel implements Stringable {
    public function __toString(): string { return 42; }
}
try { echo new InvalidLabel(); }
catch (TypeError $error) { echo "Нужен строковый результат\n"; }
Ожидаемый результат
Нужен строковый результат

Преобразуйте число в строку внутри метода осознанно или соберите подпись конкатенацией. Ошибка перехвачена для обучения; корректный метод должен соблюдать обещанный тип. Исключения из __toString разрешены начиная с PHP 7.4, но обычный вывод объекта лучше делать предсказуемым.

Связанные методы: Serializable::serialize. Карта интерфейсов.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.