Порой, даже самые простые задачи рядового программиста на 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
}