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

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

Разделы этой страницы
  1. Обработка ошибок и журналирование: полный справочник функций
  2. Сбой в программе: показать, записать или остановиться?
  3. Установка и окружение: что уже есть в PHP
  4. Как читать карточки: параметры, результат и версия
  5. Настройки целиком: уровень, показ, журнал и исторические директивы
  6. Все E_*: уровни и маски без магических чисел
  7. Как PHP вызывает ваш обработчик
  8. Собрать вместе: импорт, диагностика и понятный результат
  9. HTTP, CLI и аварийное завершение: разные результаты
  10. Упражнение: диагностировать отказ журнала без рекурсии
  11. Все функции этой книги: быстрый переход
  12. Функции и методы этой темы
  13. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 145 · От основ к справочнику

Обработка ошибок и журналирование: полный справочник функций

Диагностика PHP от первого предупреждения до временных обработчиков, безопасного журнала, трассировки и завершения CLI/HTTP-запроса. Все функции book.errorfunc.

Сбой в программе: показать, записать или остановиться?

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

Выбрать уровни диагностики и вернуть настройку

Первый пример меняет маску error_reporting, проверяет наличие предупреждений и восстанавливает прежнее значение. Он не создаёт предупреждение и не пишет в журнал.

  1. error_reporting(E_ALL)

    Включаем уровни. Возвращённая прежняя маска сохраняется в $previous.

  2. Маска & E_WARNING

    Оставляем только бит предупреждений. Он ненулевой, значит уровень включён.

  3. echo → warnings:on

    Показываем результат проверки настройки, а не сообщение об ошибке.

  4. error_reporting($previous)

    Возвращаем настройку, действовавшую до примера.

Маска уровней, показ ошибок и место журнала — разные настройки. Само включение E_WARNING не доказывает успех операции.

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

Три независимых действия на примере заказа 42
  1. Чтение тарифа сообщает о проблеме → код проверяет результат операции.
  2. Политика заказа решает: взять разрешённый резервный тариф или остановить расчёт.
  3. Журнал получает код сбоя и номер запроса; посетитель получает понятный ответ без внутренних путей.

Диагностическое сообщение PHP имеет числовой уровень: например, предупреждение E_WARNING. Оно часто сопровождает неуспех операции, но само по себе не создаёт исключение. Исключение — объект, который меняет ход выполнения до подходящего catch. Преобразование предупреждения в исключение — отдельное решение программы. Начните с различия этих двух механизмов; для функций-обработчиков пригодятся объявление функции и её параметры.

Что требуетсяИнструментЧего он не обещает
Сообщить о событииtrigger_error()Не исправляет данные и не гарантирует остановку.
Выбрать уровни диагностикиerror_reporting()Не включает показ и не выбирает файл журнала.
Записать событиеerror_log()Не меняет HTTP-статус и результат операции.
Обработать warningset_error_handler()Не перехватывает все фатальные ошибки.
Завершить запрос после непойманного Throwableset_exception_handler()Не возвращает выполнение к месту сбоя.
Узнать цепочку вызововdebug_backtrace()Не является записью всей истории программы.
Убедиться, что предупреждения включеныСамостоятельный пример
<?php
declare(strict_types=1);

$previous = error_reporting(E_ALL);
echo (error_reporting() & E_WARNING) !== 0 ? "warnings:on\n" : "warnings:off\n";
error_reporting($previous);
Ожидаемый результат
warnings:on

Скобки после имени означают вызов функции. E_ALL — готовая числовая маска уровней; знак & оставляет общий бит маски и E_WARNING. !== 0 проверяет, остался ли этот бит. Первый вызов возвращает прежнюю маску, поэтому последний возвращает окружение в исходное состояние. Это только настройка: предупреждение здесь не создаётся.

Все самостоятельные примеры запускаются отдельно в CLI. Там, где нужен HTTP, почтовая система или системный журнал ОС, граница отмечена явно. Книга охватывает все 14 функций ref.errorfunc; остальные книги справочника PHP имеют собственные главы и план авторства.

Установка и окружение: что уже есть в PHP

Функции этой книги встроены в обычную поставку PHP: отдельные Composer-пакеты, DLL или PECL-модули для них не нужны. Версия всё же важна: две функции просмотра текущего обработчика появились в PHP 8.5. Текст рассчитан на базу PHP 8.2+, а отмеченные возможности 8.5 проверены на PHP 8.5.11.

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

echo function_exists('error_log') ? "logging:available\n" : "logging:missing\n";
echo function_exists('get_error_handler') ? "inspect-handlers:available\n" : "inspect-handlers:needs-8.5\n";
Ожидаемый результат
logging:available
inspect-handlers:available

Ожидаемый результат относится к PHP 8.5.11. function_exists проверяет возможность; он не устанавливает недостающую функцию. На 8.2–8.4 вторая строка будет needs-8.5, остальные старые функции продолжают работать.

CLI, локальный сервер и PHP-FPM могут читать разные php.ini. Проверка php --ini относится к запущенному бинарнику CLI. Для веб-процесса смотрят его конфигурацию и журнал сервера; диагностическую страницу с путями и настройками не оставляют публичной. После правки конфигурации долгоживущим процессам обычно нужен перезапуск по правилам конкретного сервера. Подробнее — первый запуск PHP.

Как читать карточки: параметры, результат и версия

Запись error_log(string $message, int $message_type = 0): bool описывает контракт, а не строку для вставки в программу. string означает строку, int — целое, bool — true или false. После двоеточия указан результат. = 0 означает значение по умолчанию, если аргумент не передан. Полная сигнатура error_log содержит ещё два необязательных аргумента; краткая запись здесь нужна только для чтения обозначений.

?array означает массив или null; ?callable — вызываемое значение или null. void означает отсутствие полезного возвращаемого значения. Тип true уже, чем bool: нормальный возврат не бывает false. Сигнатуры карточек соответствуют PHP 8.5; несовместимые дополнения отмечены. «С PHP 8.2+» в карточке — минимальная версия этого учебного кода, а не утверждение, что старой функции не существовало раньше.

Глобальные callback-настройки относятся к текущему выполнению. Обычные независимые HTTP-запросы не передают друг другу эту переменную PHP автоматически; долгоживущий worker повторно выполняет задачи в одном процессе, поэтому неснятый обработчик и старый error_get_last особенно опасны. Нельзя предполагать, что установкой функции мы изменили постоянную конфигурацию всех серверных процессов.

Настройки целиком: уровень, показ, журнал и исторические директивы

Представьте два независимых выхода: экран посетителя и журнал разработчика. display_errors управляет первым, log_errors — автоматической записью во второй. error_reporting выбирает уровни для встроенного обработчика. Вызов error_log() — отдельная команда записать событие.

Значения ниже — значения PHP по документации, а не обещание настройки вашего хостинга: поставщик может подключить свой php.ini. INI_ALL допускает изменение во время выполнения там, где это не запрещено администратором; INI_SYSTEM — настройка запуска/сервера. INI_ALL не означает, что ini_set успеет сработать до ошибки компиляции самого файла.

ДирективаПо умолчаниюРежимПрактический смысл
error_reportingE_ALLINI_ALLНабор уровней, которые обрабатывает встроенная диагностика. Имена E_* удобнее числовой суммы. Свой callback проверяет эту маску самостоятельно.
display_errors1INI_ALLПоказывать диагностику: Off выключает; On/stdout выводит в обычный поток; stderr поддерживается CLI, phpdbg и CGI. В публичном HTTP-ответе выключают.
display_startup_errors1 с 8.0INI_ALLДиагностика старта PHP. Настраивается до запуска проблемного кода; ini_set внутри файла не исправит сбой, из-за которого файл не начал выполняться.
log_errors0INI_ALLАвтоматическая запись диагностик PHP. Не является выключателем явных вызовов error_log(). На сервере обычно включают.
error_logне заданINI_ALLФайл, доступный на запись пользователю PHP, либо syslog. Без значения используется журнал SAPI: например, stderr в CLI. Задавайте управляемый абсолютный путь вне публичной директории.
error_log_mode0644INI_ALLС PHP 8.2: режим создаваемого файла error_log. На Unix учитывайте umask; это не смена владельца и не ремонт прав существующего файла. На Windows действуют ACL.
fatal_error_backtraces1INI_ALLС PHP 8.5: трассировка фатальных диагностик, когда она доступна. Это не разрешение продолжить выполнение после фатального сбоя.
ignore_repeated_errors0INI_ALLСкрывать повторяющуюся диагностику. Обычно учитываются текст, файл и строка. Потеря повторов затрудняет подсчёт сбоев: для мониторинга лучше агрегировать события осознанно.
ignore_repeated_source0INI_ALLПри включённом ignore_repeated_errors не учитывать различие файла и строки. Самостоятельно дедупликацию не включает.
html_errors1; CLI обычно 0INI_ALLФормат показа с HTML-разметкой. Для CLI и JSON/API нужен обычный текст либо полностью выключенный показ. Разметка ошибок не формирует безопасный публичный API.
docref_rootпустая строкаINI_ALLАдрес справочника в HTML-диагностике разработчика. Например, /manual/. Не настройка перенаправления браузера и не адрес журнала.
docref_extпустая строкаINI_ALLСуффикс страниц справочника, например .html; начинается с точки. Имеет смысл вместе с docref_root и HTML-форматом.
error_prepend_stringне заданINI_ALLТекст перед отображаемым сообщением. Не применяется как шаблон структурированного журнала.
error_append_stringне заданINI_ALLТекст после отображаемого сообщения. Обёртки вокруг ошибки не делают её безопасной для выдачи посетителю.
syslog.facilityLOG_USERINI_SYSTEMКатегория приложения для системного журнала при error_log=syslog; задаётся администратором до запроса.
syslog.identphpINI_SYSTEMМетка приложения для системного журнала при error_log=syslog. Это полезно для отделения нескольких сервисов.
syslog.filterno-ctrlINI_ALLС PHP 7.3: all делит по переводу строки, ascii также экранирует непечатный ASCII, no-ctrl — непечатные символы; raw передаёт как есть без разделения. raw есть с 7.3.8/7.4. На Windows директива не поддерживается.
report_memleaks1INI_ALLДиагностика Zend memory manager только в debug-сборке с E_WARNING; с PHP 8.5 устарела. POSIX: stderr; Windows: OutputDebugString в отладчик, например DbgView. Не заменяет измерение памяти приложения.
log_errors_max_lenисторически 1024удаленаС 8.0 не действовала, с 8.1 удалена. Не ограничивает размер сообщений error_log(); длину и состав событий контролирует приложение.
track_errorsисторически 0удаленаУстарела с 7.2, удалена с 8.0 вместе с механизмом $php_errormsg. Используйте error_get_last либо локальный callback.
xmlrpc_errors0INI_SYSTEMФорматирует отображаемые ошибки как XML-RPC fault. В PHP 8.5 это настройка ядра, а не свидетельство установленного XML-RPC расширения. Не превращает обычный endpoint в готовый XML-RPC или JSON API.
xmlrpc_error_number0INI_ALLЗначение faultCode при xmlrpc_errors=On. Для обычного JSON API создавайте структуру ответа и HTTP-статус явно; чужой протокол не надо включать ради сокрытия warning.
Локальная разработка: fragment php.iniФрагмент или команда
error_reporting = E_ALL
display_errors = On
display_startup_errors = On
log_errors = On
html_errors = Off

Это INI, не PHP-код. Путь к отдельному учебному журналу выбирайте в своей конфигурации; после упражнения удалите диагностический файл. На публичном сервере показ выключают, а журнал размещают вне document root.

Публичный сервер: базовый fragment php.iniФрагмент или команда
error_reporting = E_ALL
display_errors = Off
display_startup_errors = Off
log_errors = On
html_errors = Off

Отдельно настройте доступный error_log, ротацию, права и срок хранения; этот фрагмент не настраивает ОС и веб-сервер. Сохраняйте E_ALL: скрывать диагноз от посетителя и скрывать его от разработчика — разные решения.

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

$previous = ini_set('html_errors', '0');
try {
    echo 'html:' . ini_get('html_errors') . "\n";
    echo ini_get('track_errors') === false ? "track_errors:absent\n" : "track_errors:present\n";
} finally {
    if ($previous !== false) {
        ini_set('html_errors', $previous);
    }
}
Ожидаемый результат
html:0
track_errors:absent

false от ini_get означает отсутствующую директиву; строка "0" — существующее выключенное значение. Строгое сравнение сохраняет эту разницу. ini_set тоже возвращает false при неуспехе. finally возвращает временную настройку при обычном выходе или исключении, но не даёт гарантий при принудительном завершении процесса.

Не отправляйте в журнал пароли, Authorization, cookies, полные тела банковских запросов, платежные реквизиты и дампы объектов клиента. Вместо «записать всё» начните с списка разрешённых полей: код события, внутренний номер операции, случайный корреляционный идентификатор. Даже сообщение исключения может содержать URL с токеном. Ограничение длины — дополнительный барьер, а не способ удалить секрет из строки.

Все E_*: уровни и маски без магических чисел

Уровень одной диагностики — одна подписанная лампочка, а маска — пульт с выбранными лампочками. | объединяет выбранные биты, & оставляет общие, ~ инвертирует. Операторы || и && работают с логическими условиями и для сборки маски не подходят. Это модель выбора, а не шкала «чем больше число, тем опаснее».

КонстантаЧисло для чтения старой конфигурацииЧто означает
E_ERROR1Фатальная диагностика выполнения; обычный callback её не перехватывает.
E_WARNING2Предупреждение выполнения. Проверяйте также возвращённый результат операции.
E_PARSE4Ошибка синтаксического разбора; обработчик в неразобранном файле ещё не зарегистрирован.
E_NOTICE8Диагностическое замечание. Не каждая проблема из старого PHP осталась notice в PHP 8.
E_CORE_ERROR16Фатальный сбой при старте ядра PHP.
E_CORE_WARNING32Предупреждение старта ядра PHP.
E_COMPILE_ERROR64Фатальный сбой компиляции Zend Engine.
E_COMPILE_WARNING128Предупреждение этапа компиляции.
E_USER_ERROR256Создаваемая кодом фатальная диагностика; передача в trigger_error устарела с 8.4. Для отказа используйте исключение или осознанный exit.
E_USER_WARNING512Предупреждение библиотеки через trigger_error; продолжение определяет обработчик и окружающий код.
E_USER_NOTICE1024Замечание библиотеки; уровень trigger_error по умолчанию.
E_STRICT2048Исторический уровень, больше не используется; сама константа устарела с 8.4. Не добавляйте её к E_ALL.
E_RECOVERABLE_ERROR4096Редкий исторический механизм восстанавливаемой фатальной диагностики внутренних объектов; не синоним любого Throwable.
E_DEPRECATED8192Движок сообщает об устаревшем использовании языка или встроенной функции.
E_USER_DEPRECATED16384Автор библиотеки сообщает об устаревшем публичном API.
E_ALL30719 в 8.4+Все действующие уровни; до 8.4 было 32767. Используйте имя, не сохранённое число.
Выбрать две диагностики своего импортаСамостоятельный пример
<?php
declare(strict_types=1);

$levels = E_USER_WARNING | E_USER_DEPRECATED;
echo ($levels & E_USER_WARNING) !== 0 ? "warning:selected\n" : "warning:skipped\n";
echo ($levels & E_USER_NOTICE) !== 0 ? "notice:selected\n" : "notice:skipped\n";
Ожидаемый результат
warning:selected
notice:skipped

Маска пригодится вторым параметром set_error_handler. Она не присваивает сообщению два уровня: trigger_error принимает один допустимый E_USER_* за вызов.

В php.ini можно писать имена констант; Apache httpd.conf не вычисляет имена PHP-констант. Там следуют синтаксису интеграции и задают числовую маску. Для новых диагностических обёрток не копируйте числовое E_ALL из старого сервера. Флаги DEBUG_BACKTRACE_PROVIDE_OBJECT и DEBUG_BACKTRACE_IGNORE_ARGS относятся к другой маске — составу трассировки — и не смешиваются с E_*.

Как PHP вызывает ваш обработчик

Предупреждение в импорте → временный callback → продолжение
  1. set_error_handler сохраняет прежний обработчик и регистрирует функцию.
  2. trigger_error создаёт сообщение → PHP передаёт уровень, текст, файл и строку в callback.
  3. callback вернул true → встроенный вывод пропущен; false → PHP применяет стандартную диагностику.
  4. Обычный возврат из callback → продолжается код после проблемной операции; throw/exit меняют этот путь.
  5. restore_error_handler снимает временный слой вместе с его маской.

Callback — обычная функция, которую вызываете не вы в этой строке, а PHP в ответ на событие. set_error_handler("reportImport") передаёт имя; reportImport() немедленно вызывает функцию. Объявление function reportImport($level, $message) ещё не обрабатывает ни одного события. $level и $message — локальные параметры, значения в них передаст PHP. Только строго false возвращает событие стандартному обработчику. Если забыть return, функция вернёт null и стандартное сообщение тоже исчезнет; поэтому явно возвращайте true или false.

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

function reportImport($level, $message) {
    echo 'Import: ' . $message . "\n";
    return true;
}
set_error_handler('reportImport', E_USER_WARNING);
trigger_error('delivery-label-missing', E_USER_WARNING);
restore_error_handler();
echo "next-row\n";
Ожидаемый результат
Import: delivery-label-missing
next-row

true здесь означает «встроенную обработку этого сообщения можно пропустить», а не «этикетка успешно создана». Условие задачи разрешило продолжить следующую строку. В реальном импорте отдельно фиксируйте, какая запись пропущена, и возвращайте итог импорта.

В простом примере следующий шаг очевиден. В библиотеке вокруг временной регистрации ставят try/finally: иначе исключение оставит глобальную для текущего выполнения настройку чужому коду. Функцию обработчика держат короткой. Если журнал недоступен или обработчик сам бросил исключение, нельзя бесконечно вызывать тот же журнал по кругу. Ошибка внутри error-handler не обрабатывается тем же обработчиком рекурсивно; стандартная диагностика и отказ самого callback всё равно требуют внимания.

Собрать вместе: импорт, диагностика и понятный результат

Импортируем две позиции. Для первой есть артикул, для второй его нет. Правило задачи разрешает пропуск некорректной строки и требует итог: принято 1, пропущено 1. Диагностику собираем отдельно. Покупателю или оператору не нужен технический dump, а разработчику полезен короткий код проблемы.

Две строки → один полезный результат и один диагноз
  1. row-1: SKU=A10 → добавить артикул в результат
  2. row-2: SKU отсутствует → создать row-missing-sku
  3. callback сохраняет разрешённый код; импорт пропускает строку
  4. finally возвращает обработчик; итог содержит 1 принятую строку
Пропустить некорректную строку и сохранить понятную диагностикуСамостоятельный пример
<?php
declare(strict_types=1);

$rows = [['sku' => 'A10'], ['title' => 'Chair']];
$events = [];
$accepted = [];
set_error_handler(function ($level, $message) use (&$events) {
    if ((error_reporting() & $level) === 0) {
        return false;
    }
    $events[] = ['event' => 'row-missing-sku', 'level' => $level];
    return true;
}, E_USER_WARNING);
try {
    foreach ($rows as $row) {
        if (!isset($row['sku']) || !is_string($row['sku']) || $row['sku'] === '') {
            trigger_error('row-missing-sku', E_USER_WARNING);
            continue;
        }
        $accepted[] = $row['sku'];
    }
} finally {
    restore_error_handler();
}
echo 'accepted:' . implode(',', $accepted) . "\n";
echo 'diagnostics:' . count($events) . "\n";
echo $events[0]['event'] . "\n";
Ожидаемый результат
accepted:A10
diagnostics:1
row-missing-sku

continue явно пропускает текущую строку; trigger_error сам этого не делает. Контекст опыта узкий: callback знает единственный разрешённый код события. Для большой библиотеки лучше передавать структурированные события в собственный сборщик напрямую: не нужно превращать каждый ожидаемый отказ в предупреждение PHP. Журналирование через error_log можно выполнить после сбора, с политикой ошибок записи и без исходных строк с персональными данными.

Официальная страница Examples исторически показывает собственный XML-журнал и уведомление разработчику. Её старый код использует уже удалённый пятый параметр errcontext, WDDX и E_USER_ERROR. Здесь сохранена полезная идея разделения сообщения и обработки, а решение написано заново для PHP 8. Не копируйте демонстрацию старого PHP в современный сервис без проверки версии и безопасности.

HTTP, CLI и аварийное завершение: разные результаты

ОкружениеЧто увидит потребительГде хранить диагноз
CLI workerСтатус процесса 0/не 0; stdout с успешным результатомstderr либо настроенный журнал; статус задаётся отдельно.
JSON APIHTTP 500 и стабильный JSON при внутреннем отказеСерверный журнал с request_id; без токенов и трассы в ответе.
HTML сайтПонятная страница сбоя с корректным HTTP-статусомСерверный журнал; данные сообщения экранируются, если всё же выводятся локально.
Авария до запуска файлаКод обработки внутри файла ещё не работаетСтартовая конфигурация PHP и журнал веб-сервера.
Граница учебного JSON API: только для указанного HTTP-окруженияПример для указанного окружения
<?php
declare(strict_types=1);

// До этого файла не должно быть отправленного тела ответа.
// display_errors=Off и журнал настраиваются до запроса в php.ini.
set_exception_handler(function (Throwable $problem): void {
    $requestId = 'demo-request-42';
    error_log(json_encode([
        'event' => 'request-failed',
        'request_id' => $requestId,
        'error_class' => $problem::class,
    ], JSON_THROW_ON_ERROR));
    http_response_code(500);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode([
        'error' => 'internal_error',
        'request_id' => $requestId,
    ], JSON_THROW_ON_ERROR);
});
throw new RuntimeException('Demo failure');

Контекстный пример проверяется синтаксически, не HTTP-сервером. Ожидаемый договор для клиента: HTTP 500, JSON с error и request_id. В реальной границе request_id создаётся для каждого запроса; константа здесь связывает строки учебного примера. Журнал должен быть заранее настроен и проверен: true от error_log(тип 0) не доказывает сохранение. При уже отправленных заголовках обработчик не может отменить предыдущий ответ. Этот файл не подключают в WordPress поверх обработчиков платформы.

Не пытайтесь «перехватить всё» комбинацией handler и shutdown. Нехватка памяти, обрыв процесса, остановка контейнера или ошибка до регистрации оставляют разные возможности. Shutdown — последняя ограниченная попытка записать факт, а не транзакция и не место для повторной оплаты. Для внешней операции храните состояние и ключ идемпотентности независимо от вывода PHP.

Место для продолжения после ожидаемого отказа — локальный catch, проверенный false или структурированный результат. Последняя граница предназначена для завершения, а не скрытия ошибки с ответом 200.

Упражнение: диагностировать отказ журнала без рекурсии

Проверьте, что случится, если назначение файла журнала недоступно. Нужны два разных результата: error_log вернул false и PHP сообщил warning. Не выводите исходный warning с внутренним путём пользователю. Не пытайтесь в его обработчике снова писать в тот же файл.

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

$parentFile = tempnam(sys_get_temp_dir(), 'php-log-parent-');
if ($parentFile === false) {
    throw new RuntimeException('Temporary file unavailable');
}
$warnings = 0;
set_error_handler(function ($level, $message) use (&$warnings) {
    $warnings++;
    return true;
}, E_WARNING);
try {
    // Обычный файл не может быть каталогом для дочернего файла.
    $written = error_log("demo-event\n", 3, $parentFile . '/events.log');
    echo $written ? "write:ok\n" : "write:failed\n";
    echo $warnings > 0 ? "diagnostic:captured\n" : "diagnostic:none\n";
} finally {
    restore_error_handler();
    unlink($parentFile);
}
Ожидаемый результат
write:failed
diagnostic:captured

Отказ воспроизводится без изменения прав каталогов и без записи вне временной области. Считаем наличие warning, а не платформозависимый английский текст ОС. В приложении о проблеме журнала должен сообщить независимый мониторинг/резервный маршрут; подавление warning в этом опыте нужно для проверки, а не для незаметной потери всех рабочих событий.

Развитие упражнения: к импорту из предыдущего раздела добавьте allowed/skipped и убедитесь, что количество пропусков вычисляет сам импорт, а не error_get_last. Затем временно заставьте выбранный callback бросить исключение: finally должен вернуть предыдущую регистрацию. Проверяйте не только строки stdout, но и обещанную модель продолжения.

Все функции этой книги: быстрый переход

ФункцияЗадача
debug_backtrace()Узнать, по какой цепочке вызовов мы пришли в текущую точку.
debug_print_backtrace()Быстро напечатать текущий стек в локальной отладке или временно перехватить его как текст.
error_clear_last()Убрать старую диагностическую запись перед отдельным опытом или новой задачей worker.
error_get_last()Прочитать последнюю стандартную диагностику сразу после неудачной операции.
error_log()Сохранить короткое событие для разработчика и оставить ответ посетителю чистым.
error_reporting()Выбрать диагностические уровни на время операции и вернуть прежний режим.
get_error_handler()В PHP 8.5 прочитать текущий callback без временной замены настройки.
get_exception_handler()Узнать, кому достанется непойманное исключение, не вызывая обработчик.
restore_error_handler()Снять свой временный обработчик вместе с маской и вернуть хозяину его окружение.
restore_exception_handler()Вернуть предыдущую последнюю границу после временной регистрации.
set_error_handler()Временно обработать предупреждения операции, не засоряя её обычный результат.
set_exception_handler()Задать последнюю границу запроса, когда Throwable не пойман ближе к операции.
trigger_error()Сообщить вызывающему коду о диагностическом событии вашей библиотеки.
user_error()Узнать в старой библиотеке второе имя trigger_error и сохранить поведение при миграции.

Последовательный маршрут: уровни → callback → временная регистрация и восстановление → журнал → последняя диагностика → трассировка → внешняя граница. Для поиска конкретной функции порядок карточек не обязателен. Связанные темы: исключения и выбор catch, работа с файлами, запуск долгоживущего worker.

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

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

error_reportingВыбрать диагностические уровни на время операции и вернуть прежний режим.

PHP 8.2+ · Встроено в PHP

error_reporting(?int $error_level = null): int

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

ВходЧто означает
$error_levelnull или отсутствие аргумента — только прочитать. Целое число — новая битовая маска E_*. Ноль исключает все уровни из стандартного отчёта; E_ALL включает действующие.

Результат и отказ. При установке возвращается прежняя маска, при чтении — текущая. Неверный тип аргумента нарушает контракт; strict_types не превращает произвольную строку в маску. Функция не проверяет доступность журнала.

Что меняется при вызове error_reporting()
  1. E_ALL → включены уровни
  2. E_ALL & ~E_USER_NOTICE → исключён notice
  3. finally → возвращена прежняя маска
Отдельный режим для проверки старого импортаСамостоятельный пример
<?php
declare(strict_types=1);

$original = error_reporting(E_ALL);
$previous = error_reporting(E_ALL & ~E_USER_NOTICE);
try {
    echo ($previous & E_USER_NOTICE) !== 0 ? "previous:notice-on\n" : "previous:notice-off\n";
    echo (error_reporting() & E_USER_NOTICE) !== 0 ? "now:notice-on\n" : "now:notice-off\n";
} finally {
    error_reporting($previous);
}
echo error_reporting() === E_ALL ? "restored\n" : "changed\n";
error_reporting($original);
Ожидаемый результат
previous:notice-on
now:notice-off
restored

Это демонстрация временной маски, а не совет скрыть проблемы старого кода навсегда. ~ исключает бит notice, & ограничивает результат действующими E_ALL. Возвращённое число важно сохранить до установки нового значения.

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

$original = error_reporting(E_ALL);
set_error_handler(function ($level, $message) {
    if ((error_reporting() & $level) === 0) {
        return false;
    }
    echo 'record:' . $message . "\n";
    return true;
});
try {
    @trigger_error('hidden-test', E_USER_WARNING);
    trigger_error('visible-test', E_USER_WARNING);
} finally {
    restore_error_handler();
    error_reporting($original);
}
Ожидаемый результат
record:visible-test

PHP всё равно вызывает callback при подавленной диагностике. Проверка конкретного бита пропускает suppressed warning в стандартный обработчик, где он также подавлен. В PHP 8 проверка error_reporting() === 0 не является корректным общим тестом для @. @ показан для совместимости со старой зависимостью; явная обработка ожидаемого отказа понятнее постоянного подавления.

В рабочем приложении маска обычно настраивается один раз при запуске. Переключение вокруг большого блока затрагивает весь его код, включая чужие библиотеки. В долгоживущем worker неснятая маска останется следующей задаче. Сообщение с уровнем E_USER_DEPRECATED лучше направить в разработческую диагностику и исправить вызов, чем сделать ошибку невидимой.

Рядом: set_error_handler(), error_log(), trigger_error().

trigger_errorСообщить вызывающему коду о диагностическом событии вашей библиотеки.

PHP 8.2+; true и deprecated E_USER_ERROR — с 8.4 · Встроено в PHP

trigger_error(string $message, int $error_level = E_USER_NOTICE): true

Сообщить вызывающему коду о диагностическом событии вашей библиотеки.

ВходЧто означает
$messageТекст сообщения. Передавайте короткий код события и безопасный контекст, длину ограничивайте в своём контракте. Байт и символ UTF-8 различаются; на автоматическое безопасное усечение не полагайтесь.
$error_levelОдин уровень E_USER_NOTICE, E_USER_WARNING, E_USER_DEPRECATED либо исторический E_USER_ERROR. Обычный E_WARNING и объединённая маска здесь недопустимы.

Результат и отказ. При нормальном возврате — true: это факт создания диагностики, а не успех операции. Недопустимый уровень с PHP 8.0 вызывает ValueError. Обработчик может бросить исключение или завершить выполнение, и тогда функция до возврата не дойдёт.

Что меняется при вызове trigger_error()
  1. Строка импорта без валюты → разрешён RUB
  2. currency-defaulted → callback получает уровень и текст
  3. true → диагностика создана; currency остаётся RUB
Опциональная колонка импорта: сообщить о выбранном значенииСамостоятельный пример
<?php
declare(strict_types=1);

set_error_handler(function ($level, $message) {
    echo 'diagnostic:' . $message . "\n";
    return true;
}, E_USER_NOTICE);
try {
    $row = ['name' => 'Desk'];
    $currency = $row['currency'] ?? 'RUB';
    if (!array_key_exists('currency', $row)) {
        $reported = trigger_error('currency-defaulted');
        echo $reported ? "diagnostic:sent\n" : "diagnostic:not-sent\n";
    }
    echo 'currency:' . $currency . "\n";
} finally {
    restore_error_handler();
}
Ожидаемый результат
diagnostic:currency-defaulted
diagnostic:sent
currency:RUB

Правило импорта заранее разрешает RUB как значение по умолчанию. Диагностика поясняет решение, а не легализует произвольную валюту. Если валюта обязательна для расчёта, нужно отклонить строку валидацией вместо подстановки.

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

function calculateDelivery($weight) {
    return $weight <= 1 ? 300 : 500;
}
function oldDeliveryPrice($weight) {
    trigger_error('Use calculateDelivery()', E_USER_DEPRECATED);
    return calculateDelivery($weight);
}
set_error_handler(function ($level, $message) {
    echo 'migration:' . $message . "\n";
    return true;
}, E_USER_DEPRECATED);
try {
    echo 'price:' . oldDeliveryPrice(2) . "\n";
} finally {
    restore_error_handler();
}
Ожидаемый результат
migration:Use calculateDelivery()
price:500

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

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

try {
    trigger_error('Wrong level in library', E_WARNING);
} catch (ValueError $error) {
    echo "configuration:invalid-level\n";
}
Ожидаемый результат
configuration:invalid-level

catch ловит ошибку самого вызова trigger_error. E_WARNING — уровень движка, E_USER_WARNING — допустимый уровень, который создаёт приложение.

Рядом: user_error(), set_error_handler(), error_log().

user_errorУзнать в старой библиотеке второе имя trigger_error и сохранить поведение при миграции.

PHP 8.2+; уточнение возврата true — с 8.4 · Встроено в PHP

user_error(string $message, int $error_level = E_USER_NOTICE): true

Узнать в старой библиотеке второе имя trigger_error и сохранить поведение при миграции.

ВходЧто означает
$messageТот же текст, что у trigger_error; предпочтительны короткие диагностические коды без секретов. Не рассчитывайте на автоматическое ограничение длины; расхождение старого описания с PHP 8.5 разобрано в основной карточке.
$error_levelТот же один E_USER_*; по умолчанию E_USER_NOTICE. E_USER_ERROR устарел с 8.4.

Результат и отказ. Полный псевдоним trigger_error: при нормальном возврате true, неверный уровень вызывает ValueError в PHP 8. Имя user не означает ошибку посетителя сайта или HTTP-ответ 400.

Что меняется при вызове user_error()
  1. Старый legacyImportImage обнаружил пустое имя
  2. user_error → тот же механизм, что trigger_error
  3. callback записал событие; legacyImportImage вернул false
Старая библиотека предупреждает о пропуске изображенияСамостоятельный пример
<?php
declare(strict_types=1);

function legacyImportImage($image) {
    if ($image === '') {
        user_error('catalog-image-skipped', E_USER_WARNING);
        return false;
    }
    return true;
}
set_error_handler(function ($level, $message) {
    echo 'import:' . $message . "\n";
    return true;
}, E_USER_WARNING);
try {
    $saved = legacyImportImage('');
    echo $saved ? "saved\n" : "not-saved\n";
} finally {
    restore_error_handler();
}
Ожидаемый результат
import:catalog-image-skipped
not-saved

Диагностика и результат функции разделены: возврат false явно сообщает, что изображение не сохранено. Продолжение работы определяется импортом, а не названием user_error.

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

$events = [];
set_error_handler(function ($level, $message) use (&$events) {
    $events[] = [$level, $message];
    return true;
}, E_USER_DEPRECATED);
try {
    user_error('old-field-name', E_USER_DEPRECATED);
    trigger_error('old-field-name', E_USER_DEPRECATED);
    echo $events[0] === $events[1] ? "same-contract\n" : "different\n";
} finally {
    restore_error_handler();
}
Ожидаемый результат
same-contract

use (&$events) позволяет callback дописывать внешний учебный массив. В тесте сравниваем именно контракт события, а не изменяющиеся номера строк. В новом коде trigger_error обычно яснее сообщает действие; менять имя ради производительности оснований нет.

Рядом: trigger_error(), set_error_handler(), error_reporting().

set_error_handlerВременно обработать предупреждения операции, не засоряя её обычный результат.

PHP 8.2+ · Встроено в PHP

set_error_handler(?callable $callback, int $error_levels = E_ALL): ?callable

Временно обработать предупреждения операции, не засоряя её обычный результат.

ВходЧто означает
$callbackИмя функции, замыкание, допустимый callable; null включает встроенную обработку, сохраняя прежний слой в стеке. PHP передаёт int $errno, string $errstr, string $errfile, int $errline; callback может принимать только нужные первые параметры.
$error_levelsМаска диагностических уровней для callback, E_ALL по умолчанию. Она независима от error_reporting. Не всякий уровень перехватывается.
Возврат callbackСтрого false отдаёт событие стандартному обработчику. true отмечает обработанным. Для ясности возвращайте bool явно. throw прерывает обычное продолжение.

Результат и отказ. Возвращает ранее установленный callable либо null, если использовался встроенный обработчик. Непригодный callback вызывает TypeError. Регистрация сама не вызывает callback и не создаёт ошибку.

Что меняется при вызове set_error_handler()
  1. reportRow объявлена → пока не вызывается
  2. Регистрация E_USER_WARNING → callback выбран
  3. Событие missing-sku → reportRow получает текст
  4. finally → прежний обработчик снова активен
Регистрация не запускает функциюСамостоятельный пример
<?php
declare(strict_types=1);

function reportRow($level, $message) {
    echo 'row:' . $message . "\n";
    return true;
}
set_error_handler('reportRow', E_USER_WARNING);
try {
    echo "registered\n";
    trigger_error('missing-sku', E_USER_WARNING);
    echo "continued\n";
} finally {
    restore_error_handler();
}
Ожидаемый результат
registered
row:missing-sku
continued

Имя reportRow передано строкой без круглых скобок. PHP вызывает его только на третьей строке внутри try, после события trigger_error. Сам callback не доказывает, что строка товара корректна: импорт решает, пропустить её или остановиться.

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

set_error_handler(function ($level, $message, $file, $line) {
    if ((error_reporting() & $level) === 0) {
        return false;
    }
    throw new ErrorException($message, 0, $level, $file, $line);
}, E_WARNING);
try {
    $data = file_get_contents(__DIR__ . '/missing-rates.json');
    echo 'bytes:' . strlen($data) . "\n";
} catch (ErrorException $error) {
    echo "delivery-rates:unavailable\n";
} finally {
    restore_error_handler();
}
echo "request-boundary\n";
Ожидаемый результат
delivery-rates:unavailable
request-boundary

Каталог примера изолирован, файл намеренно отсутствует. ErrorException переносит уровень, исходный файл и строку в исключение. Сырой текст содержит путь, поэтому посетителю отдаём собственный код. Обработчик действует только вокруг выбранной операции; нельзя без согласования менять модель ошибок всего чужого приложения.

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

set_error_handler(function ($level, $message, $file, $line) {
    echo 'base:' . $message . "\n";
    return true;
}, E_USER_WARNING);
$count = 0;
$previous = null;
$previous = set_error_handler(function ($level, $message, $file, $line) use (&$count, &$previous) {
    $count++;
    return $previous !== null ? $previous($level, $message, $file, $line) : false;
}, E_USER_WARNING);
try {
    trigger_error('rate-row-skipped', E_USER_WARNING);
    echo 'count:' . $count . "\n";
} finally {
    restore_error_handler();
    restore_error_handler();
}
Ожидаемый результат
base:rate-row-skipped
count:1

false сам по себе не вызывает предыдущий пользовательский callback: поэтому здесь он вызывается явно. Маски двух слоёв намеренно одинаковы. Для произвольного чужого обработчика прежняя маска недоступна через getter, и прозрачное автоматическое объединение требует отдельного договора. Две регистрации требуют двух восстановлений.

Рядом: restore_error_handler(), get_error_handler(), error_reporting(), set_exception_handler().

restore_error_handlerСнять свой временный обработчик вместе с маской и вернуть хозяину его окружение.

PHP 8.2+ · Встроено в PHP

restore_error_handler(): true

Снять свой временный обработчик вместе с маской и вернуть хозяину его окружение.

ВходЧто означает
Параметров нетСнимает один слой внутреннего стека регистраций. Не принимает имя функции или маску.

Результат и отказ. Всегда true, в том числе когда лишний вызов оставляет встроенный обработчик. Это не проверка «восстановлен именно мой callback». Сопоставляйте каждую свою регистрацию со своим finally.

Что меняется при вызове restore_error_handler()
  1. application активен
  2. import временно поверх application
  3. restore → import снят, application и его маска восстановлены
Вложенная обработка: сначала импорт, затем общий журналСамостоятельный пример
<?php
declare(strict_types=1);

set_error_handler(function ($level, $message) {
    echo 'application:' . $message . "\n";
    return true;
}, E_USER_WARNING);
try {
    set_error_handler(function ($level, $message) {
        echo 'import:' . $message . "\n";
        return true;
    }, E_USER_WARNING);
    try {
        trigger_error('row-1', E_USER_WARNING);
    } finally {
        restore_error_handler();
    }
    trigger_error('after-import', E_USER_WARNING);
} finally {
    restore_error_handler();
}
Ожидаемый результат
import:row-1
application:after-import

Стек похож на стопку временных инструкций: снимается последняя установленная. Первый restore возвращает общий обработчик, второй — окружение до всего примера. Такая структура не накапливает настройки при тысячах задач worker.

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

$original = error_reporting(0);
$seen = [];
set_error_handler(function ($level, $message) use (&$seen) {
    $seen[] = $message;
    return true;
}, E_USER_WARNING);
set_error_handler(function () { return true; }, E_ALL);
restore_error_handler();
try {
    trigger_error('notice', E_USER_NOTICE);
    trigger_error('warning', E_USER_WARNING);
    echo implode(',', $seen) . "\n";
} finally {
    restore_error_handler();
    error_reporting($original);
    error_clear_last();
}
Ожидаемый результат
warning

Нулевая маска стандартной диагностики нужна только для чистого учебного вывода: notice не перехватывается восстановленным callback и не показывается PHP. Warning приходит в callback независимо от error_reporting. Результат подтверждает восстановление E_USER_WARNING, а не только адреса функции.

Рядом: set_error_handler(), get_error_handler().

get_error_handlerВ PHP 8.5 прочитать текущий callback без временной замены настройки.

PHP 8.5+ · Встроено в PHP

get_error_handler(): ?callable

В PHP 8.5 прочитать текущий callback без временной замены настройки.

ВходЧто означает
Параметров нетТолько чтение текущего пользовательского обработчика.

Результат и отказ. Точное callable-значение, переданное при регистрации, либо null для встроенной обработки. Getter не возвращает маску error_levels, список прежних слоёв или накопленные ошибки. До 8.5 вызов отсутствующей функции даст Error.

Что меняется при вызове get_error_handler()
  1. Регистрация сохраняет $handler
  2. getter возвращает тот же callable
  3. === подтверждает идентичность без выполнения тела
Проверить регистрацию замыкания, не вызывая егоСамостоятельный пример
<?php
declare(strict_types=1);

$handler = function () {
    echo "handler-called\n";
    return true;
};
set_error_handler($handler, E_USER_WARNING);
try {
    echo get_error_handler() === $handler ? "same-handler\n" : "other-handler\n";
} finally {
    restore_error_handler();
}
Ожидаемый результат
same-handler

Строки handler-called нет: чтение возвращает само замыкание, но не выполняет его тело. === сравнивает идентичность сохранённого callable. В диагностике интеграции это помогает понять, установлена ли ваша обёртка.

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

$application = function () { return true; };
set_error_handler($application, E_USER_WARNING);
try {
    $before = get_error_handler();
    set_error_handler(null);
    try {
        echo get_error_handler() === null ? "temporary:built-in\n" : "temporary:custom\n";
    } finally {
        restore_error_handler();
    }
    echo get_error_handler() === $before ? "application:restored\n" : "application:changed\n";
} finally {
    restore_error_handler();
}
Ожидаемый результат
temporary:built-in
application:restored

Даже set_error_handler(null) сохраняет прежнюю настройку в стеке. Getter наблюдает результат; восстановление выполняет restore_error_handler. Для теста библиотеки такой контроль полезнее предположения, что глобальные настройки никто не меняет.

Рядом: set_error_handler(), restore_error_handler(), get_exception_handler().

set_exception_handlerЗадать последнюю границу запроса, когда Throwable не пойман ближе к операции.

PHP 8.2+ · Встроено в PHP

set_exception_handler(?callable $callback): ?callable

Задать последнюю границу запроса, когда Throwable не пойман ближе к операции.

ВходЧто означает
$callbackCallable с одним аргументом Throwable; он получает и Exception, и Error. null включает стандартный обработчик. Callback возвращает void: значение return не возобновляет выполнение.

Результат и отказ. Возвращается прежний callable либо null, если его не было. Неверный callable вызывает TypeError. После вызова обработчика исходный скрипт заканчивается: это не запасной catch с продолжением на следующей строке.

Что меняется при вызове set_exception_handler()
  1. Операция бросила Throwable → подходящего catch нет
  2. Граница получает объект отказа
  3. Завершение запроса; продолжения после throw нет
Обработчик сообщает об отказе, следующая строка не выполняется: worker.phpФрагмент или команда
<?php
declare(strict_types=1);
set_exception_handler(function (Throwable $problem): void {
    echo 'failed:' . $problem::class . "\n";
});
throw new RuntimeException('demo operation failed');
echo "must-not-run\n";

Сохраните как worker.php в отдельной учебной папке. Это содержимое дочернего процесса; его запуск и проверяемый результат показаны ниже.

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

$command = [PHP_BINARY, '-n', '-d', 'display_errors=0', '-d', 'log_errors=0', __DIR__ . '/worker.php'];
$process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($process)) {
    throw new RuntimeException('Cannot start isolated worker');
}
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$status = proc_close($process);
echo $output;
echo 'exit:' . $status . "\n";
echo $errors === '' ? "stderr:empty\n" : "stderr:present\n";
Ожидаемый результат
failed:RuntimeException
exit:0
stderr:empty

Пример специально не вызывает exit: на проверенном CLI PHP 8.5.11 обработчик, который нормально вернулся, оставил код завершения 0. Планировщик может ошибочно считать такое задание успешным. Не полагайтесь на автоматическое превращение печати ошибки в неуспешный статус. Родитель нужен лаборатории, чтобы увидеть статус дочернего процесса. Команда задана массивом без shell; пользовательские команды не выполняются. Вывод примеров мал, поэтому последовательное чтение двух каналов допустимо здесь; это не универсальный исполнитель процессов с неограниченным выводом.

Планировщик должен увидеть, что импорт не завершён: worker.phpФрагмент или команда
<?php
declare(strict_types=1);
set_exception_handler(function (Throwable $problem): void {
    echo "import:failed request=demo-42\n";
    exit(1);
});
strlen([]);
echo "import:complete\n";

Сохраните как worker.php в отдельной учебной папке. Это содержимое дочернего процесса; его запуск и проверяемый результат показаны ниже.

Планировщик должен увидеть, что импорт не завершён: запустить worker.phpСамостоятельный пример
<?php
declare(strict_types=1);

$command = [PHP_BINARY, '-n', '-d', 'display_errors=0', '-d', 'log_errors=0', __DIR__ . '/worker.php'];
$process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($process)) {
    throw new RuntimeException('Cannot start isolated worker');
}
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$status = proc_close($process);
echo $output;
echo 'exit:' . $status . "\n";
echo $errors === '' ? "stderr:empty\n" : "stderr:present\n";
Ожидаемый результат
import:failed request=demo-42
exit:1
stderr:empty

strlen с массивом создаёт TypeError; он тоже Throwable. Политика worker явно выбирает exit(1). Текст не раскрывает аргументы, системные пути и секреты. В рабочем CLI сообщение об отказе обычно пишут в stderr, а в stdout оставляют машиночитаемый успешный результат. Родитель нужен лаборатории, чтобы увидеть статус дочернего процесса. Команда задана массивом без shell; пользовательские команды не выполняются. Вывод примеров мал, поэтому последовательное чтение двух каналов допустимо здесь; это не универсальный исполнитель процессов с неограниченным выводом.

Чтобы продолжить другие заказы, ловите ожидаемые ошибки отдельного заказа через try/catch внутри цикла. Глобальная граница предназначена для последнего ответа и завершения. В HTTP-процессе отдельно выбирают http_response_code(500); exit(1) — статус процесса, а не HTTP 500. Заголовки необходимо установить до отправки тела.

Начиная с 8.3 движок временно снимает активный exception-handler на время его вызова. С 8.3.5 запускаемый callback дополнительно помещается в стек: restore_exception_handler внутри него восстанавливает его самого; следующий restore доберётся до предыдущего слоя. Не изменяйте стек из самого обработчика. Если он вернулся без установленного активного handler, движок восстановит запущенный; самостоятельная регистрация/restore меняет эту логику. Это нужно при чтении старого кода, а не как рекомендуемая схема перехвата.

Рядом: restore_exception_handler(), get_exception_handler(), set_error_handler(), error_log().

restore_exception_handlerВернуть предыдущую последнюю границу после временной регистрации.

PHP 8.2+ · Встроено в PHP

restore_exception_handler(): true

Вернуть предыдущую последнюю границу после временной регистрации.

ВходЧто означает
Параметров нетВосстанавливается один предыдущий слой exception-handler, а не error-handler.

Результат и отказ. Всегда true. Результат не сообщает, какой callback стал активным. Нужны парные регистрации/восстановления; для проверки текущего значения в 8.5 используйте getter.

Что меняется при вызове restore_exception_handler()
  1. Граница application → временная граница test
  2. restore снимает test
  3. Следующий непойманный отказ получает application
Настройки теста не остаются следующему тестуСамостоятельный пример
<?php
declare(strict_types=1);

$application = function (Throwable $problem): void {};
set_exception_handler($application);
try {
    set_exception_handler(function (Throwable $problem): void {});
    try {
        echo "test:temporary-handler\n";
    } finally {
        restore_exception_handler();
    }
    echo get_exception_handler() === $application ? "application:restored\n" : "application:changed\n";
} finally {
    restore_exception_handler();
}
Ожидаемый результат
test:temporary-handler
application:restored

Тест проверяет настройку, поэтому здесь нет непойманного исключения. Getter требует PHP 8.5; само восстановление работает и на предыдущих поддерживаемых версиях.

Восстановленная граница действительно получает непойманный отказ: worker.phpФрагмент или команда
<?php
declare(strict_types=1);
set_exception_handler(function (Throwable $problem): void {
    echo "application:fallback\n";
    exit(2);
});
set_exception_handler(function (Throwable $problem): void {
    echo "temporary:wrong-owner\n";
    exit(3);
});
restore_exception_handler();
throw new RuntimeException('demo');

Сохраните как worker.php в отдельной учебной папке. Это содержимое дочернего процесса; его запуск и проверяемый результат показаны ниже.

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

$command = [PHP_BINARY, '-n', '-d', 'display_errors=0', '-d', 'log_errors=0', __DIR__ . '/worker.php'];
$process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($process)) {
    throw new RuntimeException('Cannot start isolated worker');
}
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$status = proc_close($process);
echo $output;
echo 'exit:' . $status . "\n";
echo $errors === '' ? "stderr:empty\n" : "stderr:present\n";
Ожидаемый результат
application:fallback
exit:2
stderr:empty

По выводу и статусу видно, какой слой получил событие после восстановления. Исходный поток не возвращается к throw. Чтобы снять ещё и application, это пришлось бы сделать до выбрасывания исключения. Родитель нужен лаборатории, чтобы увидеть статус дочернего процесса. Команда задана массивом без shell; пользовательские команды не выполняются. Вывод примеров мал, поэтому последовательное чтение двух каналов допустимо здесь; это не универсальный исполнитель процессов с неограниченным выводом.

Рядом: set_exception_handler(), get_exception_handler(), restore_error_handler().

get_exception_handlerУзнать, кому достанется непойманное исключение, не вызывая обработчик.

PHP 8.5+ · Встроено в PHP

get_exception_handler(): ?callable

Узнать, кому достанется непойманное исключение, не вызывая обработчик.

ВходЧто означает
Параметров нетЧтение текущего exception-handler без изменения стека; PHP 8.5+.

Результат и отказ. Возвращает то же callable-значение, которое передали set_exception_handler, либо null. Это не последнее исключение и не проверка качества обработчика. До 8.5 функции нет.

Что меняется при вызове get_exception_handler()
  1. Worker зарегистрировал $boundary
  2. getter читает активную границу
  3. Сравнение подтверждает настройку, не запускает обработчик
Убедиться, что worker зарегистрировал нужную границуСамостоятельный пример
<?php
declare(strict_types=1);

$boundary = function (Throwable $problem): void {
    echo "boundary-executed\n";
};
set_exception_handler($boundary);
try {
    echo get_exception_handler() === $boundary ? "boundary:registered\n" : "boundary:missing\n";
} finally {
    restore_exception_handler();
}
Ожидаемый результат
boundary:registered

Проверка запуска не должна намеренно ронять задачу. Здесь читается регистрация, а для проверки поведения используют отдельный дочерний процесс, как в карточке set_exception_handler.

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

$boundary = function (Throwable $problem): void {};
set_exception_handler($boundary);
try {
    $before = get_exception_handler();
    set_exception_handler(null);
    try {
        echo get_exception_handler() === null ? "temporary:default\n" : "temporary:custom\n";
    } finally {
        restore_exception_handler();
    }
    echo get_exception_handler() === $before ? "owner:unchanged\n" : "owner:changed\n";
} finally {
    restore_exception_handler();
}
Ожидаемый результат
temporary:default
owner:unchanged

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

Рядом: set_exception_handler(), restore_exception_handler(), get_error_handler().

error_logСохранить короткое событие для разработчика и оставить ответ посетителю чистым.

PHP 8.2+ · Встроено в PHP

error_log(string $message, int $message_type = 0, ?string $destination = null, ?string $additional_headers = null): bool

Сохранить короткое событие для разработчика и оставить ответ посетителю чистым.

ВходЧто означает
$messageТекст события. Не считайте все назначения бинарно-безопасными: в стандартном журнале нулевой байт может оборвать сообщение. Для структурированных разрешённых полей используйте JSON, а не дамп всего запроса.
$message_type0 — настроенный журнал PHP; 1 — почта; 3 — дописать в файл; 4 — непосредственно журнал SAPI. Тип 2 больше не поддерживается.
$destinationДля 1 — адрес почты; для 3 — путь файла. Для 0 и 4 назначение определяет окружение. null допустим с 8.0.
$additional_headersДополнительные почтовые заголовки только для режима 1, работающего через механизм mail(). Не формируйте их из неконтролируемого ввода; null допустим с 8.0.

Результат и отказ. Обычно true при успехе и false при отказе. Особенность типа 0: всегда true, даже если сообщение не удалось сохранить. Режим 3 при недоступном назначении может также выдать warning; возвращённый bool нужно проверять. В PHP 8.5 конкретно устаревший тип 2 и отсутствие destination для типа 3 дают ValueError. Выбирайте только документированные режимы. Признак успеха почтовой передачи не является доказательством доставки адресату.

Что меняется при вызове error_log()
  1. Событие bank-timeout + request_id=demo-42
  2. Новый массив только с разрешёнными полями
  3. JSON-строка → временный файл; token не передаётся
Записать один код события и прочитать учебный файлСамостоятельный пример
<?php
declare(strict_types=1);

$path = tempnam(sys_get_temp_dir(), 'php-error-lesson-');
if ($path === false) {
    throw new RuntimeException('Temporary file unavailable');
}
try {
    $written = error_log("catalog-import-started\n", 3, $path);
    echo $written ? "write:accepted\n" : "write:failed\n";
    echo file_get_contents($path);
} finally {
    unlink($path);
}
Ожидаемый результат
write:accepted
catalog-import-started

Режим 3 дописывает данные; перевод строки добавляет наш код. tempnam создаёт отдельный файл в системной временной папке, finally удаляет только его. В production путь выбирает конфигурация приложения, а не поле HTTP-запроса.

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

$request = ['request_id' => 'demo-42', 'token' => 'not-a-real-token', 'card' => 'demo-private'];
$event = [
    'event' => 'bank-timeout',
    'request_id' => $request['request_id'],
    'retry_allowed' => false,
];
$line = json_encode($event, JSON_THROW_ON_ERROR);
$path = tempnam(sys_get_temp_dir(), 'php-safe-event-');
if ($path === false) {
    throw new RuntimeException('Temporary file unavailable');
}
try {
    if (!error_log($line . "\n", 3, $path)) {
        throw new RuntimeException('Event write failed');
    }
    $saved = json_decode(file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
    echo $saved['event'] . ':' . $saved['request_id'] . "\n";
    echo array_key_exists('token', $saved) ? "secret:present\n" : "secret:absent\n";
} finally {
    unlink($path);
}
Ожидаемый результат
bank-timeout:demo-42
secret:absent

Журнал собирается из нового массива с разрешёнными ключами. Мы не пытаемся потом вычистить неизвестные секреты из огромного дампа. Реальный request_id создавайте и ограничивайте по формату на входе; JSON экранирует переносы строк, но не проверяет смысл полей. retry_allowed=false поясняет, что таймаут банковского вызова не доказывает отсутствие списания.

Режим 0: проверить запись отдельно от возвращённого trueСамостоятельный пример
<?php
declare(strict_types=1);

$path = tempnam(sys_get_temp_dir(), 'php-default-log-');
if ($path === false) {
    throw new RuntimeException('Temporary file unavailable');
}
$previous = ini_set('error_log', $path);
if ($previous === false) {
    unlink($path);
    throw new RuntimeException('Cannot change local error_log');
}
try {
    echo error_log('worker-ready') ? "return:true\n" : "return:false\n";
    echo str_contains(file_get_contents($path), 'worker-ready') ? "file:contains-event\n" : "file:missing-event\n";
} finally {
    ini_set('error_log', $previous);
    unlink($path);
}
Ожидаемый результат
return:true
file:contains-event

Настроенный журнал может добавить время и служебный префикс, поэтому не сравниваем весь текст с постоянной строкой. Настройка изменяется только в изолированном процессе и возвращается обратно. Для боевого сервера нужна проверка маршрута журнала при развёртывании, а не доверие true.

Режим 1 требует настроенной почтовой системы и подчиняется её ограничениям; режим 4 передаёт событие SAPI, который выбирает назначение. Эти способы не выполняются в примерах: нет реальных писем, syslog и внешних получателей. Отправку аварийных уведомлений лучше строить поверх собранных журналов с агрегацией: тысяча сбоев не должна породить тысячу синхронных писем.

Для файла нужны права пользователя PHP и контроль роста: ротация, лимиты, срок хранения. Нельзя обещать транзакционность/единый порядок строк от нескольких процессов. Для большого приложения полезен настроенный PSR-3-логгер с форматтером и обработкой назначения; сама функция error_log остаётся простым низкоуровневым маршрутом. Сбой журнала не следует обрабатывать бесконечным повтором записи в тот же недоступный журнал.

Уточнение границы binary-safe: официальный Manual предупреждает о нулевом байте в message. В реализации PHP 8.5.11 режим 3 передаёт файловому потоку явную длину строки, а режимы стандартного/SAPI-журнала используют текстовый путь. Не переносите поведение прямой записи файла на все назначения. Для диагностического события проще сохранять короткий JSON без буквальных нулевых байтов.

Рядом: error_reporting(), set_error_handler(), error_get_last().

error_get_lastПрочитать последнюю стандартную диагностику сразу после неудачной операции.

PHP 8.2+; поле trace — с 8.5 · Встроено в PHP

error_get_last(): ?array

Прочитать последнюю стандартную диагностику сразу после неудачной операции.

ВходЧто означает
Параметров нетЧитает одну последнюю запись текущего выполнения. Не читает error_log и не возвращает журнал всех ошибок.

Результат и отказ. null, если записи нет, либо массив с type, message, file, line. С PHP 8.5 у фатальной диагностики с доступной трассировкой и fatal_error_backtraces есть дополнительный trace; непойманные исключения такого поля не дают. Не предполагается фиксированно четыре ключа.

Что меняется при вызове error_get_last()
  1. Чтение файла вернуло false и warning
  2. Немедленное чтение последней диагностики
  3. Проверка type → безопасный код ответа без пути
Опциональный файл отсутствует: отличить отказ от пустого содержимогоСамостоятельный пример
<?php
declare(strict_types=1);

error_clear_last();
$contents = @file_get_contents(__DIR__ . '/optional-local-label.txt');
$last = error_get_last();
echo $contents === false ? "file:unavailable\n" : "file:read\n";
echo $last !== null && $last['type'] === E_WARNING ? "diagnostic:warning\n" : "diagnostic:other\n";
echo $last !== null && $last['line'] > 0 ? "location:available\n" : "location:missing\n";
error_clear_last();
Ожидаемый результат
file:unavailable
diagnostic:warning
location:available

В изолированной папке нет optional-local-label.txt. @ подавляет стандартный показ ожидаемого warning, но не устраняет отказ. false — отказ чтения; пустая строка могла быть успешным чтением пустого файла. error_get_last вызван немедленно: посторонняя диагностика между операцией и чтением могла бы заменить запись. Полный путь не выводится посетителю.

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

error_clear_last();
$events = [];
set_error_handler(function ($level, $message) use (&$events) {
    $events[] = $message;
    return true;
}, E_USER_WARNING);
try {
    trigger_error('row-rejected', E_USER_WARNING);
    echo 'events:' . count($events) . "\n";
    echo error_get_last() === null ? "last:none\n" : "last:present\n";
} finally {
    restore_error_handler();
}
Ожидаемый результат
events:1
last:none

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

PHP 8.5: записать факт фатальной ошибки подключаемого определения: worker.phpФрагмент или команда
<?php
declare(strict_types=1);
ini_set('fatal_error_backtraces', '1');
register_shutdown_function(function (): void {
    $last = error_get_last();
    echo $last !== null ? "shutdown:diagnostic\n" : "shutdown:clean\n";
    echo isset($last['trace']) && is_array($last['trace']) ? "trace:available\n" : "trace:unavailable\n";
});
function loadRules(): void {
    require __DIR__ . '/broken-definition.php';
}
loadRules();

Сохраните как worker.php в отдельной учебной папке. Это содержимое дочернего процесса; его запуск и проверяемый результат показаны ниже.

Дополнительный файл: broken-definition.phpФрагмент или команда
<?php
class BaseRule { final public function run() {} }
class InvalidRule extends BaseRule { public function run() {} }

Этот файл входит только в изолированный опыт; не подключайте его к рабочему приложению.

PHP 8.5: записать факт фатальной ошибки подключаемого определения: запустить worker.phpСамостоятельный пример
<?php
declare(strict_types=1);

$command = [PHP_BINARY, '-n', '-d', 'display_errors=0', '-d', 'log_errors=0', __DIR__ . '/worker.php'];
$process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($process)) {
    throw new RuntimeException('Cannot start isolated worker');
}
$output = stream_get_contents($pipes[1]);
$errors = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
$status = proc_close($process);
echo $output;
echo 'exit:' . $status . "\n";
echo $errors === '' ? "stderr:empty\n" : "stderr:present\n";
Ожидаемый результат
shutdown:diagnostic
trace:available
exit:255
stderr:empty

Внешний worker успевает зарегистрировать shutdown-функцию до подключения неверного определения. Ошибка намеренная; статус 255 зафиксирован для проверенного CLI. Shutdown не восстанавливает выполнение и не гарантирован после остановки процесса ОС или при нехватке ресурсов. Если синтаксис сломан в самом первом файле до регистрации, этот callback не существует. В рабочем обработчике фильтруйте фатальные type и не считайте любое последнее notice аварией. Родитель нужен лаборатории, чтобы увидеть статус дочернего процесса. Команда задана массивом без shell; пользовательские команды не выполняются. Вывод примеров мал, поэтому последовательное чтение двух каналов допустимо здесь; это не универсальный исполнитель процессов с неограниченным выводом.

В shutdown читайте последнюю запись до своих действий, которые тоже могут вызвать warning. Проверяйте её на null. Фатальный тип помогает распознать причину завершения, но не доказывает полноту сохранённых данных: процесс мог успеть сделать часть внешних действий. Для платежей нужна модель статуса операции, а не попытка восстановить результат по последней ошибке.

Рядом: error_clear_last(), set_error_handler(), debug_backtrace(), error_log().

error_clear_lastУбрать старую диагностическую запись перед отдельным опытом или новой задачей worker.

PHP 8.2+ · Встроено в PHP

error_clear_last(): void

Убрать старую диагностическую запись перед отдельным опытом или новой задачей worker.

ВходЧто означает
Параметров нетСбрасывает запись, которую читает error_get_last, до null.

Результат и отказ. Полезное значение не возвращается. Не стирает файл журнала, не исправляет причину и не меняет обработчик, mask или результат операции.

Что меняется при вызове error_clear_last()
  1. Запись previous-job-had-warning осталась
  2. Успешное сложение её не изменило
  3. error_clear_last → null перед новой задачей
После чтения сообщения очистить только диагностическое состояниеСамостоятельный пример
<?php
declare(strict_types=1);

@trigger_error('old-import-notice', E_USER_NOTICE);
echo error_get_last() !== null ? "before:present\n" : "before:none\n";
error_clear_last();
echo error_get_last() === null ? "after:none\n" : "after:present\n";
Ожидаемый результат
before:present
after:none

Создано безвредное учебное notice; @ оставляет вывод примера чистым. Очистка относится только к последней записи. Уже записанное событие в настроенном журнале она не удаляет.

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

@trigger_error('previous-job-had-warning', E_USER_WARNING);
$sum = 120 + 30;
echo error_get_last() !== null ? "after-success:old-warning-still-present\n" : "after-success:clean\n";
error_clear_last();
$nextResult = json_decode('{"total":150}', true, 512, JSON_THROW_ON_ERROR);
echo 'next-total:' . $nextResult['total'] . "\n";
echo error_get_last() === null ? "next-diagnostic:none\n" : "next-diagnostic:present\n";
Ожидаемый результат
after-success:old-warning-still-present
next-total:150
next-diagnostic:none

Успешное сложение не сбросило старую запись. На границе следующей изолированной задачи мы очистили её намеренно. JSON при отказе в этом режиме бросил бы JsonException: состояние error_get_last всё равно нельзя использовать как универсальный bool успеха.

Рядом: error_get_last(), set_error_handler(), restore_error_handler().

debug_backtraceУзнать, по какой цепочке вызовов мы пришли в текущую точку.

PHP 8.2+ · Встроено в PHP

debug_backtrace(int $options = DEBUG_BACKTRACE_PROVIDE_OBJECT, int $limit = 0): array

Узнать, по какой цепочке вызовов мы пришли в текущую точку.

ВходЧто означает
$optionsМаска: PROVIDE_OBJECT добавляет object, IGNORE_ARGS исключает args. 0 сохраняет аргументы без object; IGNORE_ARGS исключает оба; PROVIDE_OBJECT | IGNORE_ARGS сохраняет object, но исключает аргументы.
$limitМаксимум кадров; 0 — все. Для ответа «кто вызвал?» обычно достаточно 2. Используйте неотрицательное число с понятной целью.

Результат и отказ. Массив кадров от ближайшего вызова к внешним. Кадр может иметь function, file, line, class, type, object, args; не все поля существуют всегда. Для методов type — -> или ::. file/line описывают место вызова, а не обязательно строку объявления функции.

Что меняется при вызове debug_backtrace()
  1. shipOrder вызвал writeDeliveryLabel
  2. Внутри writeDeliveryLabel снимаем 2 активных кадра
  3. Массив: writeDeliveryLabel → shipOrder; args исключены
Посмотреть путь расчёта, без аргументовСамостоятельный пример
<?php
declare(strict_types=1);

function writeDeliveryLabel() {
    $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
    foreach ($trace as $frame) {
        echo ($frame['function'] ?? 'main') . "\n";
    }
}
function shipOrder() {
    writeDeliveryLabel();
}
shipOrder();
Ожидаемый результат
writeDeliveryLabel
shipOrder

shipOrder вызывает writeDeliveryLabel; в момент снимка оба вызова ещё выполняются. Кадр 0 — ближайшая функция. foreach последовательно читает кадры, ?? даёт запасную подпись для необязательного поля. Это не история всех уже завершённых вызовов.

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

function transportProbe($token) {
    $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
    $caller = $trace[1] ?? [];
    $name = ($caller['class'] ?? '') . ($caller['type'] ?? '') . ($caller['function'] ?? 'main');
    echo 'caller:' . $name . "\n";
    echo array_key_exists('args', $trace[0]) ? "args:present\n" : "args:omitted\n";
    echo array_key_exists('object', $caller) ? "object:present\n" : "object:omitted\n";
}
class DemoCrmClient {
    public function listDeals() {
        transportProbe('not-a-real-token');
    }
}
(new DemoCrmClient())->listDeals();
Ожидаемый результат
caller:DemoCrmClient->listDeals
args:omitted
object:omitted

Так разработчик может найти вызывающий клиент в общей точке, не записывая его настройки и аргументы. IGNORE_ARGS здесь также не включает object, потому что бит PROVIDE_OBJECT не передан. В production часто лучше явно передать безопасное имя операции, чем собирать стек на каждом запросе.

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

class TraceProbe {
    public function inspect($label) {
        $frame = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT | DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
        echo isset($frame['object']) && $frame['object'] === $this ? "object:same-instance\n" : "object:missing\n";
        echo array_key_exists('args', $frame) ? "args:present\n" : "args:omitted\n";
    }
}
(new TraceProbe())->inspect('demo');
Ожидаемый результат
object:same-instance
args:omitted

object — ссылка на реальный экземпляр, а не безопасное текстовое имя класса. Этот вариант нужен для локального исследования. Не выводите объект в журнал: его приватные настройки могут содержать секреты, а удержание объекта в трассировке продлевает его жизнь.

Снимок стека требует обхода кадров; аргументы и объекты способны удерживать большой граф данных, а последующее форматирование/сериализация ещё дороже. Ограничьте глубину и снимайте стек там, где он отвечает конкретному вопросу. Флаг не гарантирует удаления секрета из собственноручно составленного сообщения или названия файла. Для трассировок Throwable отдельно настраивается zend.exception_ignore_args; #[SensitiveParameter] с 8.2 защищает отмеченный параметр в трассе, но не все ваши журналы.

Рядом: debug_print_backtrace(), error_log(), error_get_last().

debug_print_backtraceБыстро напечатать текущий стек в локальной отладке или временно перехватить его как текст.

PHP 8.2+ · Встроено в PHP

debug_print_backtrace(int $options = 0, int $limit = 0): void

Быстро напечатать текущий стек в локальной отладке или временно перехватить его как текст.

ВходЧто означает
$optionsПо умолчанию аргументы печатаются. DEBUG_BACKTRACE_IGNORE_ARGS убирает их. PROVIDE_OBJECT принимается для совместимости, но object в текстовом формате не печатается.
$limitМаксимум печатаемых кадров; 0 означает все. Для короткой цепочки задайте осмысленный предел.

Результат и отказ. Печатает непосредственно в текущий вывод, полезного значения не возвращает. Присваивание $text = debug_print_backtrace() не сохраняет трассировку в $text. Формат содержит пути/строки, меняющиеся при переносе файла.

Что меняется при вызове debug_print_backtrace()
  1. Локальная функция создаёт свой буфер
  2. Стек печатается в буфер, не в HTTP-ответ
  3. Читаем текст → закрываем только свой буфер
Отделить диагностический текст от обычного выводаСамостоятельный пример
<?php
declare(strict_types=1);

function labelTrace() {
    ob_start();
    try {
        debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1);
        $text = ob_get_contents();
    } finally {
        ob_end_clean();
    }
    echo str_contains($text, 'labelTrace()') ? "trace:labelTrace\n" : "trace:missing\n";
}
labelTrace();
Ожидаемый результат
trace:labelTrace

ob_start создаёт наш буфер вывода, ob_get_contents читает его, ob_end_clean удаляет вместе с содержимым, не отправляя его дальше. Мы показываем стабильную проверку наличия кадра; полный текст намеренно не печатается. Такой приём подходит для собственного локального буфера, а не произвольного закрытия буферов фреймворка.

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

function callDemoCrm($token) {
    ob_start();
    try {
        debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
        $text = ob_get_contents();
    } finally {
        ob_end_clean();
    }
    echo str_contains($text, 'demo-secret-marker') ? "token:leaked\n" : "token:not-in-trace\n";
    echo str_contains($text, 'buildReport') ? "caller:buildReport\n" : "caller:missing\n";
}
function buildReport() {
    callDemoCrm('demo-secret-marker');
}
buildReport();
Ожидаемый результат
token:not-in-trace
caller:buildReport

Маркер вымышленный; реальный токен для проверки не нужен. Отсутствие одного маркера — узкая проверка этого опыта, не сертификация безопасности всего логирования. Путь файла и имена функций в трассе остаются.

Когда нужен машинный анализ — выбрать имена кадров, убрать внутренние пути, добавить разрешённые поля — проще взять массив через debug_backtrace(). Не разбирайте напечатанный текст регулярным выражением как стабильный API. Для понимания буфера есть отдельный последовательный урок.

Рядом: debug_backtrace(), error_log().

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

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

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

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

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