Сообщения об ошибках PHP: методы настройки и логирования

Раздел: Ошибки PHP -> Сообщения и логи

Настройка сообщений об ошибках PHP: основные подходы

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

Основное решение: централизованная обработка через set_error_handler() и error_get_last()

Самый гибкий подход - перехват всех ошибок PHP с помощью пользовательской функции и последующая запись в лог. Это позволяет контролировать формат сообщения, уровень детализации и направление вывода (файл, база данных, внешний сервис).


// Устанавливаем обработчик всех ошибок, кроме уведомлений (E_NOTICE) и строгих стандартов (E_STRICT)
set_error_handler(function($severity, $message, $file, $line) {
    // Проверка: не подавлена ли ошибка оператором @
    if (!(error_reporting() & $severity)) {
        return false;
    }
    $logEntry = sprintf("[%s] Уровень: %d, Сообщение: %s, Файл: %s, Строка: %d\n",
        date('Y-m-d H:i:s'), $severity, $message, $file, $line);
    // Запись в общий файл лога
    file_put_contents(__DIR__ . '/error.log', $logEntry, FILE_APPEND);
    return true;
});

Типичные проблемы

  • Фатальные ошибки (E_ERROR, E_PARSE) не перехватываются set_error_handler(). Для их обработки используется register_shutdown_function() в паре с error_get_last().
  • Подавление ошибок @: обработчик может получить уведомление даже при @, если не проверять error_reporting().
  • Рекурсия при записи в лог: если сама запись вызывает ошибку (например, нет прав на запись), нужно предусмотреть fallback.

Как настроить отображение ошибок только в среде разработки?

Используйте директивы php.ini или ini_set() для управления выводом.


// В production отключаем вывод, но логируем
ini_set('display_errors', '0');
ini_set('log_errors', '1');
ini_set('error_log', '/var/log/php_errors.log');
error_reporting(E_ALL); // логируем все

Проблемы

  • После вызова session_start() или отправки заголовков (Header) изменение display_errors через ini_set() работает некорректно.
  • Если файл лога недоступен для записи, ошибки будут потеряны.

Как превратить предупреждения PHP в исключения для более строгого контроля?

Создайте класс, расширяющий ErrorException, и используйте set_error_handler() для генерации исключений.


set_error_handler(function($severity, $message, $file, $line) {
    throw new ErrorException($message, 0, $severity, $file, $line);
});

try {
    // Код, который может вызвать предупреждение
    $value = 1 / 0;
} catch (ErrorException $e) {
    echo 'Перехвачено исключение: ' . $e->getMessage();
}

Проблемы

  • Фатальные ошибки (E_ERROR) не могут быть превращены в исключения.
  • Если внутри try-блока выброшено исключение другого типа, оно не будет перехвачено как ErrorException.

Как использовать error_reporting() для управления уровнями?

Например, чтобы игнорировать уведомления (E_NOTICE), но логировать всё остальное.


error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED & ~E_STRICT);

Проблемы

  • Неверное использование побитовых операторов может привести к пропуску критических ошибок.
  • Значение error_reporting глобально, его трудно менять для отдельных участков кода.

Как записывать ошибки в несколько логов (например, отдельно для разных типов)?

Используйте пользовательский обработчик с разными файлами.


set_error_handler(function($severity, $message, $file, $line) {
    $target = ($severity & (E_WARNING | E_USER_WARNING)) ? 'warnings.log' : 'errors.log';
    file_put_contents(__DIR__ . '/' . $target, date('Y-m-d H:i:s') . " $message\n", FILE_APPEND);
    return true;
});

Проблемы

  • Необходимо обеспечить блокировку файла при одновременной записи (flock).
  • При высокой нагрузке файловые логи становятся узким местом.

Как логировать ошибки с помощью библиотеки Monolog?

Monolog - популярная библиотека для структурированного логирования.


use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$log = new Logger('app');
$log->pushHandler(new StreamHandler(__DIR__ . '/app.log', Logger::ERROR));

// Запись ошибки вручную
$log->error('Ошибка соединения с БД', ['db_host' => 'localhost']);

Проблемы

  • Требуется установка composer и внешняя зависимость.
  • Не перехватывает автоматически стандартные ошибки PHP без дополнительной настройки (нужен ErrorHandler из Symfony или собственный обработчик).

Как обрабатывать фатальные ошибки через register_shutdown_function()?

Этот метод срабатывает после завершения скрипта, даже при фатальной ошибке.


register_shutdown_function(function() {
    $error = error_get_last();
    if ($error !== null && ($error['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR))) {
        file_put_contents('fatal.log', print_r($error, true), FILE_APPEND);
    }
});

Проблемы

  • Нельзя продолжить выполнение скрипта после shutdown.
  • Если shutdown-функция сама завершится ошибкой, она будет проигнорирована.

Вывод: выбор метода зависит от сценария - для простого сайта достаточно настроек php.ini, для фреймворков и сложных приложений предпочтительнее пользовательский обработчик с Monolog.

Расширенные примеры настройки и обработки ошибок PHP

Пример 1. Полный перехват всех типов ошибок (включая фатальные) с подробным логированием

Создадим класс-обработчик, который записывает в JSON-файл данные об ошибке, включая трассировку стека (backtrace).

Пример

class ErrorHandler {
    private $logFile;
    
    public function __construct($logFile) {
        $this->logFile = $logFile;
        set_error_handler([$this, 'handleError']);
        register_shutdown_function([$this, 'handleShutdown']);
    }
    
    public function handleError($severity, $message, $file, $line) {
        if (!(error_reporting() & $severity)) {
            return false;
        }
        $this->writeLog([
            'type' => 'error',
            'severity' => $severity,
            'message' => $message,
            'file' => $file,
            'line' => $line,
            'trace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10)
        ]);
        return true;
    }
    
    public function handleShutdown() {
        $last = error_get_last();
        if ($last !== null && ($last['type'] & (E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR))) {
            $this->writeLog([
                'type' => 'fatal',
                'severity' => $last['type'],
                'message' => $last['message'],
                'file' => $last['file'],
                'line' => $last['line'],
                'trace' => []
            ]);
        }
    }
    
    private function writeLog(array $data) {
        $line = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . PHP_EOL;
        file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
    }
}

// Использование
$handler = new ErrorHandler(__DIR__ . '/errors.json');
// Тестовая ошибка
echo $undefinedVar;
Пример содержимого errors.json:
{
    "type": "error",
    "severity": 8,
    "message": "Undefined variable: undefinedVar",
    "file": "/var/www/test.php",
    "line": 12,
    "trace": [
        {"file": "/var/www/test.php", "line": 12, "function": "handleError", "class": "ErrorHandler"},
        ...
    ]
}
Пример 2. Превращение предупреждений в исключения с дополнительным контекстом

Генерируем исключение, содержащее полную информацию, включая исходный уровень ошибки.

Пример

set_error_handler(function($severity, $message, $file, $line) {
    // Создаем исключение с уровнем как кодом
    throw new ErrorException($message, 0, $severity, $file, $line);
});

function divide($a, $b) {
    if ($b == 0) {
        trigger_error('Деление на ноль', E_USER_WARNING);
        return null;
    }
    return $a / $b;
}

try {
    $result = divide(10, 0);
} catch (ErrorException $e) {
    echo 'Поймано исключение: ' . $e->getMessage() . "\n";
    echo 'Уровень: ' . $e->getSeverity();
} catch (Throwable $t) {
    echo 'Другое исключение: ' . $t->getMessage();
}
Поймано исключение: Деление на ноль
Уровень: 512
Пример 3. Настройка логирования через php.ini для разных окружений

В файле php.ini (или .user.ini) можно задать разные уровни для dev и prod.

Пример

; php.ini для разработки
display_errors = On
error_reporting = E_ALL
log_errors = On
error_log = /var/log/php_dev.log

; php.ini для продакшена
display_errors = Off
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
log_errors = On
error_log = /var/log/php_prod.log
После изменения php.ini необходимо перезапустить веб-сервер (apache, nginx+php-fpm) для применения.
Пример 4. Кастомное логирование с Monolog и интеграция с обработчиком ошибок

Подключаем Monolog, создаём обработчик, который пишет все ошибки в лог, а фатальные - отдельно.

Пример

require 'vendor/autoload.php';

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FingersCrossedHandler;
use Monolog\Handler\TestHandler;

$logger = new Logger('app');
$logger->pushHandler(new StreamHandler(__DIR__ . '/app.log', Logger::DEBUG));
$fatalHandler = new StreamHandler(__DIR__ . '/fatal.log', Logger::CRITICAL);
$logger->pushHandler($fatalHandler);

set_error_handler(function($severity, $message, $file, $line) use ($logger) {
    $level = Logger::ERROR;
    if ($severity & E_WARNING) $level = Logger::WARNING;
    elseif ($severity & E_NOTICE) $level = Logger::NOTICE;
    $logger->addRecord($level, $message, ['file' => $file, 'line' => $line]);
    return true;
});

register_shutdown_function(function() use ($logger, $fatalHandler) {
    $last = error_get_last();
    if ($last && ($last['type'] & (E_ERROR | E_PARSE))) {
        $logger->critical($last['message'], ['file' => $last['file'], 'line' => $last['line']]);
    }
});

// Тестовый вызов
$arr = [];
echo $arr[0]; // Notice: Undefined offset
В app.log появится строка:
[2025-03-18T12:00:00.000000+00:00] app.NOTICE: Undefined offset: 0 {"file":"test.php","line":27} []
В fatal.log ничего не добавится, так как это не фатал.
Пример 5. Работа с пользовательскими сообщениями об ошибках (trigger_error)

Создадим функцию, которая генерирует предупреждение и логирует его вместе с контекстом вызова.

Пример

function validateEmail($email) {
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        trigger_error('Некорректный email: ' . $email, E_USER_WARNING);
        return false;
    }
    return true;
}

set_error_handler(function($severity, $message, $file, $line) {
    $log = date('Y-m-d H:i:s') . " [$severity] $message in $file:$line\n";
    file_put_contents('user_errors.log', $log, FILE_APPEND);
    return true;
});

echo validateEmail('invalid');
Вывод: false
Содержимое user_errors.log:
2025-03-18 12:05:00 [512] Некорректный email: invalid in /var/www/test.php:6
Пример 6. Использование error_get_last() для проверки произошедших ошибок

Полезно, если нужно узнать, была ли ошибка во время выполнения, не прерывая скрипт.

Пример

$old = error_reporting(E_ALL & ~E_NOTICE);
$nosuch = $undefinedArray['key']; // не вызовет E_NOTICE, но PHP всё равно запишет в error_get_last?
error_reporting($old);

$lastError = error_get_last();
if ($lastError) {
    echo 'Последняя ошибка: ' . $lastError['message'];
    error_clear_last(); // очищаем, чтобы не мешала
}
Если E_NOTICE подавлен через error_reporting, PHP может не записать её в error_get_last. Поэтому такой способ ненадёжен. Лучше использовать кастомный обработчик.

Сообщение об ошибке PHP - comments

En
Php error message (php)