Перейти к содержимому
Поиск
Php Разработка

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

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 19 · С нуля → практика

Многомерные массивы: от списка внутри списка до обхода неизвестной глубины

Создаём обычный и вложенный массив в PHP, читаем каждый шаг пути, меняем данные и проходим foreach; затем возвращаем полезный список из рекурсии и лишь после этого переходим к JSON.

Массив внутри массива: собрать два отдельных списка на сборку

Два покупателя заказали разный набор напитков. Первый — чай и кофе, второй — какао. Нужно держать оба заказа вместе, сохранив границу каждого. Обычный массив хранит несколько значений; одним из этих значений может быть другой массив. Так получается вложенная структура. Сначала запишем один простой список, затем поместим списки в общий набор.

Два уровня массива $orders
  1. Внешняя ячейка 0 → внутренний список: 0 → Чай, 1 → Кофе.
  2. Внешняя ячейка 1 → внутренний список: 0 → Какао.
  3. $orders[0] выбирает весь первый список.
  4. $orders[0][1] выбирает Кофе внутри первого списка.

Представьте два подписанных пакета внутри общей корзины: внешний ключ находит пакет, внутренний ключ — выбранный товар. Это модель адресации, а не буквальные коробки в памяти. PHP-массив не обязан быть прямоугольной таблицей: в первом заказе два элемента, во втором один.

Один заказ пока является обычным массивомСамостоятельный пример
<?php
$order = ['Чай', 'Кофе'];
echo 'Первый напиток: ', $order[0], "\n";
echo 'Второй напиток: ', $order[1], "\n";
Ожидаемый результат
Первый напиток: Чай
Второй напиток: Кофе

Справа [] создаёт массив, запятая разделяет элементы, ключи 0 и 1 назначены автоматически. Каждый элемент order — строка. Если эта запись ещё незнакома, сначала пройдите обычный массив.

Поместить два заказа в один внешний массивСамостоятельный пример
<?php
$orders = [
    ['Чай', 'Кофе'],
    ['Какао'],
];
echo 'Заказ 1, напиток 2: ', $orders[0][1], "\n";
echo 'Заказ 2, напиток 1: ', $orders[1][0], "\n";
Ожидаемый результат
Заказ 1, напиток 2: Кофе
Заказ 2, напиток 1: Какао

У внешнего массива две внутренние пары []. В [0][1] первая пара скобок выбирает заказ, вторая — элемент этого заказа. Отступы только показывают структуру человеку; вложенность создают именно массивы, а не количество пробелов.

Все данные сейчас созданы в самом PHP. Для появления вложенности не нужны JSON, база данных или внешний сервис. Сначала освоим ключи и обход такой структуры, затем узнаем, как она может прийти из внешнего текста.

Каждый ключ — отдельный шаг: прочитать, скопировать, изменить

Если в выражении несколько [] подряд, читайте слева направо. Можно разложить путь на промежуточную переменную: взять первый заказ целиком, затем выбрать кофе. Вопрос о получении значения из многомерного массива возникает именно здесь: внешняя структура и нужная строка — разные значения.

Посмотреть на два шага одного путиСамостоятельный пример
<?php
$orders = [['Чай', 'Кофе'], ['Какао']];
$firstOrder = $orders[0];
$secondDrink = $firstOrder[1];
echo 'Через промежуточный список: ', $secondDrink, "\n";
echo 'Прямым путём: ', $orders[0][1], "\n";
Ожидаемый результат
Через промежуточный список: Кофе
Прямым путём: Кофе

firstOrder содержит массив [Чай, Кофе], secondDrink — строку Кофе. Оба способа чтения дали одинаковое конечное значение. Нельзя печатать firstOrder как обычную строку через echo: это всё ещё целый массив.

Промежуточное присваивание обычного массива даёт отдельное значение. Если покупатель заменил кофе на какао, записывайте по полному пути в исходный orders либо явно присвойте изменённую копию назад. Сначала увидим обе ситуации.

Изменение копии и изменение исходного заказаСамостоятельный пример
<?php
$orders = [['Чай', 'Кофе'], ['Какао']];
$firstOrder = $orders[0];
$firstOrder[1] = 'Какао';
echo 'Копия: ', $firstOrder[1], "\n";
echo 'Исходный заказ до записи: ', $orders[0][1], "\n";
$orders[0][1] = 'Какао';
echo 'Исходный заказ после записи: ', $orders[0][1], "\n";
echo 'Другой заказ: ', $orders[1][0], "\n";
Ожидаемый результат
Копия: Какао
Исходный заказ до записи: Кофе
Исходный заказ после записи: Какао
Другой заказ: Какао

Изменение firstOrder не было записью в orders. Присваивание orders[0][1] изменило только выбранный элемент первого заказа. Отдельный второй заказ не участвует в этом пути. Ссылки и объекты внутри массивов уточняют копирование; здесь их нет.

У отсутствующего ключа прямое чтение выдаёт предупреждение. Не добавляйте [0] наугад, пока строка не появится. Сначала определите структуру. Для действительно необязательного поля оператор ?? выбирает резерв при отсутствии или null; для обязательного элемента это способ спрятать ошибку, если договор не предусматривает резерв.

Внешний foreach получает один внутренний массив за проход

Для сборщика нужна ведомость каждого заказа. Известно, что в каждом заказе есть первый напиток, поэтому сначала обойдём только внешние элементы. foreach ($orders as $order) берёт очередное значение внешнего массива; поскольку этим значением является массив, местная $order содержит целый внутренний список.

Взять первый напиток из каждого заказаСамостоятельный пример
<?php
$orders = [['Чай', 'Кофе'], ['Какао']];
foreach ($orders as $order) {
    echo 'Первый напиток заказа: ', $order[0], "\n";
}
echo "Первые позиции проверены\n";
Ожидаемый результат
Первый напиток заказа: Чай
Первый напиток заказа: Какао
Первые позиции проверены

Проход 1: order = [Чай, Кофе], чтение [0] даёт Чай. Проход 2: order = [Какао], [0] даёт Какао. Кофе не потерялся: правило намеренно берёт только первый напиток, а не все значения. Пустые заказы этому учебному договору не соответствуют.

Во внешнем циклеЧто хранит $orderРезультат $order[0]
Первый проход[Чай, Кофе]Чай
Второй проход[Какао]Какао

Вложенный массив не требует автоматически двух циклов. Число циклов зависит от задачи: если нужно известное поле или известный номер, достаточно прямого обращения. Если нужно каждое значение каждого заказа — переходим к двум циклам.

Два foreach: полностью собрать первый заказ, затем перейти ко второму

Теперь ведомость должна включать все напитки. Внешний цикл берёт номер и внутренний список; внутренний перебирает напитки этого списка. as $orderIndex => $order даёт ключ и значение. Номер для человека получаем как индекс + 1: программный ключ 0 соответствует первому заказу.

Полная ведомость сборки двух заказовСамостоятельный пример
<?php
$orders = [['Чай', 'Кофе'], ['Какао']];
foreach ($orders as $orderIndex => $order) {
    $orderNumber = $orderIndex + 1;
    echo 'Заказ ', $orderNumber, "\n";
    foreach ($order as $drink) {
        echo '  Подготовить: ', $drink, "\n";
    }
    echo "Заказ собран\n";
}
Ожидаемый результат
Заказ 1
  Подготовить: Чай
  Подготовить: Кофе
Заказ собран
Заказ 2
  Подготовить: Какао
Заказ собран

Внутренний foreach должен закончить первый список целиком, прежде чем внешний возьмёт второй. Строка «Заказ собран» стоит после внутреннего цикла, но ещё внутри внешнего, поэтому появляется для каждого заказа.

Порядок действий не чередует одинаковые позиции разных заказов
  1. Внешний: orderIndex = 0, order = [Чай, Кофе].
  2. Внутренний: drink = Чай → напечатать строку.
  3. Внутренний: drink = Кофе → напечатать строку; внутренний цикл закончен.
  4. Напечатать «Заказ собран» для первого заказа.
  5. Внешний: orderIndex = 1, order = [Какао]; внутренний выводит Какао.
  6. Закончить второй заказ, затем весь внешний цикл.

Используйте разные понятные имена для уровней: orders → order → drink. Если внутреннее значение тоже назвать order, текущее значение внешнего цикла будет затёрто в теле. Если drink само оказалось массивом, задача имеет дополнительный уровень, который этот конкретный вывод строк ещё не обрабатывает.

Карточки по артикулам: структура отражает задачу магазина

Для реального каталога одного названия мало: у товара есть цена и остаток. Соберём карточки с ключами title, price, stock, затем поместим их во внешний словарь по артикулу. Строка TEA-01 теперь внешний ключ, а title — ключ внутри выбранной карточки.

Сформировать строки каталога по известным полямСамостоятельный пример
<?php
$products = [
    'TEA-01' => ['title' => 'Чай', 'price' => 150, 'stock' => 3],
    'COF-02' => ['title' => 'Кофе', 'price' => 200, 'stock' => 2],
];
foreach ($products as $sku => $product) {
    echo $sku, ' — ', $product['title'], ': ', $product['price'], ' руб.; остаток ', $product['stock'], "\n";
}
Ожидаемый результат
TEA-01 — Чай: 150 руб.; остаток 3
COF-02 — Кофе: 200 руб.; остаток 2

Внешний цикл нужен для всех товаров, а поля каждой карточки известны и читаются по именам. Поэтому снова достаточно одного foreach. products[0] здесь не существует: внешний набор имеет ключи TEA-01 и COF-02.

Выбор структуры меняет удобную операцию: список хранит порядок, словарь позволяет обратиться к товару по известному артикулу. Два одинаковых строковых ключа не создают две карточки — последнее значение заменяет прежнее. Если повторные позиции должны сохраняться отдельно, нужен список записей с полем sku.

Три уровня: склад → товар → поле; число циклов выбирает задача

Один товар лежит на двух складах. Добавим внешний словарь складов, сохранив внутри привычный каталог. Теперь путь до остатка состоит из трёх ключей: south выбирает склад, TEA-01 выбирает товар, stock выбирает число. На каждом промежуточном шаге результат ещё массив.

Сводная ведомость остатков по складамСамостоятельный пример
<?php
$warehouses = [
    'north' => ['TEA-01' => ['title' => 'Чай', 'stock' => 3]],
    'south' => [
        'TEA-01' => ['title' => 'Чай', 'stock' => 8],
        'COF-02' => ['title' => 'Кофе', 'stock' => 2],
    ],
];
echo 'Чай на южном складе: ', $warehouses['south']['TEA-01']['stock'], "\n";
foreach ($warehouses as $warehouseName => $products) {
    foreach ($products as $sku => $product) {
        echo $warehouseName, ' / ', $sku, ': ', $product['stock'], ' уп.', "\n";
    }
}
Ожидаемый результат
Чай на южном складе: 8
north / TEA-01: 3 уп.
south / TEA-01: 8 уп.
south / COF-02: 2 уп.

Первый foreach выбирает склад, второй все товары этого склада. Третий цикл не нужен: stock — известное поле. Глубина массива равна трём уровням, но задача перечисления остатков использует два цикла.

Если инструмент диагностики должен перечислить все поля, потребуется третий foreach по product. Перед ним важно знать, что конечные значения — строки и числа: произвольный массив или объект нельзя без разбора превращать в строку.

Ведомость всех полей известной трёхуровневой структурыСамостоятельный пример
<?php
$warehouses = [
    'north' => ['TEA-01' => ['title' => 'Чай', 'stock' => 3]],
    'south' => ['COF-02' => ['title' => 'Кофе', 'stock' => 2]],
];
foreach ($warehouses as $warehouseName => $products) {
    foreach ($products as $sku => $product) {
        foreach ($product as $field => $value) {
            echo $warehouseName, ' / ', $sku, ' / ', $field, ': ', $value, "\n";
        }
    }
}
Ожидаемый результат
north / TEA-01 / title: Чай
north / TEA-01 / stock: 3
south / COF-02 / title: Кофе
south / COF-02 / stock: 2

Внешние имя склада и артикул сохраняются, пока внутренний цикл выводит title и stock текущей карточки. Только после завершения всех полей выполняется переход к следующему товару. Так программа печатает полный человеческий путь до каждого значения.

Пять ключей: длинный путь можно прочитать без догадок

В пакете каталога рядом со складами могут находиться дата обновления и другие сведения. У склада могут быть адрес и товары. Поэтому осмысленные промежуточные поля warehouses и products добавляют уровни. Посмотрите на полную запись и прочитайте путь по одному ключу.

Получить число и использовать его в решении о заказеСамостоятельный пример
<?php
$catalog = [
    'warehouses' => [
        'north' => [
            'products' => [
                'TEA-01' => ['stock' => 3],
            ],
        ],
    ],
];
$available = $catalog['warehouses']['north']['products']['TEA-01']['stock'];
$requested = 2;
if ($available >= $requested) {
    echo 'Северный склад выдаст ', $requested, ' уп.; после выдачи останется ', $available - $requested, "\n";
} else {
    echo "На северном складе недостаточно товара\n";
}
Ожидаемый результат
Северный склад выдаст 2 уп.; после выдачи останется 1

Пять ключей дали int 3. Это число попало в available, затем в сравнение и вычитание. Пример рассчитывает сообщение; он не списывает товар из реальной базы и не записывает новое stock в catalog.

Шаг путиПолученное значение
['warehouses']Массив всех складов.
['north']Массив сведений северного склада.
['products']Массив товаров этого склада.
['TEA-01']Карточка одного товара.
['stock']Число 3, с которым можно сравнивать количество.

Не добавляйте уровни ради пяти пар скобок. У каждого контейнера должна быть роль. Если форма известна, точный путь самый прозрачный. Если глубина ветвей различается и заранее неизвестна, новый цикл для каждого возможного уровня перестаёт решать задачу; тогда изучают рекурсию.

Неизвестная глубина: одно правило для каждого внутреннего массива

Раздел каталога может содержать названия товаров и подразделы разной глубины. Для первого результата выведем все названия в порядке обхода: Чай, Кофе, Какао. Если очередное значение — массив, надо применить то же правило к нему. Если строка — вывести название. Это рекурсия: функция вызывает себя с меньшей частью исходной структуры.

До примера нужны параметр, вызов и возвращение управления. Объявление function showNames($items) задаёт местное имя items для текущего массива. При вызове showNames($catalog) в него попадает весь каталог, при внутреннем showNames($value) — только одна ветвь. У каждого вызова свой items и своё место внутри foreach.

Готовая функция is_array имеет договор is_array(mixed $value): bool: принимает любое значение и возвращает true для массива, false для прочих типов. Она ничего не печатает, не меняет вход и не углубляется сама. Наш if использует её ответ как выбор следующего действия. В этом первом дереве листья — только строки; произвольные внешние значения требуют дополнительной проверки.

Обход каталога [Чай, [Кофе, [Какао]]]
  1. Корневой foreach берёт Чай: is_array → false, вывести Чай.
  2. Следующее значение — массив: is_array → true, вызвать showNames с этой ветвью.
  3. Внутренний foreach выводит Кофе, затем вызывает showNames для [Какао].
  4. Самый глубокий вызов выводит Какао и заканчивается.
  5. Выполнение возвращается к ожидавшим циклам и завершает оставшиеся проходы.
Показать все названия из ветвей разной глубиныСамостоятельный пример
<?php
function showNames($items)
{
    foreach ($items as $value) {
        if (is_array($value)) {
            showNames($value);
        } else {
            echo 'Товар: ', $value, "\n";
        }
    }
}
$catalog = ['Чай', ['Кофе', ['Какао']]];
showNames($catalog);
echo "Каталог просмотрен\n";
Ожидаемый результат
Товар: Чай
Товар: Кофе
Товар: Какао
Каталог просмотрен

Каждый внутренний вызов завершает только свою ветвь. После него ожидавший foreach продолжает соседей. Пустой массив завершился бы сразу: в нём нет элементов. Здесь явного return нет; конец функции возвращает управление и null, а полезным эффектом является вывод.

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

Получить список из рекурсии и использовать его после вызова

Выводить во время обхода удобно для просмотра, но службе комплектации нужен готовый список: она сама решит, как подписать строки. Перепишем тот же обход с return. Каждый вызов создаёт свой пустой массив names. Строку добавляет в него сразу. Для вложенного массива получает готовый список branchNames и добавляет его названия по одному. После полного foreach возвращает собранный список.

Новая строкаВход → действие → результат
$names = [];В начале каждого вызова создаётся его отдельный пустой список результата.
$branchNames = collectNames($value);Передать внутренний массив; дождаться возвращённого списка и сохранить именно его.
$names[] = $name;Добавить одну строку в текущий результат, сохранив уже добавленные строки.
return $names;Вернуть результат после всех соседей; ранний return внутри первого прохода потерял бы остальные.
Сформировать список этикеток из дерева каталогаСамостоятельный пример
<?php
function collectNames($items)
{
    $names = [];
    foreach ($items as $value) {
        if (is_array($value)) {
            $branchNames = collectNames($value);
            foreach ($branchNames as $name) {
                $names[] = $name;
            }
        } else {
            $names[] = $value;
        }
    }
    return $names;
}
$catalog = ['Чай', ['Кофе', ['Какао']], 'Печенье'];
$labels = collectNames($catalog);
foreach ($labels as $index => $title) {
    echo 'Этикетка ', $index + 1, ': ', $title, "\n";
}
Ожидаемый результат
Этикетка 1: Чай
Этикетка 2: Кофе
Этикетка 3: Какао
Этикетка 4: Печенье

В labels получен обычный плоский массив [Чай, Кофе, Какао, Печенье]. Рекурсивная функция ничего не напечатала. Обычный внешний foreach использовал результат для конкретных подписей; последняя короткая ветвь Печенье показывает, что соседи после глубокой ветви не потеряны.

Как возвращаются именно массивы значений
  1. collectNames([Какао]) возвращает [Какао] в branchNames ожидавшего вызова.
  2. Тот уже накопил Кофе; добавляет Какао и возвращает [Кофе, Какао].
  3. Корневой вызов уже накопил Чай; добавляет оба возвращённых названия.
  4. Его foreach берёт следующего соседа Печенье и добавляет его.
  5. Корневой return отдаёт [Чай, Кофе, Какао, Печенье] в выражение справа от $labels =.
  6. Вызвавший код нумерует полученные значения обычным циклом.

Модель похожа на поручения: каждый помощник отдаёт список своей ветви тому, кто его попросил. В PHP никакие отдельные люди или параллельные процессы не создаются: это последовательные вложенные вызовы в одном выполнении. Незавершённые вызовы занимают стек и хранят своё состояние.

Сохранить путь: остаток 3 должен остаться остатком конкретного товара

Если собрать из складского дерева только числа, значения 3 и 8 потеряют адрес. Для отчёта добавим человеческий путь к каждой конечной величине. Параметр prefix получает уже пройденную часть, по умолчанию пустую строку. При переходе к очередному ключу добавляем его имя через точку — оператор соединения строк. Вложенный вызов получает новую часть пути, а предыдущий вызов сохраняет свой prefix.

Вернуть готовые строки сверки из пятиуровневого дереваСамостоятельный пример
<?php
function collectLines($items, $prefix = '')
{
    $lines = [];
    foreach ($items as $key => $value) {
        $path = $prefix . $key;
        if (is_array($value)) {
            $branchLines = collectLines($value, $path . ' / ');
            foreach ($branchLines as $line) {
                $lines[] = $line;
            }
        } else {
            $lines[] = $path . ': ' . $value;
        }
    }
    return $lines;
}
$catalog = [
    'warehouses' => [
        'north' => ['products' => ['TEA-01' => ['stock' => 3]]],
        'south' => ['products' => ['TEA-01' => ['stock' => 8]]],
    ],
    'status' => 'Обновлено',
];
$reportLines = collectLines($catalog);
echo "Сверка каталога\n";
foreach ($reportLines as $line) {
    echo $line, "\n";
}
Ожидаемый результат
Сверка каталога
warehouses / north / products / TEA-01 / stock: 3
warehouses / south / products / TEA-01 / stock: 8
status: Обновлено

Внутренний вызов после stock возвращает строку с полным путём. Соседний south начинается от прежнего prefix warehouses /, а не от законченного пути north. Внешний reportLines получает три готовые строки; заголовок печатается уже после получения результата.

Здесь формат результата — список строк для человека. Если ключ сам содержит « / », восстановить машинный путь по этой строке однозначно не получится: для машинного редактирования сохраняйте отдельный массив ключей. Договор листьев ограничен строками и числами; false и null требуют выбранного формата, объект или ресурс — отдельной политики. Пустой массив не создаёт строки отчёта: при необходимости показать пустые разделы это правило надо изменить явно.

Граница применимости: конечное дерево, допустимые листья и стоимость

Наши самостоятельные примеры получают заранее заданное конечное дерево массивов со строковыми или числовыми листьями. Условие is_array выбирает контейнер, но само по себе не доказывает, что структура конечна. Ссылкой PHP можно поместить массив внутрь самого себя; такой ввод короткий учебный обход не принимает. Не запускайте его на произвольных структурах без договора.

Требование вырослоЧто нужно изменить
Глубина приходит извнеПередавать счётчик глубины и прекращать обработку до следующего спуска при превышении принятого предела.
Миллионы соседних элементовОграничить общий объём/число обработанных элементов; предел глубины не ограничивает ширину.
Ошибку нельзя выдать частичным успехомВернуть отдельный статус ошибки либо выбросить описанное исключение; не назвать оборванный список полным отчётом.
Нужен поток результатовИзучить генератор или итератор: полный массив результата занимает память по размеру вывода.
Известны конкретные поля товараИспользовать точные пути и foreach по товарам: универсальный обход не проверяет sku, цену или обязательность stock.

Каждый узел исходного дерева посещается при обходе. В варианте collectNames списки дочерних результатов дополнительно переносятся в списки родителей: работа зависит и от глубины, поэтому обещание «всегда линейно и быстро» здесь неверно. collectLines ещё создаёт строки путей. Для небольшого учебного дерева это помогает понимать возврат; большой импорт требует оценки памяти, пределов и выбранного способа накопления.

Если задача требует число конечных значений, можно возвращать целый накопитель вместо списка: для листа прибавлять 1, для массива прибавлять число, которое вернула его ветвь, а return поставить после foreach. Это другая метрика, чем count($data, COUNT_RECURSIVE): встроенная функция считает также элементы-контейнеры. Сумма чисел, число листьев, число товаров и число всех элементов — разные результаты; выбирайте их по требованию, а не по удобству функции.

В продолжении о рекурсии сохраняются отдельные темы пределов, циклических категорий, дерева по parent_id и явного стека. Рекурсивные итераторы и array_walk_recursive — другие инструменты: сравнивайте, дают ли они путь, контейнеры, листья и нужный способ изменения. Они не отменяют проверки смысла исходных данных.

Ограниченная глубина: вернуть весь список или объяснить отказ

Импортёр договорился принимать корневой массив и один уровень вложенных подразделов. Корень имеет depth = 0; первый внутренний массив — depth = 1. Более глубокую структуру нужно отклонить целиком. Обрезанный список нельзя выдавать за полный набор этикеток: товары исчезли бы без объяснения.

Сохраним уже знакомый collectNames, добавив параметры maxDepth и depth. Перед началом работы вызов сравнивает их. При превышении throw new RuntimeException(...) прекращает обычный путь; catch (RuntimeException $error) получает ошибку, а $error->getMessage() возвращает её сообщение. Эти конструкции подробно разобраны в уроке исключений. Здесь они показаны после работающей рекурсии и полного пути return.

Разрешённая структура даёт этикетки; слишком глубокая — причину отказаСамостоятельный пример
<?php
function collectNamesLimited(array $items, int $maxDepth, int $depth = 0): array
{
    if ($depth > $maxDepth) {
        throw new RuntimeException('Подразделы глубже разрешённого уровня');
    }
    $names = [];
    foreach ($items as $value) {
        if (is_array($value)) {
            $branchNames = collectNamesLimited($value, $maxDepth, $depth + 1);
            foreach ($branchNames as $name) {
                $names[] = $name;
            }
        } else {
            $names[] = $value;
        }
    }
    return $names;
}
$labels = collectNamesLimited(['Чай', ['Кофе']], 1);
foreach ($labels as $title) {
    echo 'Подготовить этикетку: ', $title, "\n";
}
try {
    $deepLabels = collectNamesLimited(['Чай', ['Кофе', ['Какао']]], 1);
    foreach ($deepLabels as $title) {
        echo 'Подготовить дополнительную этикетку: ', $title, "\n";
    }
} catch (RuntimeException $error) {
    echo 'Второй каталог не принят: ', $error->getMessage(), "\n";
}
Ожидаемый результат
Подготовить этикетку: Чай
Подготовить этикетку: Кофе
Второй каталог не принят: Подразделы глубже разрешённого уровня

Первый вызов достигает лишь depth 1 и отдаёт полный массив из двух названий. Во втором ветвь [Какао] вызывает depth 2: условие прекращает её и передаёт ошибку наружу. Присваивание deepLabels не завершено; цикл после него пропущен, поэтому частичных дополнительных этикеток нет.

Входы этого примера — массивы со строковыми листьями, а maxDepth — заранее согласованное неотрицательное число. Тип array не проверяет вид листьев. Предел глубины гарантирует ограничение спусков по ветви, но не ограничивает количество соседей и память результата. Для реального внешнего пакета нужны также лимит размера, проверка формы и общий бюджет обхода. Нельзя выбрать единственную «безопасную глубину» для всех серверов и задач.

Упражнение: подставьте maxDepth = 0 для первого каталога. Чай будет добавлен в местный накопитель корня, но вход в [Кофе] вызовет отказ, и вызывающий код не получит частичный массив [Чай]. Для показа сообщения заключите и этот вызов в try/catch. Так проверяется отличие внутреннего промежуточного состояния от завершённого возвращаемого результата.

Практика: общий остаток одного артикула и следующий источник данных

На северном складе 3 упаковки чая, на южном 8. Нужен общий остаток TEA-01, а не сумма каждого встреченного числа в каталоге. Во всех складах этот товар есть, поля проверены. Достаточно перебрать склады и прочитать известный путь: результат должен равняться 11.

Собрать остаток именно чаяСамостоятельный пример
<?php
$warehouses = [
    'north' => ['TEA-01' => ['stock' => 3], 'COF-02' => ['stock' => 2]],
    'south' => ['TEA-01' => ['stock' => 8]],
];
$teaStock = 0;
foreach ($warehouses as $products) {
    $teaStock = $teaStock + $products['TEA-01']['stock'];
}
echo 'Чай на всех складах: ', $teaStock, ' уп.', "\n";
Ожидаемый результат
Чай на всех складах: 11 уп.

Количество кофе 2 не участвует: требование выбрало артикул TEA-01. Если чай может отсутствовать, сначала уточните значение отсутствия: ноль или ошибка неполных данных. Только после этого выбирайте резерв ?? 0 либо отказ.

Мы уже умеем построить, прочитать, изменить и обойти многомерный массив в PHP. Теперь можно заменить источник: внешний сервис передаёт текст JSON, а json_decode получает из него значения PHP. После успешного разбора действуют те же ключи и циклы. Следующий материал про JSON добавляет формат обмена и обработку неудачи к уже понятной структуре.

JSON приходит текстом; массив появляется после разбора

Поставщик прислал текст карточки: название и остаток. Пока это строка, к полю stock нельзя обратиться как к элементу массива. JSON — формат обмена: объект записывается в фигурных скобках с именованными полями, список — в квадратных. Это не PHP-код, знак => внутри JSON не используется.

Одна карточка поставщика
  1. Текст {"title":"Чай","stock":3}
  2. json_decode разбирает текст
  3. PHP-массив с ключами title и stock
  4. Читаем stock → 3

json_decode($json, true) выбирает представление JSON-объектов ассоциативными PHP-массивами. В примере также переданы предел вложенности 16 и флаг JSON_THROW_ON_ERROR: при неправильном JSON разбор бросит исключение. Эта константа — именованный режим функции, а не переменная. Первый опыт использует заранее известную корректную строку; приём неизвестного ответа потребует следующих проверок ниже.

Прочитать остаток из известного текстаСамостоятельный пример
<?php
$json = '{"title":"Чай","stock":3}';
$product = json_decode($json, true, 16, JSON_THROW_ON_ERROR);
echo $product['title'], "\n";
echo $product['stock'], "\n";
Ожидаемый результат
Чай
3

Переменная json остаётся строкой, product получает результат разбора. Ключи в скобках совпадают с полями карточки. Успешный decode доказывает синтаксис JSON, но не наличие нужных полей и не допустимость их значений.

Попробуйте сами. Поменяйте stock на 0. Должно ли это считаться отсутствием поля?

Разбор решения

Нет. Карточка содержит известный нулевой остаток. Не фильтруйте такой случай через empty как будто поставщик ничего не сообщил.

Список карточек: каждый уровень читается отдельно

Разобрать известный каталог и собрать сумму остатковСамостоятельный пример
<?php
function knownCatalogUnits($json)
{
    $catalog = json_decode($json, true, 16, JSON_THROW_ON_ERROR);
    $total = 0;
    foreach ($catalog['items'] as $product) {
        $total += $product['stock'];
    }
    return $total;
}
$json = '{"items":[{"title":"Чай","stock":3},{"title":"Кофе","stock":2}]}';
echo knownCatalogUnits($json), "\n";
Ожидаемый результат
5

Сначала items выбирает список, foreach выдаёт одну карточку, stock выбирает число внутри неё. Функция иллюстрирует маршрут по заранее известной структуре; она намеренно ещё не является безопасным парсером чужого API.

Путь к первому остатку — $catalog['items'][0]['stock']: поле → первый элемент → поле. Но внешняя система может прислать items = null, строку вместо числа или сообщение об ошибке вместо каталога. Следующая практическая секция превращает этот короткий опыт в надёжный приём: ограничивает размер, отличает JSON-объект от списка, проверяет каждый уровень и только затем записывает данные.

При associative=true некоторые различия исходных JSON-объектов и списков теряются. Когда договор требует именно JSON-список, одной последующей проверки array_is_list бывает недостаточно. Поэтому пример устройства ниже сначала сохраняет различие объектов и массивов, а затем строит проверенные данные. Ошибка формата ведёт в обработчик исключения.

Мини-ПК прислал показания: зачем JSON превращается в массив и обратно

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

Вложенный массив: список карточек товаров

Внешний массив $products хранит два внутренних массива. Каждый из них — карточка одного товара. Эту структуру мы сами создаём в PHP.

  • $products — список товаров

    Ключ 0 ведёт к первой карточке, ключ 1 — ко второй.

    • 0 → первая карточка

      title → Чай; stock → 3.

    • 1 → вторая карточка

      title → Кофе; stock → 2.

Сначала выбираем карточку, затем поле в ней$products[0]['title'] → Чай

Каждая пара квадратных скобок выбирает значение по ключу на очередном уровне. Это условная схема связей, а не физическая раскладка памяти. В дальнейших примерах появятся три и пять уровней, затем похожие структуры из JSON.

Мини-ПК на объекте измерил температуру и влажность. Ему нужно передать показания вашему PHP-сервису, хотя программы работают на разных машинах и могут быть написаны на разных языках. По HTTP передаются байты. JSON даёт обеим сторонам понятный текстовый формат: имена полей, числа, строки, списки и вложенные объекты. Внутри PHP удобнее обращаться к массиву; при передаче следующему сервису снова нужен согласованный формат сообщения.

Один пакет показаний
  1. Мини-ПК: JSON-текст
  2. PHP: разбор JSON
  3. Проверка формы → массивы
  4. foreach: поля и диапазоны
  5. Транзакция: сохранить в базе
  6. json_encode: ответ устройству
От показаний устройства до SQLite и JSON-ответаСамостоятельный пример
<?php
declare(strict_types=1);

$body = '{"device":"mini-01","readings":[{"sensor":"temperature","value":31.5},{"sensor":"humidity","value":0}]}';
if (strlen($body) > 4096) {
    throw new LengthException('Слишком большой пакет');
}
$wire = json_decode($body, false, 16, JSON_THROW_ON_ERROR);
if (!$wire instanceof stdClass || !is_array($wire->readings ?? null)) {
    throw new UnexpectedValueException('Нужен JSON-объект со списком readings');
}
$packet = (array) $wire;
if (!is_array($packet) || ($packet['device'] ?? null) !== 'mini-01'
    || !is_array($packet['readings'] ?? null) || !array_is_list($packet['readings'])
    || count($packet['readings']) < 1 || count($packet['readings']) > 20) {
    throw new UnexpectedValueException('Неверный пакет устройства');
}
$ranges = ['temperature' => [-50, 125], 'humidity' => [0, 100]];
$validated = [];
$seen = [];
foreach ($packet['readings'] as $reading) {
    if (!$reading instanceof stdClass) {
        throw new UnexpectedValueException('Показание должно быть JSON-объектом');
    }
    $reading = (array) $reading;
    if (!is_array($reading) || !is_string($reading['sensor'] ?? null)) {
        throw new UnexpectedValueException('Не указан датчик');
    }
    $sensor = $reading['sensor'];
    $value = $reading['value'] ?? null;
    if (!isset($ranges[$sensor]) || isset($seen[$sensor])
        || (!is_int($value) && !is_float($value)) || !is_finite((float) $value)
        || $value < $ranges[$sensor][0] || $value > $ranges[$sensor][1]) {
        throw new UnexpectedValueException('Недопустимое показание');
    }
    $seen[$sensor] = true;
    $validated[] = ['sensor' => $sensor, 'value' => $value];
}
$database = new PDO('sqlite::memory:', options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$database->exec('CREATE TABLE readings (device TEXT, sensor TEXT, value REAL)');
$insert = $database->prepare('INSERT INTO readings (device, sensor, value) VALUES (?, ?, ?)');
$database->beginTransaction();
try {
    foreach ($validated as $reading) {
        $insert->execute([$packet['device'], $reading['sensor'], $reading['value']]);
    }
    $database->commit();
} catch (Throwable $error) {
    if ($database->inTransaction()) { $database->rollBack(); }
    throw $error;
}
$reply = ['device' => $packet['device'], 'accepted' => count($validated)];
echo 'В базе: ', $database->query('SELECT COUNT(*) FROM readings')->fetchColumn(), "\n";
echo json_encode($reply, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), "\n";
Ожидаемый результат
В базе: 2
{"device":"mini-01","accepted":2}

Это самостоятельный CLI-пример с настоящей SQLite в памяти: внешних запросов и нужных файлов он не изменяет. Все показания проверяются до записи; при одном плохом элементе весь пакет отклоняется. Влажность 0 допустима и не теряется через empty(). Массив ответа собирается явно: внутренние данные или неизвестные поля входа не пересылаются дальше автоматически.

Здесь сначала выбран associative=false: JSON-объект остаётся stdClass, а JSON-список — массивом. Это позволяет отвергнуть объект с ключами "0", "1" вместо списка readings. При associative=true оба варианта могли бы стать одинаковым PHP-массивом, и array_is_list уже не доказывал бы исходный формат. После проверки мы явно превращаем нужный объект в массив через (array); такое приведение действует на один уровень, поэтому отдельное показание преобразуется внутри foreach. Это не универсальное преобразование произвольных классов.

Разбор JSON проверяет синтаксис, а следующий код — смысл. Строка "31.5" не принимается вместо числа по договору этого устройства, неизвестный датчик и повтор одного датчика в пакете отклоняются. Для другой модели допустима частичная приёмка, но тогда ответ должен перечислять принятые и отклонённые элементы: нельзя молча терять показания. Вложенные массивы здесь появились из реального списка readings, а не ради упражнения с квадратными скобками.

При превращении примера в HTTP endpoint потребуются отдельные шаги: проверка метода и Content-Type, ограниченное чтение тела, подтверждение устройства секретом или подписью, проверка принадлежности объекта, время измерения и ID пакета для защиты от повторного сохранения. Поле device в JSON само по себе ничего не доказывает. В примере нет аутентификации и дедупликации, поэтому готовым публичным endpoint он не является. Полный HTTP-контур показан в приёме вебхуков и самостоятельном проекте интеграции.

При отправке через Guzzle параметр json => $payload сам кодирует массив и готовит JSON-запрос. Передавать туда уже закодированную строку обычно ошибка: получится JSON-строка внутри JSON вместо ожидаемого объекта. Если тело сформировано вручную через json_encode, передают его как body с соответствующим Content-Type. Выбирайте один слой кодирования и проверяйте требования конкретного API: часть методов ожидает form-параметры или multipart, JSON не универсален.

Многомерный массив — отражение предметной области

Сервис доставки возвращает отправление, внутри получателя и список мест, внутри каждого места — товары. Маркетплейс возвращает список заказов, внутри каждого заказа позиции и адрес. JSON — текстовое представление этой структуры, а не готовая таблица. После json_decode(..., true) JSON-объекты становятся ассоциативными массивами, JSON-массивы — PHP-массивами списков. На каждом уровне вы обращаетесь к тому значению, которое находится именно там: $payload["orders"] — список, $order["items"] — другой список, $item["quantity"] — число.

Граница интеграции
  1. HTTP-ответ: статус, заголовки, ограниченный размер
  2. json_decode с исключением и глубиной
  3. Проверка структуры и каждого типа
  4. Нормализация во внутреннюю модель
  5. Расчет, сохранение и отчет об ошибке

Успешно декодировать JSON недостаточно. Текст null тоже корректен, но не подходит вместо объекта заказа. Поставщик может прислать HTML ошибки с HTTP 200 или изменить поле с числа на строку. JSON_THROW_ON_ERROR отличает ошибку синтаксиса от допустимого null; валидация схемы отличает допустимый JSON от нужных вам данных. Размер ответа ограничивают при чтении, глубину — при разборе. Не доверяйте случайной форме одного успешного ответа.

Проверяем каждый уровень, затем считаем

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

$json = '{"orders":[{"id":"O-17","items":[{"sku":"A","quantity":2,"priceKopecks":15000},{"sku":"B","quantity":1,"priceKopecks":9900}]}]}';
$payload = json_decode($json, true, 32, JSON_THROW_ON_ERROR);
if (!is_array($payload) || !isset($payload['orders'])
    || !is_array($payload['orders']) || !array_is_list($payload['orders'])) {
    throw new UnexpectedValueException('orders должен быть списком');
}
foreach ($payload['orders'] as $orderIndex => $order) {
    if (!is_array($order) || !is_string($order['id'] ?? null)
        || !is_array($order['items'] ?? null) || !array_is_list($order['items'])) {
        throw new UnexpectedValueException("Заказ {$orderIndex}: неверная форма");
    }
    $total = 0;
    foreach ($order['items'] as $itemIndex => $item) {
        if (!is_array($item) || !is_string($item['sku'] ?? null)
            || !is_int($item['quantity'] ?? null) || $item['quantity'] < 1 || $item['quantity'] > 1000
            || !is_int($item['priceKopecks'] ?? null) || $item['priceKopecks'] < 0 || $item['priceKopecks'] > 100000000) {
            throw new UnexpectedValueException("Заказ {$orderIndex}, позиция {$itemIndex}: неверные данные");
        }
        $total += $item['quantity'] * $item['priceKopecks'];
    }
    echo $order['id'], ': ', $total, "\n";
}
Ожидаемый результат
O-17: 39900

Здесь зафиксирована схема учебного поставщика: ID и SKU строки, деньги int, количество 1..1000. Для рабочего импорта дополнительно ограничьте количество заказов/строк и суммарное значение под разрядность PHP, проверьте валюту, права и идемпотентность. Путь ошибки позволяет найти проблемную позицию без печати полного персонального ответа.

Плохой JSON, большие ID и строки внутри JSON

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

foreach (['{"orders":', 'null', '{"orders":[]}'] as $body) {
    try {
        $data = json_decode($body, true, 16, JSON_THROW_ON_ERROR);
        if (!is_array($data) || !is_array($data['orders'] ?? null)) {
            throw new UnexpectedValueException('Нужен объект со спискомorders');
        }
        echo "Контракт принят\n";
    } catch (JsonException $exception) {
        echo "НекорректныйJSON\n";
    } catch (UnexpectedValueException $exception) {
        echo "Не та структура\n";
    }
}
Ожидаемый результат
НекорректныйJSON
Не та структура
Контракт принят

Не превращайте любую ошибку в пустой массив: тогда сбой поставщика будет выглядеть как «все заказы исчезли». Сохраняйте ограниченный диагностический контекст и идентификатор запроса, не токен и не полный адрес покупателя.

Для идентификаторов больше PHP_INT_MAX используйте согласованный строковый контракт или JSON_BIGINT_AS_STRING при декодировании. Если поле metadata по договору содержит еще одну JSON-строку, декодируйте только это поле второй раз и снова проверяйте структуру. Не удаляйте обратные слеши заменой строк: так можно испортить законные кавычки, пути и содержимое текста. Произвольная рекурсивная попытка декодировать любую строку меняет типы там, где это не предусмотрено API.

Вернуть данные без изменения смысла

json_encode кодирует список с последовательными числовыми ключами как JSON-массив; пропуски после фильтрации могут дать объект. Восстанавливайте список через array_values только там, где действительно нужна последовательность. Не включайте JSON_NUMERIC_CHECK для произвольного ответа: телефон или артикул может потерять значимые символы. Все строки должны быть UTF-8. Не кодируйте объект домена целиком, если в нем есть секретные публичные свойства; соберите явное представление ответа.

Попробуйте сами. API прислал orders как объект с ID-ключами вместо списка. Исправлять array_values молча?

Разбор решения

Если это новая документированная форма, напишите явный адаптер и тест обеих поддерживаемых версий. Если контракт обещает список, сообщите ошибку: молчаливое преобразование скрывает изменение API и может потерять важные ключи.

Плохой JSON в реальной интеграции: диагностировать и исправлять адресно

Интеграция вчера работала, а сегодня json_decode выбросил JsonException. Сначала проверьте HTTP-статус, Content-Type, размер, кодировку и характер первых байтов. Ответ может быть HTML-страницей авторизации, сообщением PHP Warning перед JSON, оборванным документом или UTF-8 BOM. Это разные причины, и одной «очисткой строки» их не исправить.

Что пришлоКак действовать
HTML страницы входа или проксиПроверить адрес, авторизацию, редиректы и HTTP-статус; JSON в таком ответе не искать.
PHP Warning / отладочный echo перед даннымиИсправить производителя ответа: ошибки в журнал, JSON отдельно. Не отрезать неизвестный текст.
Ровно UTF-8 BOM в начале от известного старого устройстваВременный адаптер может удалить эти три байта; договориться об исправлении отправителя.
Оборванное тело или ошибка UTF-8Зафиксировать сбой и исправить источник/доставку. Не угадывать недостающие кавычки и значения.
Специальный защитный префикс по документированному договору APIОбработать только точный ожидаемый префикс в отдельном адаптере этого API.
Убрать только известный BOM, остальные повреждения отклонитьСамостоятельный пример
<?php
declare(strict_types=1);

function decodeKnownDevice(string $body): array
{
    if (strlen($body) > 4096) {
        throw new LengthException('Превышен предел тела');
    }
    if (str_starts_with($body, "\xEF\xBB\xBF")) {
        $body = substr($body, 3);
    }
    $packet = json_decode($body, true, 16, JSON_THROW_ON_ERROR);
    if (!is_array($packet) || !is_int($packet['value'] ?? null)) {
        throw new UnexpectedValueException('Нужен объект с целым value');
    }
    return $packet;
}
$samples = [
    'bom' => "\xEF\xBB\xBF" . '{"value":0}',
    'warning' => 'Warning: offline {"value":5}',
    'html' => '<html>Login required</html>',
    'truncated' => '{"value":',
    'utf8' => "{\"value\":\"\xFF\"}",
    'wrong-type' => '{"value":"5"}',
];
foreach ($samples as $name => $body) {
    try { echo $name, ': ', decodeKnownDevice($body)['value'], "\n"; }
    catch (JsonException) { echo "ошибка JSON\n"; }
    catch (UnexpectedValueException) { echo "неверная схема\n"; }
}
Ожидаемый результат
bom: 0
warning: ошибка JSON
html: ошибка JSON
truncated: ошибка JSON
utf8: ошибка JSON
wrong-type: неверная схема

BOM удаляется только в самом начале и только как точная последовательность EF BB BF. Этот адаптер — осознанное исключение для известного производителя, не универсальная починка любого API. Для новых отправителей JSON передают в UTF-8 без BOM. Известный префикс не заменяет проверку синтаксиса и структуры.

Не используйте substr($body, strpos($body, '{')) как универсальное лечение: фигурная скобка может находиться в ошибке, корректный JSON может быть списком или скаляром, а повреждённый ответ останется повреждённым. Не удаляйте все обратные слеши: они могут экранировать кавычки внутри нормальной строки. Двойное декодирование нужно только для поля, которое по договору действительно содержит ещё одну JSON-строку.

Для диагностики полезны ID запроса, статус, размер, тип ответа и код ошибки разбора. Полное тело часто содержит токены и персональные данные. Если нужен фрагмент для отладки, используйте контролируемый журнал и предварительное удаление секретов. json_validate доступен с PHP 8.3, но перед json_decode обычно не нужен: это второй разбор тех же данных. Когда нужно работать с результатом, один decode с исключением даёт и проверку синтаксиса, и данные.

Упражнение: отправитель прислал readings как объект с ключами датчиков, хотя договор требует список. Где произошла ошибка? Правильный разбор: JSON синтаксически корректен, но схема изменилась. Уточните новую версию договора; не применяйте array_values без понимания, что означают ключи. Иначе можно отбросить именно идентификатор датчика.

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

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

json_decodeРазобрать JSON-текст в PHP-значение, затем отдельно проверить его структуру.

PHP 5.2+ · json

json_decode(string $json, ?bool $associative = null, int $depth = 512, int $flags = 0): mixed

Разобрать JSON-текст в PHP-значение, затем отдельно проверить его структуру.

Часть договораОбъяснение
jsonТекст UTF-8, а не уже готовый массив.
associativetrue → JSON-объекты в array; false → stdClass; null → выбор через OBJECT_AS_ARRAY.
depth = 512Предел вложенности; вне 1–2147483647 в PHP 8 вызывает ValueError.
flags = 0Комбинация режимов разбора через |.
ВозвратPHP-значение любого подходящего типа. Без THROW ошибка даёт null и JSON_ERROR_*; с THROW — JsonException.
Из одной строки в значение
  1. PHP-строка содержит {"stock":0}.
  2. json_decode с true создаёт массив: ключ stock → число 0.
  3. Чтение $data["stock"] достаёт число; echo выводит 0.
Прочитать одно полеСамостоятельный пример
<?php
$text = '{"stock":0}';
$data = json_decode($text, true);
echo $data["stock"];
Ожидаемый результат
0

Первый аргумент — JSON-текст, второй true просит PHP-массив для JSON-объектов. Квадратные скобки читают значение по имени stock. Это заранее правильный учебный документ; при приёме внешнего текста добавляйте обработку ошибок, как ниже.

Выбрать массив или обычный объектСамостоятельный пример
<?php
$text = '{"stock":0}';
$array = json_decode($text, true, 16, JSON_THROW_ON_ERROR);
$object = json_decode($text, false, 16, JSON_THROW_ON_ERROR);
echo $array['stock'], "\n";
echo $object->stock, "\n";
var_dump(is_array(json_decode($text, null, 16, JSON_OBJECT_AS_ARRAY)));
var_dump(is_object(json_decode($text, false, 16, JSON_OBJECT_AS_ARRAY)));
Ожидаемый результат
0
0
bool(true)
bool(true)

Для массива используются [ключ], для объекта ->свойство. associative=null оставляет решение флагу; явное false побеждает OBJECT_AS_ARRAY. Полученный stdClass хранит данные, но не является классом заказа и не запускает ваш конструктор.

Прочитать имя поля с дефисом или пустое имяСамостоятельный пример
<?php
$object = json_decode('{"stock-count":0,"":7}', false, 16, JSON_THROW_ON_ERROR);
echo $object->{'stock-count'}, "\n";
$field = 'stock-count';
echo $object->{$field}, "\n";
echo $object->{''};
Ожидаемый результат
0
0
7

В фигурных скобках после -> можно передать строковое имя свойства: дефис становится частью имени, а не знаком вычитания. Переменная $field позволяет выбрать имя по данным программы. Пустое имя JSON тоже допустимо: с PHP 7.1 оно сохраняется как пустое имя свойства, прежде использовалось _empty_. Если поля приходят извне, до чтения проверяйте наличие и тип; фигурные скобки сами такой проверки не выполняют.

Показать потерю различия пустого объекта и спискаСамостоятельный пример
<?php
$asArray = json_decode('{}', true, 16, JSON_THROW_ON_ERROR);
$asObject = json_decode('{}', false, 16, JSON_THROW_ON_ERROR);
echo json_encode($asArray, JSON_THROW_ON_ERROR), "\n";
echo json_encode($asObject, JSON_THROW_ON_ERROR), "\n";
echo json_decode('{"stock":1,"stock":2}', true, 16, JSON_THROW_ON_ERROR)['stock'];
Ожидаемый результат
[]
{}
2

Массивный режим удобен для обработки, но не является точным сохранением формы JSON. {} и [] оба могут стать пустым PHP-массивом. Дублирующееся имя stock оставило последнее значение; это не проверка уникальности ключей.

Ошибка параметра и слишком глубокий документ различаютсяСамостоятельный пример
<?php
try {
    json_decode('{}', true, 0, JSON_THROW_ON_ERROR);
} catch (ValueError $error) {
    echo "Неверный параметр depth\n";
}
try {
    json_decode('[[[1]]]', true, 2, JSON_THROW_ON_ERROR);
} catch (JsonException $error) {
    echo $error->getCode() === JSON_ERROR_DEPTH ? "Превышена вложенность\n" : "Другая ошибка\n";
}
Ожидаемый результат
Неверный параметр depth
Превышена вложенность

depth=0 недопустим сам по себе. Во втором вызове параметр допустим, но документ глубже установленного предела. Лимит подбирают по реальному контракту, а не ставят максимальный ради подавления ошибки.

Все строки должны быть UTF-8. Разрешённые флаги: OBJECT_AS_ARRAY, BIGINT_AS_STRING, INVALID_UTF8_IGNORE, INVALID_UTF8_SUBSTITUTE, THROW_ON_ERROR. Максимальный допустимый depth — 2147483647, минимум 1. Большой предел не отменяет ограничения памяти. Числовые строковые ключи при массивном режиме могут стать int-ключами по правилам PHP-массивов.

Рядом: json_encode, json_last_error, json_validate. Полный список флагов и ошибок.

Продолжение: прикладные примеры. Ниже тот же инструмент участвует в обработке ответа сервиса.

С associative=true объекты JSON становятся массивами. JSON_THROW_ON_ERROR бросает JsonException при ошибке текста; depth ограничивает вложенность. Корректный JSON может дать scalar или null.

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

$body = '{"product":{"sku":"A","stock":0}}';
$data = json_decode($body, true, 16, JSON_THROW_ON_ERROR);
if (!is_array($data) || !is_array($data['product'] ?? null) || !is_int($data['product']['stock'] ?? null)) {
    throw new UnexpectedValueException('Неверная схема');
}
echo $data['product']['stock'], "\n";
Ожидаемый результат
0

Проверка null через?? не теряет нулевой остаток. Для рабочего контракта добавьте диапазон, обязательный SKU и лимит размера ответа.

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

$data = json_decode('{"id":9223372036854775808}', true, 16, JSON_THROW_ON_ERROR | JSON_BIGINT_AS_STRING);
echo get_debug_type($data['id']), ':', $data['id'], "\n";
Ожидаемый результат
string:9223372036854775808

Значение больше максимального знакового 64-битного int. Внешний идентификатор не нужно арифметически обрабатывать; храните и передавайте его строкой.

Сравните. json_validate проверяет синтаксис без использования данных; перед decode обычно не нужен второй разбор.

json_encodeСобрать корректный JSON-текст из PHP-данных без ручной склейки кавычек.

PHP 5.2+ · json

json_encode(mixed $value, int $flags = 0, int $depth = 512): string|false

Собрать корректный JSON-текст из PHP-данных без ручной склейки кавычек.

Часть договораОбъяснение
valueСкаляр, null, массив или поддерживаемое представление объекта; строки UTF-8.
flagsРежимы вывода, представления и ошибок. Все перечислены в справочнике флагов.
depth = 512Допустимая глубина обхода; положительный предел.
ВозвратСтрока при успехе. Без THROW при ошибке false и код; с THROW JsonException, кроме частичного режима.
Сформировать текст ответа
  1. PHP-массив ["stock" => 0] описывает одно поле.
  2. json_encode строит строку {"stock":0}.
  3. Строка возвращается в $body; echo или HTTP-ответ — отдельное действие.
Получить JSON, затем вывести егоСамостоятельный пример
<?php
$data = ["stock" => 0];
$body = json_encode($data);
echo $body;
Ожидаемый результат
{"stock":0}

=> связывает ключ с его значением внутри PHP-массива. В JSON поле отделяется двоеточием — это уже другой синтаксис. Функция вернула текст и сама не печатала его.

Почему повторное encode добавляет кавычки и обратные слешиСамостоятельный пример
<?php
$data = ["stock" => 0];
$once = json_encode($data, JSON_THROW_ON_ERROR);
echo $once, "\n";
echo json_encode($once, JSON_THROW_ON_ERROR);
Ожидаемый результат
{"stock":0}
"{\"stock\":0}"

Во втором вызове вход уже строка, поэтому JSON честно содержит строку с JSON-текстом внутри. Не лечите это stripslashes: уберите лишний вызов encode, если протокол не требует вложенной строки.

По умолчанию видны публичные свойстваСамостоятельный пример
<?php
class ProductView
{
    public string $sku = "0041";
    private string $supplierToken = "demo-secret";
}
echo json_encode(new ProductView(), JSON_THROW_ON_ERROR);
Ожидаемый результат
{"sku":"0041"}

class задаёт устройство объекта, new создаёт экземпляр. public-поле попало в JSON, private — нет. Но случайно добавленное публичное служебное поле тоже может попасть наружу: для стабильного API лучше явно перечислять поля ответа или реализовать JsonSerializable.

Ресурс нельзя передать как содержимое файлаСамостоятельный пример
<?php
$stream = fopen('php://memory', 'w+');
if ($stream === false) {
    throw new RuntimeException("Не удалось открыть память");
}
try {
    json_encode(["stream" => $stream], JSON_THROW_ON_ERROR);
} catch (JsonException $error) {
    var_dump($error->getCode() === JSON_ERROR_UNSUPPORTED_TYPE);
} finally {
    fclose($stream);
}
Ожидаемый результат
bool(true)

Ресурс — дескриптор открытого потока, не его данные. Выберите, что действительно отправлять: строку UTF-8, URL файла, отдельную загрузку или согласованное двоичное кодирование. finally закрывает только поток, созданный этим примером.

У encode ошибка глубины относится к обходу структурыСамостоятельный пример
<?php
var_dump(json_encode(["stock" => 0], 0, 0));
var_dump(json_last_error() === JSON_ERROR_DEPTH);
try {
    json_encode(["stock" => 0], JSON_THROW_ON_ERROR, 0);
} catch (JsonException $error) {
    var_dump($error->getCode() === JSON_ERROR_DEPTH);
}
echo json_encode(2, JSON_THROW_ON_ERROR, 0);
Ожидаемый результат
bool(false)
bool(true)
bool(true)
2

Нулевой предел здесь намеренно показывает ошибочную настройку. Массив не удалось обойти: без THROW получен false, с THROW — JsonException, а не ValueError. Для скалярного 2 вложенного обхода нет, и encode вернул строку. Это наблюдаемое поведение PHP 8.5; для нормального вызова выбирайте положительный предел. Не переносите договор decode/validate на encode.

PHP-список с ключами 0,1,… в таком порядке становится JSON-массивом. Пропуски или иной порядок ключей дают JSON-объект. JSON_FORCE_OBJECT меняет форму рекурсивно; array_values используйте только для технических индексов. Строки должны быть UTF-8, циклические ссылки, INF/NAN, ресурсы и обычные enum без backing value требуют отдельного представления. JsonSerializable может выполнять пользовательский код и бросать свои исключения.

Не вставляйте полученную строку в произвольный HTML-атрибут или SQL как готовую защиту. Отдельный HTTP JSON-ответ требует Content-Type: application/json, статуса и отсутствия постороннего вывода. Встраивание JSON в HTML имеет отдельные правила контекстного экранирования.

Рядом: json_decode, json_last_error, JsonSerializable::jsonSerialize. Полный список флагов и ошибок.

Продолжение: прикладные примеры. Ниже тот же инструмент участвует в обработке ответа сервиса.

Возвращает JSON-строку; с JSON_THROW_ON_ERROR ошибки кодирования дают JsonException. Строки должны быть UTF-8. Явно выбирайте поля ответа.

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

$row = ['sku' => '0012', 'stock' => 0, 'supplierToken' => 'secret'];
$response = ['sku' => $row['sku'], 'stock' => $row['stock']];
echo json_encode($response, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE), "\n";
Ожидаемый результат
{"sku":"0012","stock":0}

Разрешенный список полей предотвращает утечку служебного токена. В HTTP установите Content-Type:application/json и корректный статус.

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

$ids = [0 => 12, 2 => 19];
echo json_encode(array_values($ids), JSON_THROW_ON_ERROR), "\n";
Ожидаемый результат
[12,19]

Индексы здесь технические, поэтому их безопасно отбросить. У словаря ID→запись такого преобразования делать нельзя без изменения контракта.

Сравните. Для HTML нужен отдельный контекстный вывод; сериализация PHP не является заменой открытого JSON-контракта.

json_validateПроверить только синтаксис JSON, если декодированные данные сейчас не нужны.

PHP 8.3+ · json

json_validate(string $json, int $depth = 512, int $flags = 0): bool

Проверить только синтаксис JSON, если декодированные данные сейчас не нужны.

Часть договораОбъяснение
jsonТекст в UTF-8.
depthОт 1 до 2147483647; параметр вне диапазона даёт ValueError.
flags0 либо JSON_INVALID_UTF8_IGNORE. Другие флаги дают ValueError.
Возвратtrue/false; при false причина доступна в json_last_error. Это не проверка схемы данных.
Проверить текст, не строя массив
  1. JSON-текст → json_validate читает синтаксис.
  2. Верный текст → true, неверный → false и код ошибки.
  3. PHP-массив/объект результата не создаётся; схему заказа функция не проверяет.
Проверка возвращает bool, не данныеСамостоятельный пример
<?php
var_dump(json_validate('{"stock":0}'));
var_dump(json_validate('{"stock":}'));
var_dump(json_validate('null'));
Ожидаемый результат
bool(true)
bool(false)
bool(true)

Даже null — правильный JSON. Если нужны поля, сразу используйте decode с обработкой ошибки, чтобы не разбирать один текст дважды.

Проверить небольшой текст перед сохранением как документаСамостоятельный пример
<?php
function acceptsJsonDocument(string $body): bool
{
    return strlen($body) <= 4096 && json_validate($body, 32);
}
foreach (['{"enabled":true}', '{broken', 'false'] as $body) {
    var_dump(acceptsJsonDocument($body));
}
Ожидаемый результат
bool(true)
bool(false)
bool(true)

Договор этой функции допускает любой небольшой JSON-документ; настройку приложения она не применяет. Если требуются поле enabled и bool, надо декодировать и проверить их отдельно. Лимит тела проверяется до парсинга.

У validate свой короткий список флаговСамостоятельный пример
<?php
try {
    json_validate('{}', 0);
} catch (ValueError $error) {
    echo "Недопустимая глубина\n";
}
try {
    json_validate('{}', 16, JSON_THROW_ON_ERROR);
} catch (ValueError $error) {
    echo "Недопустимый флаг\n";
}
Ожидаемый результат
Недопустимая глубина
Недопустимый флаг

В PHP 8.3+ допустимы flags=0 и JSON_INVALID_UTF8_IGNORE. SUBSTITUTE и THROW сюда не относятся. Ошибка параметра — ValueError, а неправильный JSON при корректных параметрах — false.

Валидатор экономит память по сравнению с построением ненужной структуры, но всё равно получает готовую строку в памяти и разбирает её. Режим IGNORE может принять текст с пропущенными неверными байтами; это не обещание строгого сохранения входа. При false сразу считайте json_last_error или json_last_error_msg.

Рядом: json_decode, json_last_error, json_last_error_msg. Полный список флагов и ошибок.

Продолжение: прикладные примеры. Ниже тот же инструмент участвует в обработке ответа сервиса.

С PHP 8.3 возвращает bool для синтаксиса JSON. Подходит для проверки сохраненного текстового документа, когда объектная модель далее не нужна.

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

$raw = '{"enabled":true}';
echo json_validate($raw, 16) ? "СинтаксисJSON верен\n" : "НеверныйJSON\n";
Ожидаемый результат
СинтаксисJSON верен

Если настройка будет сразу применяться, одного синтаксиса мало: декодируйте и проверьте enabled как bool. Этот пример проверяет только документ.

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

echo json_validate('null') ? "JSON\n" : "НеJSON\n";
echo json_validate('{broken') ? "JSON\n" : "НеJSON\n";
Ожидаемый результат
JSON
НеJSON

Показывает различие валидности языка и формы вашего API. Null может быть законным JSON, но неверным заказом.

Сравните. json_decode нужен для использования данных. Для PHP 8.2 выберите decode с JSON_THROW_ON_ERROR.

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

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

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

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

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