Обработка предупреждений PHP: настройка отображения и логирования

Раздел: Программирование на PHP -> Ошибки и предупреждения PHP

Основные способы управления предупреждениями PHP

Как надёжно скрыть или записать предупреждения на продакшене?

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

; php.ini для продакшена
display_errors = Off
log_errors = On
error_log = /var/log/php_errors.log
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT

В среде разработки, напротив, ошибки выводятся на экран:

; php.ini для разработки
display_errors = On
error_reporting = E_ALL

Пошагово: Найдите файл php.ini командой php --ini. Измените нужные строки и перезапустите веб-сервер. Если нет доступа к php.ini, используйте .htaccess (для Apache) или .user.ini (для Nginx/FPM).

Типичные ошибки:

  • Ошибка 500 после изменений в php.ini - проверьте синтаксис, опечатки в директивах.
  • Лог ошибок не создаётся - убедитесь, что папка для логов существует и доступна для записи пользователю веб-сервера.
  • Включение display_errors на продакшене раскрывает информацию о путях и структуре приложения - злоумышленники могут использовать это.

Цель: единая настройка для всего приложения, минимальное влияние на производительность, безопасность и удобство отладки.

Как временно подавить предупреждение для одной операции?

Используйте оператор @ перед выражением. Он подавляет все сообщения об ошибках, возникшие в данном выражении.

$result = @file_get_contents('http://example.com');
if ($result === false) {
    // Обработка ошибки без вывода предупреждения
}

Проблемы:

  • Оператор @ подавляет и фатальные ошибки (например, синтаксические), что может скрыть реальные проблемы.
  • Снижает скорость работы - PHP при этом всё равно создаёт обработчик.
  • Не даёт возможности логирования - ошибка теряется.

Используется редко, в основном для функций, где ошибка ожидаема, например, при проверке существования файла через @file_exists.

Как перехватить все предупреждения и записать их в свой лог?

Установите пользовательский обработчик через set_error_handler(). Он будет вызываться при каждом предупреждении, кроме фатальных ошибок.

function customErrorHandler($severity, $message, $file, $line) {
    error_log("[Пользовательское предупреждение] $message в $file:$line", 3, '/tmp/my_errors.log');
    // Можно также вывести сообщение в браузер, если нужно
    if (ini_get('display_errors')) {
        echo "
Ошибка: $message
"; } } set_error_handler('customErrorHandler'); // Теперь любое предупреждение попадёт в наш лог trigger_error('Это пользовательское предупреждение', E_USER_WARNING);

Проблемы:

  • Не обрабатывает фатальные ошибки (E_ERROR, E_PARSE, E_CORE_ERROR) - для них нужен register_shutdown_function() или set_exception_handler().
  • Если внутри обработчика возникнет ошибка, она может привести к циклическому вызову.

Подходит для централизованного логирования в фреймворках или при необходимости отправки ошибок в мониторинг.

Как изменить уровень отображаемых ошибок в конкретной части скрипта?

Функция error_reporting() позволяет временно изменить уровень ошибок для текущего контекста.

// Выключить предупреждения о неопределённых переменных
error_reporting(E_ALL & ~E_WARNING & ~E_NOTICE);
echo $undefined_var; // Не выведет предупреждения
error_reporting(E_ALL); // Восстановить полный уровень

Ошибки:

  • Легко забыть восстановить предыдущий уровень, что приведёт к потере других сообщений.
  • Не влияет на настройки display_errors - если вывод отключён, ошибки всё равно не покажутся.

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

Как избежать предупреждений при работе с пользовательскими данными?

Используйте функции валидации и фильтрации, например filter_input() или проверку существования ключа через isset().

// Вместо:
$username = $_GET['user']; // Предупреждение, если ключа нет

// Используйте:
$username = filter_input(INPUT_GET, 'user', FILTER_SANITIZE_STRING);
// Или с проверкой:
$username = isset($_GET['user']) ? $_GET['user'] : null;

Проблемы:

  • Неудачная фильтрация может исказить данные (например, удалить нужные символы).
  • Не защищает от всех типов предупреждений (например, деление на ноль).

Предпочтительный подход - проверять наличие ключей перед обращением и использовать валидацию вместо подавления.

Как настроить запись ошибок PHP в системный журнал?

Директива error_log = syslog в php.ini или вызов openlog() и syslog() в коде.

openlog('myApp', LOG_PID | LOG_PERROR, LOG_LOCAL0);
syslog(LOG_WARNING, 'Необычная ситуация в модуле X');
closelog();

Проблемы:

  • Требуются права на запись в системный журнал (обычно есть у демона).
  • Формат сообщений может отличаться в разных ОС.

Полезно для консольных скриптов или приложений, работающих в окружении с централизованным сбором логов (rsyslog, syslog-ng).

Расширенные примеры управления предупреждениями

Пример 1: Комбинирование display_errors и log_errors

Сценарий: на локальной машине нужно видеть все ошибки, но на тестовом сервере только логировать их.

Пример
if (php_sapi_name() === 'cli') {
    ini_set('display_errors', '1');
    error_reporting(E_ALL);
} else {
    ini_set('display_errors', '0');
    ini_set('log_errors', '1');
    ini_set('error_log', '/tmp/php_errors.log');
    error_reporting(E_ALL & ~E_DEPRECATED);
}

// Провокация ошибки
trigger_error('Тестовое предупреждение', E_USER_WARNING);
Результат в браузере: ничего не выведено (если не CLI). В журнале /tmp/php_errors.log появится запись с временем и сообщением.

Пример 2: Обработчик с перехватом фатальных ошибок

Стандартный set_error_handler не ловит E_ERROR. Используем register_shutdown_function для получения последней ошибки.

Пример
function shutdownHandler() {
    $error = error_get_last();
    if ($error && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        error_log("Фатальная ошибка: {$error['message']} в {$error['file']}:{$error['line']}");
        http_response_code(500);
        echo "Критическая ошибка, администратор уведомлён.";
    }
}

function warningHandler($severity, $message, $file, $line) {
    error_log("Предупреждение: $message в $file:$line");
}

set_error_handler('warningHandler');
register_shutdown_function('shutdownHandler');

// Фатальная ошибка:
echo $undefined->method();
Вывод: "Критическая ошибка, администратор уведомлён." В лог попадёт сообщение о фатальной ошибке.

Пример 3: Класс для централизованного логирования с Monolog

Использование популярной библиотеки для гибкого логирования. Устанавливается через Composer: composer require monolog/monolog.

Пример
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\ErrorHandler;

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

// Автоматически перехватывает PHP ошибки и исключения
ErrorHandler::register($log);

// Теперь все предупреждения уходят в лог
trigger_error('Ошибка в модуле оплаты', E_USER_WARNING);

// Можно ещё добавить ручное логирование
$log->warning('Устаревший метод вызван', ['class' => 'OldClass']);
В файле /var/log/app.log появится запись формата Monolog: [2023-10-15 10:00:00] app.WARNING: Ошибка в модуле оплаты [] []

Пример 4: Сравнение производительности при использовании @ и error_reporting

Оператор @ замедляет выполнение, так как PHP включает механизм подавления. Error_reporting работает быстрее.

Пример
$start = microtime(true);
for ($i = 0; $i < 10000; $i++) {
    @file_get_contents('/tmp/nonexistent_' . $i);
}
echo "Время с @: " . (microtime(true) - $start) . " сек\n";

error_reporting(E_ALL & ~E_WARNING);
$start = microtime(true);
for ($i = 0; $i < 10000; $i++) {
    file_get_contents('/tmp/nonexistent_' . $i);
}
error_reporting(E_ALL);
echo "Время с error_reporting: " . (microtime(true) - $start) . " сек\n";
Примерный вывод:
Время с @: 0.245 сек
Время с error_reporting: 0.089 сек

Вывод: использование error_reporting предпочтительнее по производительности.

Пример 5: Генерация пользовательских предупреждений с категориями

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

Пример
define('E_USER_INFO', 1024); // Пользовательский уровень
trigger_error('Информационное сообщение', E_USER_INFO); // По умолчанию вызовет обработчик только для E_USER_WARNING и выше

// Но можно в обработчике проверять код:
set_error_handler(function($severity, $msg, $file, $line) {
    if ($severity === 1024) {
        error_log("[INFO] $msg");
    } elseif ($severity === E_USER_WARNING) {
        error_log("[WARNING] $msg");
    }
});

trigger_error('Пользовательское предупреждение', E_USER_WARNING);
В лог попадут обе записи, но для E_USER_INFO - с тегом INFO, для E_USER_WARNING - с тегом WARNING.

Предупреждение контента PHP - comments

En
Content warning php (php)