Сообщения об ошибках 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
Создадим класс-обработчик, который записывает в 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"},
...
]
}
Генерируем исключение, содержащее полную информацию, включая исходный уровень ошибки.
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
В файле 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) для применения.
Подключаем 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 ничего не добавится, так как это не фатал.
Создадим функцию, которая генерирует предупреждение и логирует его вместе с контекстом вызова.
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
Полезно, если нужно узнать, была ли ошибка во время выполнения, не прерывая скрипт.
$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. Поэтому такой способ ненадёжен. Лучше использовать кастомный обработчик.