Обработка исключений и ошибок в PHP

Недавно мне попался проект, где добрая часть кода составляла обработка исключений. То есть создавалось впечатление, что автор хотел предусмотреть чуть ли не все возможные варианты. Хуже всего было то, что логика приложения была сама по себе завязана на эти самые исключения. То есть, вместо элементарной проверки входных данных кидалось исключение.

Пользоваться таким кодом очень сложно, поскольку приходится заключать каждую функцию/метод в блок try/catch, хотя было бы достаточно, если функция вернёт обычный false.

Мне кажется, что основная причина такого использования исключений в том, что у php-программистов нет понимания зачем исключения вообще нужны, а также в том, что их часто путают с обработкой ошибок.

Для новичков, которые не имеют опыта полноценного программирования (например на Pascal/Delphi) исключения — это вообще тёмный лес... Попробуем разобраться.

Что такое «исключения»?

Вы удивитесь, но даже в официальном руководстве PHP нет ответа на этот вопрос. Там как бы сразу предполагается, что программисты уже знают что это такое или понимают это из семантики самого слова «исключения».

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

Считывается файл в какую-то переменную. Перед этим для этого выделяется блок памяти. В процессе чтения файла, возникает исключительная ситуация, например файл удалили или он заблокирован на чтение. Данная программа аварийно завершает свою работу, а значит участок памяти не был освобождён.

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

На Паскале это может выглядеть так:

try
    TreeView.LoadFromStream(stream);
except
    showmessage('Ошибка загрузки потока. Что-то с системой...');
end;

В данном примере блок try — это т.н. защищённый блок кода — если в нём возникнет исключение, то управление будет передано в блок except — это обработчик исключения.

В другом примере используется блок finally.

FIniFile := TRegIniFile.Create(s);

try
    FIniFile.ReadString('', 'data','');
    ...
finally
    FIniFile.Free;
end;

Здесь переменная FIniFile считывает некие данные, но если они ошибочные, то возникает исключение. Но здесь главная задача — это корректно освободить память и это происходит в блоке finally, который гарантированно сработает как при возникновении исключения, так и без него.

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

Исключения в PHP

В PHP исключения реализуются похожим образом.

try {
    ... защищённый участок кода ...
} catch (Exception $e) {
    ... код, если возникла исключительная ситуация ...
}

Или вариант с finally:

try {
    ... защищённый участок кода ...
} catch (Exception $e) {
    ... код, если возникла исключительная ситуация ...
} finally {
    ... код с гарантированный выполнением ...
}

При этом можно использовать как try/catch/finally, так и try/finally.

Казалось бы всё просто, но как обычно в PHP есть нюансы.

«Спотыкачка» для новичков

Рассмотрим простой пример. Есть функция для деления двух чисел.

function my1($a, $b)
{
    try {
        $r = $a / $b;
    } catch (Exception $e) {
        $r = 'Нельзя делить на ноль';
    }

    return $r;
}

echo my1(1, 0);

Мы ожидаем, что выполнение этого выведет «Нельзя делить на ноль». Однако на самом деле мы получаем сообщение от PHP «Warning: Division by zero». То есть блок catch не сработал.

Почему так происходит?

Не все ошибки — исключения

В PHP все ошибки имеют тип в виде предопределённых констант: E_ERROR, E_WARNING, E_NOTICE и т.д.

Так вот ошибки E_WARNING не являются исключительной ситуацией, поскольку программа может продолжить своё выполнение дальше. Если после echo my1(1, 0); мы разместим ещё какой-то код, то PHP его выполнит.

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

То есть когда мы пытаемся поделить число на ноль, то это всего лишь некорректная обработка входящих данных, которая не может привести к полному краху.

Обработка данных

PHP в первую очередь это процедурное программирование, где функции самостоятельно умеют обрабатывать входящие данные. Хороший пример это функции для текста, например strpos(), которая возвращает не только позицию вхождения, но и FALSE, если его нет.

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

Наш пример можно было бы переписать так:

function my2($a, $b)
{
    if ($b == 0)
        return false; // 'Нельзя делить на ноль';
    else
        return $a / $b;
}

if ($r = my2(1, 0) !== FALSE)
    echo $r;
else
    echo 'Ошибка';

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

Как не нужно делать

Плохая практика — вместо обработки входящих данных, генерировать исключение. В PHP можно сгенерировать исключение вручную с помощью throw.

function my3($a, $b)
{
    if ($b === 0)
        throw new Exception('Нельзя делить на ноль');
    else
        return $a / $b;
}

try {
    echo my3(1, 0);
} catch (Exception $e) {
    echo $e->getMessage();
}

Здесь, если $b равен нулю, генерируется исключение. Проблема здесь в том, что использовать такую функцию приходится только в блоке try/catch, что не только усложнят код, но и смешивает логику приложения с исключениями. Причём этот пример достаточно простой: если копнуть глубже, то исключения могут быть разных типов (классов) и их «ветвистая» обработка безумно усложняет код.

Правильны подход

Функция должна сама обрабатывать свои исключения.

function my4($a, $b)
{
    try {
        if ($b === 0) 
            throw new Exception('Нельзя делить на ноль');
        else
            return $a / $b;

    } catch (Exception $e) {
        return $e->getMessage();
    }
}

echo my4(1, 0);

Если $b равен нулю, генерируется исключение, которое тут же ловится в блоке catch. Это как бы совмещённый подход, когда функция и проверяет данные, и генерирует исключения. Поскольку можно отследить разные ситуации, то можно сгенерировать и разные исключения. Например если в функцию будет передано не число, а строка или другой нечисловой тип данных, то возвратить либо сообщение об ошибке, либо просто 0.

Как отследить WARNING

Важно понимать, что в PHP ошибки и исключения — разные вещи и работают они по разному. По большому счёту можно вообще обойтись без исключений, поскольку функции PHP сами по себе неплохо справляются с нештатными ситуациями.

Однако в PHP есть механизм по «превращению» ошибок в исключения. Для этого следует переопределить стандартный обработчик ошибок с помощью функции set_error_handler().

function exception_error_handler($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) {
        // Этот код ошибки не входит в error_reporting
        return;
    }
    throw new ErrorException($message, 0, $severity, $file, $line);
}

set_error_handler('exception_error_handler');

После этого мы можем вызвать самый первый пример с my1()

echo my1(1, 0);

и получить как и ожидаем: «Нельзя делить на ноль».

Выводы

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

Если функция генерирует исключение, то лучше всего, если она сама же его и обработает.

Плохо, если функция только генерирует исключение в надежде, что его «кто-то» «где-то» словит. И совсем плохо, если логика функции/приложения построена на исключениях. Такой подход негативно сказывается как на логике работы, так и на общем объёме кода. Разобраться в нём будет проблематично.

Оставьте комментарий!

Комментарий будет опубликован после проверки. Вы соглашаетесь с правилами сайта.

(обязательно)