Джино: хостинг и веб-сервисы

Система Orphus
Russian version
Добавить на Del.icio.us
English version
Добавить на Digg.com

 dkLab | Конструктор | PHP_CodeFilter: перехват фатальных ошибок PHP? Это возможно 

Карта сайта :: Форум «Лаборатории» :: Проект «Денвер»
Проект «Orphus» :: Куроводство: наблы :: Конструктор


23 апреля 2005 г.
Обсудить на форуме

Чайник 

Данная статья может показаться весьма сложной для понимания.
Пристегните ремни безопасности!

Perl можно по праву назвать языком, в котором одинаково легко манипулировать как данными, так и кодом программы. Действительно, язык позволяет создавать «на лету» новые подпрограммы и работать с ними, как с обыкновенными переменными. Благодаря исключительно мощной инструкции eval и развитому механизму работы с исключениями очень удобно писать трансляторы с различных мета-языков в Perl-код (например, шаблонизаторы). При этом программист может быть уверенным: он имеет полный контроль над ходом работы оттранслированного кода, и даже в случае фатальной ошибки в таком коде он получит возможность ее обработать (например, сформировать осмысленное сообщение).

Перехват ошибок в Perl

Рассмотрим типичный пример транслятора с некоторого мета-языка шаблонов в Perl-код:

Листинг 1
# Превращает HTML-шаблон с "псевдо-PHP вставками" (<? ... ?>) 
# в корректный Perl-код с сериями print-ов. При этом все, что
# расположено вне тэгов, выводится, а то, что внутри, - 
# исполняется как Perl-программа.
sub сompile { 
  my ($html)=@_;
  $c = chr(1);
  $code = "?>$html<?";
  $code =~ s{<\?=}{<?print }sgo;
  $code =~ s{\?>(.*?)<\?}{print(qq$c$1$c);}sgo;
  return $code;
}
# Считываем шаблон из файла.
my $template = "template.htm";
local $/; open(local *F, $template);
my $code = compile(<F>);
# Стартуем код с "подменой" имени текущего файла.
eval("#line 1 \"$template\"\n$code");
# В случае ЛЮБОЙ ошибки в оттранслированном коде (будь то
# ошибка синтаксиса, вызов неопределенной функции - что угодно)
# мы попадем сюда. Работа программы НЕ завершится!
if ($@) die "Handled error: $@";

Вам сейчас нет нужды особенно вникать в приведенный код. Вместо этого вы можете обратиться к девятой набле, где детально рассмотрен предложенный подход по простейшей трансляции шаблонов в Perl (модуль CGI::Embedder).

Однако, одна строчка в коде представляет для нас особенный интерес. Вот она:

Листинг 2
eval("#line 1 \"$template\"\n$code");

Директива Perl #line, стоящая на отдельной строке, заставляет интерпретатор «притвориться», будто бы следующий за ней код исполняется «в контексте» указанного файла, начиная с приведенной строки. В нашем случае это файл template.htm, строка 1. Теперь, даже если в коде $code произойдет что-нибудь серьезное (например, возникнет ошибка), Perl сообщит о ней как о «проблеме in file template.htm line N», а не как о «проблеме в eval-коде»!

Чтобы окончательно понять, как работает #line, попробуйте запустить следующую Perl-программу:

Листинг 3
#line 100 "first.htm"
warn "test1";

Вы увидите следующий результат:

Листинг 4
test1 at first.htm line 100.

Как видите, Perl «уверен», что он выполняет код на строке 100 файла first.htm, в то время как на самом деле запускается команда на строке 2 файла со скриптом!

Но это еще не все преимущества Perl. Инструкция eval работает как универсальный перехватчик исключений (ошибок), по аналогии с блоком try...catch в языках программирования C++ и Java. Иными словами, любое возникшее внутри eval исключение (например, вызов стандартной функции die() или синтаксическая ошибка) приведет не к завершению всей программы, а только лишь к выходу из инструкции eval. Это открывает нам богатые возможности по обработке исключительных ситуаций: действительно, достаточно «обернуть» опасный участок кода в eval и потом проверить переменную $@: в случае проблемы там будет содержаться сообщение об ошибке (или объект-исключение, что то же самое).

Таким образом, Perl позволяет программисту:

  • Контролировать реакцию на любые ошибки в динамически сгенерированном коде.
  • «Подставлять» в контекст исполняемого кода произвольное имя файла и номер строки.

Фатальные ошибки в PHP: E_ERROR

К сожалению, язык PHP (пока?) не может похвастаться всеми этими возможностями. В нем, конечно, имеется встроенная функция eval(), однако она вовсе не такая мощная. Действительно, фатальная ошибка (E_ERROR), возникшая в запущенном через eval() коде, приводит к немедленной (и безусловной) остановке скрипта!

Чайник 

Что такое «фатальная ошибка»? Например, довольно типичный случай — вызов неопределенной функции.

PHP также не поддерживает директиву #line, а поэтому все диагностические сообщения, сгенерированные в eval-коде, будут сообщать: «проблема в eval()'d code on line N». Конечно, если мы пытаемся написать транслятор с языка шаблонов в PHP, это нас ни в коей мере не устроит.

Как же выходят из положения авторы различных транслирующих обработчиков шаблонов (наподобие Smarty), превращающих свои шаблоны в PHP-код с последующим его запуском? А никак не выходят. Тот, кто хоть раз использовал Smarty, знает, что сообщения об ошибках, которые он генерирует, ссылаются на строки в уже оттранслированном PHP-коде, расположенном во временных файлах, а вовсе даже не на строки исходного шаблона. Конечно, это жутко неудобно при отладке сайта.

Впрочем, перехватить нефатальные ошибки (E_WARNING, E_NOTICE и т. д.) возможно с помощью функции set_error_handler(). Однако вызов неопределенной функции (и любая другая ошибка класса E_ERROR) по-прежнему приведет к немедленному завершению работы всей программы и выдаче нелицеприятного сообщения об ошибке в «eval()'d code».

Перехватить за 60 секунд

Со «слабостью» PHP-шной функции eval(), касающейся безусловного завершения скрипта при ошибке E_ERROR, мы никак бороться не можем. Тем не менее, формирование «лицеприятной» ссылки на файл, содержащий ошибку, нам все же по силам!

Мы воспользуемся одним «побочным эффектом», имеющим место во всех версиях PHP 4 и 5. Речь идет о «срабатывании» обработчика выходного потока, установленного по ob_start(), при любом завершении скрипта — не важно, ошибочном или легальном.

Проще всего продемонстрировать этот эффект может следующий код:

Листинг 5
<?php
# Устанавливаем обработчик выходного потока скрипта.
ob_start('ob_handler');
# Печатаем что-нибудь.
echo "Something";
# Вызываем неопределенную функцию внутри eval!
eval('undefinedFunc();');
# Печатаем еще что-то (к этому моменту скрипт уже мертв!).
echo "Other";
# Функция-обработчик просто добавляет некоторый "хвост"
# к тексту, выведенному скриптом ранее.
function ob_handler($text) 
{
    return "$text<hr>Hello from handler!";
}
?>

Вы увидите, что, несмотря на неперехватываемую ошибку класса E_ERROR, обработчик ob_handler() успешно запустился:

Листинг 6
Something
Fatal error: Call to undefined function: undefinedfunc() in e.php(7) : eval()'d code on line 1
--------------------------------------------------------
Hello from handler!

Как же в обработчике определить, завершился скрипт аварийно или же корректно? Существует всего лишь один способ сделать это, но он довольно «грязен». Необходимо разобрать регулярным выражением «хвост» текста, переданного в функцию, и грубо проверить, содержится ли в нем сообщение об ошибке «в eval()'d code». В случае необходимости именно это сообщение и нужно заменить на текст, содержащий «фальшивое» имя файла.

Лирическое отступление 
Дополнительная проблема возникает с режимом display_errors=off, log_errors=on (см. ini_set() или php.ini), при котором PHP не выводит сообщения об ошибках в выходной поток, а сразу записывает их в файл журнала сервера. В отличие от перехвата выходного потока скрипта, обработать акт записи сообщения в журнал сервера невозможно. Более того, если display_errors=off, то мы даже не в состоянии определить, возникла ошибка или нет.

Префильтр кода: модуль PHP_CodeFilter

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

Чайник 

К счастью, ignorance is bliss, и поэтому вы можете не разбираться во всей этой куче «грязных хаков», а просто использовать модуль в своих программах.

Преобразование (трансляцию) файла с некоторого мета-языка шаблонов в PHP-код с последующим его запуском я назвал префильтрацией кода (по аналогии с известным модулем для Perl Filter::Util::Call). С использованием этой идеологии можно «запускать» некоторый шаблон, как будто бы он является обычным PHP-кодом, но проводить в нем предварительную обработку — например, «разворачивать» собственные директивы в «чистый» PHP-код.

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

Листинг 7
некоторый статический HTML
<input value="<?=htmlspecialchars($var)?>">
некоторый статический HTML

Перед выводом $var ее необходимо «квотить» — превращать кавычки в entity &quot;, знак < («меньше») — в &lt; и т. д., чтобы не возникло конфликтов с кавычками атрибутов или HTML-тэгами. Нечего и говорить, что утомительно все время писать одно и то же. А что в программировании утомительно, то в наибольшей степени подвержено ошибкам и проблемам с безопасностью.

И вот, с помощью префильтра кода мы можем заставить оператор

Листинг 8
<?=...?>

автоматически задействовать квотинг при выводе! Для этого достаточно заменять данный оператор «на лету» на следующий код:

Листинг 9
<?=htmlspecialchars(..., ENT_QUOTES)?>

В примере, приведенном ниже, как раз для такой префильтрации используется библиотека PHP_CodeFilter. Обратите внимание, что вместо include мы применяем вызов $filter->includeFile(...): так происходит не просто включение файла шаблона, но и его префильтрация.

Чайник 

Конечно, никто не мешает вам создать функцию с именем include_html() и записать в ней вызов $filter->includeFile(...), чтобы в дальнейшем везде писать просто include_html(...). Ибо короче.

Листинг 10
<?php ## Пример скрипта, использующего префильтр кода для шаблонов.
show_source(__FILE__); echo "<hr>";

require_once "../../lib/config.php";
require_once "PHP/CodeFilter.php";

// Если нужно, подключаем Debug_BacktraceDumper для
// улучшенного вывода отладочных сообщений PHP.
if (true) {
    require_once "Debug/BacktraceDumper.php";
    Debug_BacktraceDumper::set_error_handler();
}

// Некоторая переменная с HTML-разметкой.
$test = "<b>test</b>";

// Создаем объект-фильтр и настраиваем его параметры.
$filter = new PHP_CodeFilter();
$filter->addFilter('filter_phpOutputOperator');

// Запускаем включаемый файл (аналог include), но его 
// код перед выполнением обрабатывается префильтром.
$filter->includeFile('template.php');

// string filter_phpOutputOperator(string $code)
// Префильтр кода: заменяет операторы '< ?= ... ? >' 
// (без пробелов, конечно) на '< ?=htmlspecialchars(...)? >',
// что обеспечивает автоматический квотинг HTML в шаблонах
// и повышает безопасность скриптов.
function filter_phpOutputOperator($code)
{
    return preg_replace(
        '/(<\?=) (.*?) (?: \s*;)* \s* (\? >)/sx', 
        '$1htmlspecialchars($2, ENT_QUOTES)$3', 
        $code
    );
}
?>

Чайник 

В скрипте также демонстрируется применение модуля Debug_BacktraceDumper, позволяющего сделать все предупреждения интерпретатора более «дружелюбными», что сильно упрщает отладку. Конечно, вы можете и не подключать его, если не хотите.

Логика квотинга текста реализована в функции filter_phpOutputOperator(), как раз и заменяющей стандартный оператор PHP <?=...?> на его «квотирущий» вариант.

Для полноты картины давайте посмотрим на шаблон template.php, подключаемый в скрипте:

Листинг 11
<!-- Шаблон, код которого пре-фильтруется перед запуском. -->
<body>

<p><b>Проверка htmlspecialchars-квотинга вывода:</b><br>
<i>(Должна быть выведена HTML-разметка, как после <tt>htmlspecialchars()</tt>.)</i><br>
<?=$test?>

<p><b>Проверка использования неопределенной переменной:</b><br>
<i>(Должна быть ссылка на <tt><?=$__FILE__?></tt>.)</i><br>
<?=$undefined_variable?>

<?$filter->includeFile('template2.php')?>

<p><b>Проверка вызова несуществующей функции (фатальная ошибка):</b><br>
<i>(Должна быть ссылка на <tt><?=$__FILE__?></tt>!)</i>
<?aaa()?>

</body>

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

  • Хотя переменная $test содержит HTML-разметку, при выводе операторами <?=...?> она «проквотилась», и тэги стали «безобидными». Казалось бы, самый обычный PHP-код, а поведение его поменялось. Магия? Нет, префильтрация!
  • Все сообщения об ошибках (даже фатальных) оказались снабжены верными ссылками на файлы, в которых они были сгенерированы, — несмотря на то, что механизм префильтрации использует функцию eval() для запуска результирующего кода!

Чайник 

Библиотека PHP_CodeFilter устроена так, что она корректно обрабатывает даже режим log_errors=on, так что файлы журналов не будут захламляться.

Что дальше?

Метод, реализованный в модуле PHP_CodeFilter, теоретически позволяет в будущем ввести поддержку директивы #line, отсутствующей в PHP. В самом деле, достаточно просто сразу после перехвата ошибки просканировать последний «отфильтрованный» код в поисках строки, упомянутой в сообщении, и найти директиву #line, непосредственно ей предшествующую.

Для оптимизации скорости возможна реализация кэширующих версий префильтров, которые будут транслировать код только при его первом запуске, в дальнейшем «подсовывая» интерпретатору уже готовую PHP-программу. Кэширование кода, например, используется в Smarty и сильно повышает производительность.

Ну и, конечно, простор для фантазии при написании функций-префильтров поистине неограничен! С их помощью вы можете реализовывать любые трансляторы — например, поддержку языков наподобие Smarty, XTemplate и т. д. При этом с точки зрения отладочных сообщений все будет выглядеть так, будто бы код шаблона «запускается» напрямую, минуя фазу трансляции в PHP-код.

Резюме

Рискну предположить, что с применением метода, реализованного в модуле PHP_CodeFilter, процесс написания шаблонных языков выходит на качественно новую ступень, ранее доступную только в языке Perl. Например, появляется возможность создания Smarty-подобного транслятора, допускающего удобную отладку шаблонных файлов.

Все исходные коды скриптов и библиотек этой статьи доступны в директории примеров (zip). Итак:

Другие полезные ссылки:







Дмитрий Котеров, Лаборатория dk. ©1999-2016
GZip
Добавить на Del.icio.us   Добавить на Digg.com   Добавить на reddit.com