Отладка PHP: как эффективно использовать стек вызовов

Раздел: Обработка ошибок PHP -> Отладка PHP

Понимание стека вызовов в PHP и его применение при отладке

Основной способ получить стек вызовов - функция debug_backtrace().

Она возвращает массив с информацией о каждом уровне вызова: файл, строка, функция, класс, объект, аргументы. Это наиболее полное и гибкое средство для ручной отладки.

Как получить массив с полным стеком вызовов в любой точке скрипта?


function a() {
    b();
}
function b() {
    c();
}
function c() {
    $trace = debug_backtrace();
    print_r($trace);
}
a();
  

Результат (сокращён):

Array
(
    [0] => Array
        (
            [file] => /path/test.php
            [line] => 3
            [function] => c
            [args] => Array ()
        )
    [1] => Array
        (
            [file] => /path/test.php
            [line] => 7
            [function] => b
            [args] => Array ()
        )
    [2] => Array
        (
            [file] => /path/test.php
            [line] => 11
            [function] => a
            [args] => Array ()
        )
)
  

Проблема:

  • При большом количестве вложенностей массив может быть большим, что снижает производительность.
  • В производственной среде вывод полного стека может раскрыть чувствительные данные (пути, имена переменных).

Решение:

  • Использовать debug_backtrace() только в режиме разработки (например, под условием if (defined('DEBUG') && DEBUG)).
  • Для выборочного вызова использовать флаги: debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) - не включает аргументы, уменьшает размер.

Как вывести стек прямо в браузер без ручного форматирования?

Функция debug_print_backtrace() выводит информацию о стеке непосредственно в стандартный поток вывода (обычно в HTML).


function inner($x) {
    debug_print_backtrace();
}
function outer($val) {
    inner($val);
}
outer(42);
  

Результат (пример):

#0  inner(42) called at [/path/test.php:8]
#1  outer(42) called at [/path/test.php:11]
  

Проблема:

Вывод смешивается с остальным содержимым страницы, если не обернуть в <pre> или не использовать буферизацию.

Решение:

Перед вызовом включить буферизацию ob_start(), затем получить содержимое через ob_get_clean() и передать в лог или специальный блок.

Как получить стек из объекта исключения?

Любое исключение хранит трассировку в методе getTrace(). Это удобно при перехвате ошибок.


try {
    throw new Exception('Что-то пошло не так');
} catch (Exception $e) {
    $trace = $e->getTrace();
    // или вывести строкой:
    echo $e->getTraceAsString();
}
  

Результат getTraceAsString():

#0 {main}
  

Проблема:

Если исключение было создано в глубоком стеке, трассировка может быть неполной, если мы используем getTrace() после перехвата в другом месте (например, после ре-броса).

Решение:

Записывать трассировку сразу в момент создания исключения (например, сохранять в свойство) или использовать обработчик исключений set_exception_handler(), где стек ещё не изменился.

Как использовать Xdebug для расширенного стека вызовов?

Расширение Xdebug предоставляет функцию xdebug_print_function_stack() и автоматически выводит стек при ошибках. Оно показывает аргументы, переменные и номера строк.


// php.ini
xdebug.mode=develop
xdebug.show_error_trace=1

// код
function foo($a) {
    bar($a + 1);
}
foo(5);
  

При ошибке (например, неопределённая функция) Xdebug выведет детальный стек прямо на странице.

Проблема:

Xdebug не всегда установлен на production-серверах, и его режим разработки может быть отключён. Перегрузка стека может замедлить выполнение.

Решение:

Использовать условное включение Xdebug через xdebug_start_error_collection() или настройки php.ini только для dev-окружения.

Цели и случаи использования каждого варианта

debug_backtrace()

  • Цель: точный, программируемый анализ стека - например, логирование с контекстом или проверка стека вызовов для автозагрузчика.
  • Случаи: отладка сложных рекурсий, трейсинг вызовов в ORM, проверка порядка исполнения.

debug_print_backtrace()

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

Исключения

  • Цель: стандартизированная обработка ошибок с возможностью передачи стека в виде строки или массива.
  • Случаи: логирование ошибок, отправка стека в мониторинг (например, Sentry), создание кастомных обработчиков.

Xdebug

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

Расширенные примеры работы со стеком вызовов

Пример 1. Рекурсивный обход и ограничение глубины

Пример

function factorial($n) {
    if ($n > 20) {
        // Если рекурсия слишком глубокая, получаем стек для анализа
        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
        echo 'Стек слишком глубокий: ' . count($trace) . ' уровней';
        return null;
    }
    if ($n === 0) return 1;
    return $n * factorial($n - 1);
}
echo factorial(100);
Стек слишком глубокий: 21 уровней

Пример 2. Логирование стека с контекстом в файл

Пример

function logError($message) {
    $trace = debug_backtrace();
    $log = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL;
    foreach ($trace as $idx => $call) {
        $file = $call['file'] ?? 'unknown';
        $line = $call['line'] ?? '?';
        $func = $call['function'] ?? 'main';
        $log .= "  #$idx $func() in $file:$line" . PHP_EOL;
    }
    file_put_contents('/tmp/error.log', $log, FILE_APPEND);
}
function first($a) { second($a * 2); }
function second($b) { logError('Тестовое сообщение'); }
first(10);

Содержимое файла /tmp/error.log:

[2025-03-22 12:34:56] Тестовое сообщение
  #0 logError() in /path/script.php:12
  #1 second() in /path/script.php:21
  #2 first() in /path/script.php:24
  #3 {main} in /path/script.php:25

Пример 3. Использование Exception::getTraceAsString() для форматированного вывода

Пример

function divide($a, $b) {
    if ($b == 0) {
        throw new InvalidArgumentException('Деление на ноль');
    }
    return $a / $b;
}
try {
    divide(5, 0);
} catch (InvalidArgumentException $e) {
    echo "Ошибка: " . $e->getMessage() . PHP_EOL;
    echo "Стек:\n" . $e->getTraceAsString();
}
Ошибка: Деление на ноль
Стек:
#0 /path/script.php(10): divide(5, 0)
#1 {main}

Пример 4. Выборочное получение стека без аргументов для уменьшения объёма

Пример

function process($data) {
    helper($data, 'extra');
}
function helper($input, $flag) {
    // Нас интересует только список функций, не аргументы
    $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
    echo 'Вызвана из: ' . ($trace[1]['function'] ?? 'main') . PHP_EOL;
}
process('test');
Вызвана из: process

Пример 5. Переопределение callable для автоматического трейсинга

Пример

class TraceDecorator {
    private $callback;
    public function __construct(callable $cb) {
        $this->callback = $cb;
    }
    public function __invoke(...$args) {
        $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
        $caller = $trace[1]['function'] ?? 'unknown';
        echo "Вызов из $caller: " . __CLASS__ . PHP_EOL;
        return ($this->callback)(...$args);
    }
}
$sum = new TraceDecorator(function($a, $b) { return $a + $b; });
function calulate($x, $y, $callback) {
    return $callback($x, $y);
}
echo calulate(3, 4, $sum);
Вызов из calulate: TraceDecorator
7

Пример 6. Комбинирование с set_error_handler для детального отчёта

Пример

set_error_handler(function($severity, $message, $file, $line) {
    $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
    $output = "[$severity] $message in $file:$line\n";
    foreach ($trace as $idx => $call) {
        if ($idx === 0) continue; // пропускаем сам обработчик
        $output .= "  #$idx {$call['function']}() at {$call['file']}:{$call['line']}\n";
    }
    file_put_contents('/tmp/php_errors.log', $output, FILE_APPEND);
    return true;
});
trigger_error('Пользовательское уведомление', E_USER_NOTICE);

В логе появится запись с полным стеком, кроме самого обработчика.

Стек вызовов в PHP - comments

En
Php call stack (php)