Отладка 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-чат.