Синтаксическая проверка PHP-файла на ошибки без его выполнения
Порой, даже самые простые задачи рядового программиста на PHP могут требуют к себе внимания. В этот раз речь пойдёт о синтаксической проверке php-файла перед его подключением.
Пример из жизни — динамически подключаемые php-файлы через функцию include(), если файл содержит ошибку — получим 500 ошибку сервера, которая весьма сурова для нашего кода. До версии PHP 5.0.4 можно было обойтись функцией php_check_syntax(), чтобы проверить файл перед его подключением, но в последующих версиях её нет, поэтому, казалось бы, остаётся только один простой способ проверки через командную строку:
php -l имя_файла
В php-реализации получается примерно так:
function php_check_syntax($file, &$error) {
// анализируем файл
exec("php -l ".$file, $error, $code);
// ошибок нет
if ($code == 0) {
return true;
}
// ошибки есть
return false;
}
Но, этот способ практически бесполезен для отечественных хостингов, т.к. функции exec() и shell_exec() возглавляют небезопасный список php-функций и их просто отключают. Так поступает masterhost и многие другие.
Найти другой функционал синтаксической проверки файлов на базе стандартных решений — не получилось, — поэтому будет магия! А поможет нам победить непослушный код — функция token_get_all() и функция eval().
Функция token_get_all() предназначена для сбора информации о php-токенах при помощи лексического анализатора Zend. Под токенами подразумеваются элементы участвующие в синтаксической структуре php-файла, это кавычки, фигурные скобки, просто скобки, точки с запятой и т.д. Мы проверим парность этих символов, чтобы открытые скобки оставались закрытыми… Это первый этап нашей проверки (в цикле foreach). Он нужен для корректного запуска второго этапа.
Второй этап проверки — та самая магия! Небольшой хак с использованием функции eval(). Самое главное соблюсти очень важное условие — код из файла не должен выполниться, почему объяснять думаю не надо. Вот такая нехитрая конструкция позволит удовлетворить наши запросы:
ob_start();
$res = eval('if (0) {?>'.$code.'<?php }; return true;');
$error_text = ob_get_clean();
Нужно просто вставить код в разрыв условия, которое никогда не будет выполнено. Фигурные скобки и другие токены php-кода были проверены на первом этапе, поэтому проблем быть не должно.
Теперь о некоторой неявной проблеме, которая может возникнут в недрах браузера… при исполнении синтаксически неверного кода, внутри функции eval() возникает ошибка 500, её можно «поймать» в консоли firebug (расширение для браузера FireFox). Всё дело в заголовках. Проблема возникает не всегда и не везде, обязательным условием её появления служит выключенный показ ошибок:
display_errors = off
Чтобы избежать ошибочных заголовков следует устанавливать заголовок вручную:
header('HTTP/1.0 200 OK');
Законченный код функции синтаксической проверки php-файла будет таким:
/**
* Syntax check PHP file
*
* @param string file path
*
* @return boolean checking result
*/
function syntax_check_php_file ($file) {
// получим содержимое проверяемого файла
@$code = file_get_contents($file);
// файл не найден
if ($code === false) {
throw new Exception('File '.$file.' does not exist');
}
// первый этап проверки
$braces = 0;
$inString = 0;
foreach ( token_get_all($code) as $token ) {
if ( is_array($token) ) {
switch ($token[0]) {
case T_CURLY_OPEN:
case T_DOLLAR_OPEN_CURLY_BRACES:
case T_START_HEREDOC: ++$inString; break;
case T_END_HEREDOC: --$inString; break;
}
}
else if ($inString & 1) {
switch ($token) {
case '`':
case '"': --$inString; break;
}
}
else {
switch ($token) {
case '`':
case '"': ++$inString; break;
case '{': ++$braces; break;
case '}':
if ($inString) {
--$inString;
}
else {
--$braces;
if ($braces < 0) {
throw new Exception('Braces problem!');
}
}
break;
}
}
}
// расхождение в открывающих-закрывающих фигурных скобках
if ($braces) {
throw new Exception('Braces problem!');
}
$res = false;
// второй этап проверки
ob_start();
$res = eval('if (0) {?>'.$code.'<?php }; return true;');
$error_text = ob_get_clean();
// устранение ошибки 500 в функции eval(), при директиве display_errors = off;
header('HTTP/1.0 200 OK');
if (!$res) {
throw new Exception($error_text);
}
return true;
}
Запустить синтаксическую проверку php-кода с помощью нашей функции можно так:
try {
if ( syntax_check_php_file($file_path) ) {
include $file_path;
}
}
catch (Exception $e) {
// error message
}