Данная статья может показаться весьма сложной для понимания. Пристегните ремни безопасности!
Perl можно по праву назвать языком, в котором одинаково легко манипулировать как данными,
так и кодом программы. Действительно, язык позволяет создавать «на лету» новые подпрограммы
и работать с ними, как с обыкновенными переменными. Благодаря исключительно мощной инструкции
eval и развитому механизму работы с исключениями очень удобно писать трансляторы
с различных мета-языков в Perl-код (например, шаблонизаторы). При этом программист может быть
уверенным: он имеет полный контроль над ходом работы оттранслированного кода, и даже в
случае фатальной ошибки в таком коде он получит возможность ее обработать (например, сформировать
осмысленное сообщение).
Перехват ошибок в Perl
Рассмотрим типичный пример транслятора с некоторого мета-языка шаблонов в Perl-код:
# Превращает 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).
Однако, одна строчка в коде представляет для нас особенный интерес. Вот она:
Директива Perl #line, стоящая на отдельной строке, заставляет интерпретатор «притвориться»,
будто бы следующий за ней код исполняется «в контексте» указанного файла, начиная с
приведенной строки. В нашем случае это файл template.htm, строка 1. Теперь, даже если
в коде $code произойдет что-нибудь серьезное (например, возникнет ошибка), Perl сообщит
о ней как о «проблеме in file template.htm line N», а не как о «проблеме в eval-коде»!
Чтобы окончательно понять, как работает #line, попробуйте запустить следующую Perl-программу:
Как видите, 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(), при любом завершении скрипта —
не важно, ошибочном или легальном.
Проще всего продемонстрировать этот эффект может следующий код:
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-код.
Чтобы не ходить далеко за примером, рассмотрим одно из возможных приложений
префильтра кода: повышение безопасности скрипта. Часто приходися встречать в
программах такие конструкции:
некоторый статический HTML
<input value="<?=htmlspecialchars($var)?>">
некоторый статический HTML
Перед выводом $var ее необходимо «квотить» — превращать кавычки в entity ",
знак < («меньше») — в < и т. д., чтобы не возникло конфликтов с кавычками атрибутов
или HTML-тэгами. Нечего и говорить, что утомительно все время писать одно и то же. А что в
программировании утомительно, то в наибольшей степени подвержено ошибкам и проблемам с безопасностью.
И вот, с помощью префильтра кода мы можем заставить оператор
В примере, приведенном ниже, как раз для такой префильтрации используется библиотека PHP_CodeFilter.
Обратите внимание, что вместо include мы применяем вызов $filter->includeFile(...): так
происходит не просто включение файла шаблона, но и его префильтрация.
Конечно, никто не мешает вам
создать функцию с именем include_html() и записать в ней вызов $filter->includeFile(...), чтобы
в дальнейшем везде писать просто include_html(...). Ибо короче.
<?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, подключаемый в скрипте:
<!-- Шаблон, код которого пре-фильтруется перед запуском. -->
<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-подобного транслятора, допускающего
удобную отладку шаблонных файлов.