PHP: обрезаем backtrace у exception

Разберём следующий кейс.

Разрабатываем мы на своём любимом похапэ некую систему и используем при этом некую библиотеку.
Пускай, например, это будет вот эта поделка для работы с базой данных.

И вот мы вызываем какой-то метод в нашей системе, тот вызывает ещё какой-то и так далее.
И, в конце концов, где-то мы обращаемся к вышеозначенной библиотеке:

$db->query('SELECT * FROM `test` WHERE `id`=?i')->el();

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

И вот, вызванный нами, метод query() начинает обращаться к каким-то другим классам библиотеки, те к следующим и так далее, всё как обычно.
В какой-то момент доходит дело до того, чтобы данные в запрос на нужные места вставлять и тут выясняется, что данных нет никаких. Ну и, ясное дело, всё валится с исключением:

go\DB\Exceptions\DataNotEnough: Data elements (0) less than the placeholders in /test/my/db/Helpers/Templater.php on line 126
 
Call Stack:
    1. {main}() /test/my/index.php:0
    2. callMyClass() /test/my/index.php:14
    3. MyClass->method() /test/my/index.php:11
    4. MyClass->selectDB() /test/my/MyClass.php:7
    5. go\DB\DB->query() /test/my/MyClass.php:20
    6. go\DB\DB->makeQuery() /test/my/db/DB.php:92
    7. go\DB\Helpers\Templater->parse() /test/my/db/DB.php:312
    8. preg_replace_callback() /test/my/db/Helpers/Templater.php:57
    9. go\DB\Helpers\Templater->placeholderClb() /test/my/db/Helpers/Templater.php:57

Смотрим мы печальными глазами на этот вывод и читаем сообщение, ага, забыли данных накидать.
А вот где именно это случилось, хрен поймёшь.
Исключение указывает на точку, где его выбросили, то есть глубоко в недрах библиотеки и делать нам там нечего.
Нам нужна та точка, где мы в эту библиотеку с невалидными данными вошли.

К счастью, здесь нам ещё Call Stack выводится.
Распарсив его взглядом, мы находим искомое: MyClass.php, строка #20.

Но каждый раз парсить взглядом дело утомительное. Да и не при всех настройках этот стек так ровно и красиво выводится.

Exception::$file и Exception::$line

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

Берём документацию на класс Exception.
Оказывается, нам доступны для записи свойства $file и $line. И они даже работают:

class MyException extends \Exception
{
    public function __construct($message)
    {
        $this->file = 'Kakaetohrenvmestoimenifaila';
        $this->line = 123456789;
        parent::__construct($message);
    }
}
 
throw new MyException('Error!');
Uncaught exception 'MyException' with message 'Error!' in Kakaetohrenvmestoimenifaila:123456789

Теперь нам нужно выяснить, что нам в них можно записать.

Exception::getTrace()

Объект exception также содержит в себе стек.
Перезаписать его, правда, также лихо, как и $file, никто не даст, но можно хотябы прочитать.

Формат стека аналогичен описанному для debug_backtrace.

Теперь нам нужно просто найти в нём то, что нам требуется.

Резка по namespace или class

Самое простое, если библиотека расположена в своём пространстве имён (go\DB в примере).
Просто ищем обращения к классам из этого неймспейса:

class MyException extends \Exception
{
    public function __construct($message) 
    {
        $this->truncateByNamespace('my\ns');
        parent::__construct($message);        
    }
 
    public function truncateByNamespace($namespace) {
        $prefix = $namespace.'\\';
        $trace = array();
        foreach (\array_reverse($this->getTrace()) as $item) {
            $trace[] = $item;
            if ((!empty($item['class'])) && (\strpos($item['class'], $prefix) === 0)) {
                $this->newtrace = \array_reverse($trace);
                $this->file = isset($item['file']) ? $item['file'] : null;
                $this->line = isset($item['line']) ? $item['line'] : null;
                return;
            }
        }
    }
}
  • Ищем первый элемент стека с полем class, принадлежащим заданному пространству
  • Не забываем, что не во всех элементах стека есть это поле
  • getTrace() возвращает перевёрнутый стек. 0-й индекс — последний вызов.
  • Искать с конца стека выход из нашего namespace нельзя. В примере, обломимся на preg_replace_callback().

Обана:

go\DB\Exceptions\DataNotEnough: Data elements (0) less than the placeholders in /test/my/MyClass.php on line 20
 
Call Stack:
    0.0004     123016   1. {main}() /test/my/index.php:0
    0.0008     126332   2. callMyClass() /test/my/index.php:24
    0.0008     126400   3. MyClass->method() /test/my/index.php:21
    0.0008     126460   4. MyClass->selectDB() /test/my/MyClass.php:7
    0.0041     164488   5. go\DB\DB->query() /test/my/MyClass.php:20
    0.0041     164624   6. go\DB\DB->makeQuery() /test/my/db/DB.php:92
    0.0054     177396   7. go\DB\Helpers\Templater->parse() /test/my/db/DB.php:312
    0.0055     177792   8. preg_replace_callback() /test/my/db/Helpers/Templater.php:57
    0.0055     178724   9. go\DB\Helpers\Templater->placeholderClb() /test/my/db/Helpers/Templater.php:57

Теперь мы сразу видим, где ошибка.
Стек, правда, остался старым, но это и к лучшему — если требуется видна вся картина.

Если не пространство имён, можно искать по имени класса или по префиксу в нём.

Резка по файлу или каталогу

Другой, допотопный, вариант — библиотека лежит в отдельном файле или каталоге.

Точно также обходим стек и проверяем $item['file'] за следующими исключениями:

  • В случае с классом, нужно брать тот элемент, в котором этот класс найден, в случае с файлом же — предыдущий элемент.
  • Если ошибка происходит в первом же методе библиотеки — в стеке файла не будет, следует смотреть значение $this->file.

Exception::__toString()

Есть ещё метод __toString() с помощью которого можно делать разные и не всегда понятные вещи.
Но это на любителя.

Нюансы

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

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

Реализация

Пара библиотечек:

  • axy\backtrace: обработка стека вызовов.
  • axy\errors: небольшая помощь в определении своих исключений.

PHP 5.4+ only

4 комментария »

  • А где должен лежать в данном примере MyException, раз тут в конструкторе зашивается ‘my\ns’? В \My\Exception ?

    kostyl, 22.01.2014, 23:54

  • Ну, раз его бросает библиотека, лежащая в my\ns, то скорее всего где-то в её недрах.
    Я предпочитаю исключения в my\ns\errors держать.

    vasa_c, 23.01.2014, 12:09

  • при желании, оказывается, можно и трейс переписать — http://3v4l.org/FTBog :)

    timur, 27.01.2014, 15:17

  • timur, ну это уже для совсем отмороженных :)

    vasa_c, 27.01.2014, 15:39

Leave a comment