Avatar ·

Как перехватить фатальную ошибку (E_ERROR) в PHP?

Всем привет! Я новичок в PHP и уже несколько дней бьюсь над проблемой. У меня есть скрипт, который обрабатывает загрузку файлов от пользователей. Иногда, если файл слишком большой или повреждён, PHP просто "падает" с фатальной ошибкой (E_ERROR) — например, при попытке выделить память или при вызове несуществующего метода у объекта. Я уже настроил `set_error_handler()`, но он не ловит фатальные ошибки. Вот пример кода: ```php nonExistentMethod(); // Здесь будет E_ERROR } processData($userInput); ?> ``` В этом случае скрипт просто выводит "Fatal error: Call to undefined method..." и завершается. Мне нужно, чтобы при такой ошибке скрипт не падал, а записывал ошибку в лог и продолжал выполнение (или хотя бы показывал пользователю вежливое сообщение). Я слышал про `register_shutdown_function()`, но не понимаю, как его правильно применить. В документации написано, что он вызывается при завершении скрипта, но как в нём отличить фатальную ошибку от обычного завершения? И можно ли после фатальной ошибки продолжить выполнение кода? Буду благодарен за подробное объяснение с примерами. Версия PHP 7.4+. <<>>

Проблема с перехватом E_ERROR в PHP

Вы правы: стандартный set_error_handler() не перехватывает фатальные ошибки (E_ERROR, E_PARSE, E_CORE_ERROR и т.д.), потому что они происходят на уровне выполнения, когда скрипт уже не может безопасно продолжать работу. Однако PHP предоставляет два основных инструмента для работы с такими ошибками: register_shutdown_function() и исключения Throwable (в PHP 7+).

Решение 1: Использование register_shutdown_function()

Этот метод регистрирует функцию, которая вызывается при завершении скрипта — как при успешном выполнении, так и при фатальной ошибке. Чтобы отличить фатальную ошибку от нормального завершения, используйте error_get_last():

register_shutdown_function(function() {
    $error = error_get_last();
    if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        // Фатальная ошибка произошла
        error_log("Фатальная ошибка: {$error['message']} в файле {$error['file']} на строке {$error['line']}");
        echo "Произошла критическая ошибка. Пожалуйста, попробуйте позже.";
        // Здесь можно отправить email администратору, записать в БД и т.д.
    }
});

// Ваш проблемный код
function processData($data) {
    $obj = new SomeClass();
    $obj->nonExistentMethod(); // Вызовет E_ERROR
}

processData($userInput);

Важно: После фатальной ошибки скрипт всё равно завершится — вы не можете продолжить выполнение после E_ERROR. Но shutdown-функция даёт возможность корректно обработать ситуацию (залогировать, вывести сообщение пользователю).

Решение 2: Использование try-catch с Throwable (PHP 7+)

Начиная с PHP 7, большинство фатальных ошибок стали "исключаемыми" — они выбрасывают исключения, реализующие интерфейс Throwable. Например, вызов несуществующего метода теперь выбрасывает Error, который можно поймать:

try {
    $obj = new SomeClass();
    $obj->nonExistentMethod(); // Выбросит Error
} catch (\Throwable $e) {
    // Ловим любые ошибки и исключения
    error_log("Поймана ошибка: " . $e->getMessage());
    echo "Что-то пошло не так. Мы уже работаем над этим.";
}

// Код продолжает выполняться после catch
echo "Этот текст будет выведен!";

Это работает для большинства ситуаций (вызов несуществующего метода, деление на ноль в некоторых контекстах и т.д.), но не для всех. Например, ошибки памяти (Allowed memory size exhausted) всё ещё могут быть не перехвачены.

Решение 3: Комбинированный подход (рекомендуется)

Лучшая практика — объединить оба метода:

// 1. Преобразуем предупреждения и заметки в исключения
set_error_handler(function($severity, $message, $file, $line) {
    throw new ErrorException($message, 0, $severity, $file, $line);
});

// 2. Обрабатываем фатальные ошибки при завершении
register_shutdown_function(function() {
    $error = error_get_last();
    if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        error_log("Неотлавливаемая ошибка: {$error['message']}");
        echo "Критическая ошибка. Пожалуйста, свяжитесь с администратором.";
    }
});

// 3. Основной код с try-catch
try {
    // Ваш потенциально опасный код
    processData($userInput);
} catch (\Throwable $e) {
    // Ловим всё, что можно поймать
    error_log("Обработанная ошибка: " . $e->getMessage());
    echo "Произошла ошибка: " . htmlspecialchars($e->getMessage());
}

Важные замечания

  • В production-режиме отключите вывод ошибок (ini_set('display_errors', 0)) и включите логирование (ini_set('log_errors', 1)).
  • Фатальные ошибки, такие как E_PARSE (синтаксические ошибки), не могут быть перехвачены ни одним из методов — скрипт не выполняется вообще. Проверяйте код через линтеры перед деплоем.
  • Для ошибок памяти используйте memory_limit и мониторинг, а не попытки их перехватить — после такой ошибки состояние скрипта неопределённо.
<<>>

Альтернативный подход: Использование глобального обработчика исключений

Вместо комбинирования нескольких функций вы можете использовать set_exception_handler() для обработки всех неперехваченных исключений (включая Error в PHP 7+). Это более чистый способ для приложений, где вы хотите централизованно обрабатывать все ошибки:

// Глобальный обработчик неперехваченных исключений
set_exception_handler(function(\Throwable $e) {
    error_log("Необработанное исключение: " . $e->getMessage() . " в " . $e->getFile() . ":" . $e->getLine());
    http_response_code(500);
    echo "Внутренняя ошибка сервера. Пожалуйста, попробуйте позже.";
});

// Этот обработчик ловит:
// - Все обычные исключения (Exception)
// - Фатальные ошибки, которые стали исключениями (Error, TypeError, ParseError и т.д.)
// - Но НЕ ловит: ошибки памяти, таймауты выполнения

// Пример кода с ошибкой:
function riskyOperation() {
    $arr = [];
    $arr->nonExistentMethod(); // Выбросит Error
}

riskyOperation(); // Будет перехвачено set_exception_handler
echo "Этот код не выполнится, если ошибка не будет поймана в try-catch.";

Плюсы: Простота, централизованная обработка, работает с большинством современных фатальных ошибок.
Минусы: Не ловит ошибки, которые не стали исключениями (например, некоторые ошибки памяти), и скрипт всё равно завершается после обработчика.

Для максимальной надёжности комбинируйте set_exception_handler с register_shutdown_function, как описано в первом ответе.