Отладка PHP: методы регистрации и отображения ошибочных ситуаций

Раздел: Разработка на PHP -> Отладка

Ошибки в PHP могут быть различного типа: фатальные, предупреждения, заметки, пользовательские. Их правильная обработка позволяет повысить надежность приложения и упростить отладку. Рассмотрим несколько подходов.

Основные подходы к обработке ошибок в PHP

Как создать единый механизм обработки всех ошибок и исключений?

Наиболее эффективное решение - преобразование обычных ошибок в исключения с помощью ErrorException. Это позволяет использовать конструкцию try-catch для большинства ошибок (кроме фатальных).

set_error_handler(function ($severity, $message, $file, $line) {
    if (!(error_reporting() & $severity)) {
        return false;
    }
    throw new \ErrorException($message, 0, $severity, $file, $line);
});

try {
    echo $undefinedVar;
} catch (\ErrorException $e) {
    file_put_contents('errors.log', $e->__toString(), FILE_APPEND);
    echo 'Обнаружена ошибка: ' . $e->getMessage();
}

В этом примере ошибка Notice преобразуется в исключение, перехватывается блоком catch и логируется. Для фатальных ошибок (E_ERROR) используется register_shutdown_function.

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

  • Обработчик не срабатывает для фатальных ошибок - используется register_shutdown_function.
  • Если не проверять error_reporting, исключение будет выброшено даже для подавленных ошибок (оператор @). Исправление: проверка if (!(error_reporting() & $severity)).
  • Сигнатура функции обработчика должна содержать все параметры, иначе PHP выдаст предупреждение.

Как обрабатывать только исключения, не затрагивая обычные ошибки?

Можно использовать стандартные блоки try-catch. Ошибки уровня Notice и Warning не будут перехвачены, поэтому их необходимо устранять в коде или преобразовывать в исключения.

try {
    throw new \InvalidArgumentException('Неверные данные');
} catch (\InvalidArgumentException $e) {
    echo 'Исключение: ' . $e->getMessage();
}
// Notice не будет перехвачен
echo $notDefined;

Проблема: ошибки типа Notice остаются необработанными. Решение - дополнительно настроить обработчик.

Как настроить отображение ошибок на разных этапах?

На локальном сервере все ошибки можно показывать, на продакшене - скрывать и логировать.

if (getenv('APP_ENV') === 'development') {
    error_reporting(E_ALL);
    ini_set('display_errors', 1);
} else {
    error_reporting(E_ALL & ~E_DEPRECATED & ~E_STRICT);
    ini_set('display_errors', 0);
    ini_set('log_errors', 1);
    ini_set('error_log', '/var/log/php_errors.log');
}

Проблема: забывают скрыть ошибки на продакшене - пользователь может увидеть чувствительную информацию. Решение: всегда проверять окружение.

Как вести лог ошибок для анализа?

Применяются директивы php.ini или вызовы error_log() для записи в файл.

ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/app_errors.log');
// явная запись
error_log('Пользовательское сообщение', 3, 'custom.log');

Проблемы: неверный путь к файлу, недостаточно прав. Решение: создавать файл с правильными правами, использовать абсолютные пути.

Как создать собственный обработчик для фильтрации ошибок?

Функция set_error_handler позволяет определить собственную реакцию на разные уровни ошибок.

set_error_handler(function($level, $msg, $file, $line) {
    switch ($level) {
        case E_WARNING:
            file_put_contents('warnings.log', "[$file:$line] $msg\n", FILE_APPEND);
            break;
        case E_NOTICE:
            // игнорируется
            break;
        default:
            throw new \ErrorException($msg, 0, $level, $file, $line);
    }
    return true; // предотвращается выполнение стандартного обработчика
});

Проблема: если не вернуть true, PHP вызовет стандартный обработчик. Решение: вернуть true для полного переопределения.

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

Фатальные ошибки не передаются в set_error_handler. Следует применить register_shutdown_function и error_get_last.

register_shutdown_function(function() {
    $lastError = error_get_last();
    if ($lastError && in_array($lastError['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
        file_put_contents('fatal.log', print_r($lastError, true), FILE_APPEND);
        http_response_code(500);
        echo 'Фатальная ошибка: ' . $lastError['message'];
    }
});

Проблема: функция shutdown вызывается также при нормальном завершении. Проверка error_get_last необходима, чтобы реагировать только на ошибки.

Как получить красивую страницу ошибки с трассировкой?

Библиотека Whoops предоставляет интерактивный отладчик для локальной разработки. Установка через Composer.

composer require filp/whoops
// в index.php
$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();

Проблема: использование Whoops на продакшене может раскрыть исходный код. Решение: включать только для development окружения.

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

Ниже приведены дополнительные практические сценарии.

Пример 1: Полный класс для обработки ошибок с выводом JSON для API

Пример
class ErrorHandler {
   private $logFile;
   public function __construct($logFile) {
       $this->logFile = $logFile;
   }
   public function register() {
       set_error_handler([$this, 'handleError']);
       register_shutdown_function([$this, 'handleFatal']);
   }
   public function handleError($level, $msg, $file, $line) {
       if (!(error_reporting() & $level)) {
           return false;
       }
       $exception = new \ErrorException($msg, 0, $level, $file, $line);
       $this->log($exception);
       if ($this->isApiRequest()) {
           header('Content-Type: application/json');
           http_response_code(500);
           echo json_encode(['error' => 'Internal Server Error', 'message' => $msg]);
           exit;
       }
       throw $exception;
   }
   public function handleFatal() {
       $err = error_get_last();
       if ($err && in_array($err['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) {
           $exception = new \ErrorException($err['message'], 0, $err['type'], $err['file'], $err['line']);
           $this->log($exception);
           while (ob_get_level()) ob_end_clean();
           http_response_code(500);
           echo 'Серверная ошибка. Попробуйте позже.';
       }
   }
   private function log(\Throwable $e) {
       $entry = date('Y-m-d H:i:s') . ' ' . $e->__toString() . PHP_EOL;
       file_put_contents($this->logFile, $entry, FILE_APPEND);
   }
   private function isApiRequest() {
       return isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false;
   }
}
$handler = new ErrorHandler(__DIR__ . '/errors.log');
$handler->register();
Запись в errors.log:
2025-03-30 12:00:00 exception 'ErrorException' with message 'Undefined variable: test' in /var/www/index.php:10
При AJAX-запросе с Accept: application/json возвращается JSON: {"error":"Internal Server Error","message":"Undefined variable: test"}

Пример 2: Интеграция с Monolog

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

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

$mailer = new Swift_Mailer(new Swift_SmtpTransport('smtp.example.com', 587));
$logger->pushHandler(new SwiftMailerHandler($mailer, new Swift_Message('Critical Error'), Logger::CRITICAL));

set_error_handler(function ($level, $msg, $file, $line) use ($logger) {
   $logger->log(Logger::WARNING, "$msg in $file:$line");
   return true;
});
В app.log записываются все Warning и выше. При критической ошибке отправляется email.

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

Пример
register_shutdown_function(function() {
   $error = error_get_last();
   if ($error !== null && in_array($error['type'], [E_ERROR, E_USER_ERROR])) {
       while (ob_get_level()) ob_end_clean();
       http_response_code(500);
       echo '<html><body><h1>Ошибка сервера</h1><p>Мы уже работаем над этим.</p></body></html>';
   }
});
Вместо сообщения PHP выводится чистая HTML-страница с кодом 500.

Пример 4: Преобразование Warning в исключение для REST API

Пример
set_error_handler(function($severity, $message, $file, $line) {
   throw new \ErrorException($message, 0, $severity, $file, $line);
});

try {
   $result = 1 / 0; // Division by zero - E_WARNING
} catch (\ErrorException $e) {
   http_response_code(400);
   echo json_encode(['code' => $e->getCode(), 'message' => $e->getMessage()]);
}
HTTP 400, JSON: {"code":0,"message":"Division by zero"}

Пример 5: Отправка уведомления в Telegram при критических ошибках

Пример
set_error_handler(function($level, $msg, $file, $line) {
   if (in_array($level, [E_ERROR, E_USER_ERROR])) {
       $text = "Ошибка: $msg в $file:$line";
       $botToken = '...';
       $chatId = '...';
       file_get_contents("https://api.telegram.org/bot{$botToken}/sendMessage?chat_id={$chatId}&text=" . urlencode($text));
   }
   return false;
});
При каждой ошибке уровня ERROR отправляется сообщение в Telegram-чат.

Обработка ошибок PHP - comments

En
Php error (php)