Разберём следующий кейс.
Разрабатываем мы на своём любимом похапэ некую систему и используем при этом некую библиотеку.
Пускай, например, это будет вот эта поделка для работы с базой данных.
И вот мы вызываем какой-то метод в нашей системе, тот вызывает ещё какой-то и так далее.
И, в конце концов, где-то мы обращаемся к вышеозначенной библиотеке:
$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
А где должен лежать в данном примере 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