Синтаксическая проверка 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 }