Обработка пользовательских ошибок при разработке на PHP
Пользовательские ошибки в PHP возникают при выполнении кода, когда программист явно вызывает функцию trigger_error. В отличие от системных ошибок (например, неверный синтаксис), пользовательские ошибки дают возможность контролировать логику приложения и реагировать на некорректные данные или действия пользователя. В контексте управления пользователями такие ошибки часто применяются при валидации ввода, проверке прав доступа или логировании подозрительных действий.
Основное эффективное решение: перехват пользовательских ошибок через set_error_handler
Как сделать так, чтобы пользовательские ошибки не прерывали выполнение скрипта, но при этом фиксировались в логе?
Установка пользовательского обработчика ошибок с помощью set_error_handler позволяет перехватывать все уровни ошибок, включая E_USER_ERROR, E_USER_WARNING и E_USER_NOTICE. Внутри обработчика можно записать сообщение в файл лога, отправить уведомление администратору или преобразовать ошибку в исключение. Пример базового обработчика:
<?php
function userErrorHandler($errno, $errstr, $errfile, $errline) {
$logMessage = "[Пользовательская ошибка] $errstr в $errfile строке $errline";
error_log($logMessage, 3, '/var/log/user_errors.log');
// Для E_USER_ERROR выполнение прерывать не обязательно
// Можно вывести сообщение пользователю
if ($errno == E_USER_ERROR) {
die("Произошла фатальная ошибка. Пожалуйста, обратитесь к администратору.");
}
return true;
}
set_error_handler('userErrorHandler');
// Пример вызова пользовательской ошибки
trigger_error("Попытка доступа к запрещённому ресурсу", E_USER_WARNING);
// Даже при E_USER_ERROR скрипт может продолжить работу, если обработчик не завершает выполнение
echo "Скрипт продолжает работу.";
?>
(в файле /var/log/user_errors.log появится запись) [Пользовательская ошибка] Попытка доступа к запрещённому ресурсу в /var/www/script.php строке 14
Типичная проблема: если не вернуть true из обработчика, PHP продолжит стандартную обработку ошибки, что приведёт к дублированию сообщений. Также необходимо помнить, что обработчик не перехватывает фатальные ошибки (E_ERROR, E_PARSE) – для них требуется регистрация функции завершения через register_shutdown_function.
Цель использования: централизованное логирование и мягкая обработка пользовательских ошибок без остановки приложения для некритичных ситуаций (E_USER_WARNING, E_USER_NOTICE).
Вариант 1: Преобразование пользовательской ошибки в исключение ErrorException
Как заставить пользовательскую ошибку вести себя как исключение и перехватывать её в try/catch?
Создаётся обработчик, который выбрасывает исключение класса ErrorException. Это позволяет использовать стандартную механику исключений для любых уровней ошибок. Особенно полезно при работе с пользовательским вводом, где каждая ошибка должна быть обработана индивидуально.
<?php
function exceptionErrorHandler($severity, $message, $file, $line) {
throw new ErrorException($message, 0, $severity, $file, $line);
}
set_error_handler('exceptionErrorHandler');
try {
// Вызов пользовательской ошибки
trigger_error("Имя пользователя содержит недопустимые символы", E_USER_ERROR);
} catch (ErrorException $e) {
$log = date('Y-m-d H:i:s') . " - " . $e->getMessage();
file_put_contents('user_errors.log', $log . PHP_EOL, FILE_APPEND);
echo "Проверьте введённые данные: " . $e->getMessage();
}
?>
Проверьте введённые данные: Имя пользователя содержит недопустимые символы
Важно: ErrorException перехватывает только те ошибки, которые были вызваны trigger_error. Стандартные фатальные ошибки (E_ERROR) в исключение не превратятся. Также при использовании такого подхода все пользовательские ошибки становятся исключениями – это может нарушить логику, где ожидается только предупреждение, а не остановка выполнения.
Цель: унификация обработки ошибок и исключений, возможность точного реагирования на каждую ситуацию в коде управления пользователями (например, при регистрации или аутентификации).
Вариант 2: Использование assert() для проверки предусловий
Как проверять корректность данных пользователя без написания отдельных условных конструкций?
Функция assert() позволяет задать условие, которое должно быть истинным. Если условие ложно, генерируется предупреждение (E_WARNING) или, в зависимости от настроек, выбрасывается исключение. Удобно для отладки, но в production обычно отключается.
<?php
assert_options(ASSERT_ACTIVE, 1);
assert_options(ASSERT_WARNING, 1);
assert_options(ASSERT_CALLBACK, function($file, $line, $code, $desc) {
error_log("Ассерт не пройден в $file:$line: $desc");
});
$userId = $_POST['user_id'] ?? null;
assert($userId !== null && is_numeric($userId), "user_id должен быть числом");
// Если assert не пройден, в лог запишется сообщение, но скрипт продолжит работу
?>
(если $userId не задан или не число, в логе появится запись) Ассерт не пройден в /var/www/script.php:9: user_id должен быть числом
В production assert() лучше отключать (ASSERT_ACTIVE = 0). Если этого не сделать, assert может генерировать предупреждения, которые при включённом display_errors будут видны пользователю. Также assert не подходит для обработки ошибок, которые должны быть обязательно обработаны – он лишь проверяет условия.
Цель: быстрая отладка и проверка предусловий в процессе разработки, особенно при тестировании форм регистрации или ввода данных.
Вариант 3: Пользовательские исключения с собственным классом
Как создать своё исключение для ошибок, связанных с управлением пользователями?
Определение класса, наследующего от Exception, и его выброс при нарушении бизнес-правил. Это даёт возможность группировать ошибки по типу и обрабатывать их отдельно.
<?php
class UserValidationException extends Exception {}
class UserAuthException extends Exception {}
function validatePassword($password) {
if (strlen($password) < 8) {
throw new UserValidationException("Пароль слишком короткий");
}
if (!preg_match('/[A-Z]/', $password)) {
throw new UserValidationException("Пароль должен содержать заглавные буквы");
}
return true;
}
try {
validatePassword("weak");
} catch (UserValidationException $e) {
echo "Ошибка валидации: " . $e->getMessage();
// Запись в лог
error_log($e->getMessage() . " от пользователя " . $_SERVER['REMOTE_ADDR']);
} catch (UserAuthException $e) {
// другая обработка
}
?>
Ошибка валидации: Пароль слишком короткий
Не стоит выбрасывать исключения для каждой мелкой ошибки - это замедляет выполнение и усложняет код. Лучше использовать для критических нарушений бизнес-логики. Также нужно быть осторожным с тем, чтобы не перехватывать все исключения через Exception, так как это может скрыть реальные проблемы.
Цель: структурированная обработка ошибок, связанных с действиями пользователя (некорректный ввод, попытка взлома, нарушение прав доступа).
Расширенные примеры работы с пользовательскими ошибками в PHP
Пример 1. Комбинированный обработчик с разными уровнями ошибок
Создаётся централизованная функция, которая логирует ошибки и, в зависимости от уровня, либо продолжает выполнение, либо останавливает скрипт.
<?php
// Настройка обработчика
function customErrorHandler($errno, $errstr, $errfile, $errline) {
$levels = [
E_USER_ERROR => 'Ошибка',
E_USER_WARNING => 'Предупреждение',
E_USER_NOTICE => 'Уведомление',
];
$levelName = $levels[$errno] ?? 'Неизвестный уровень';
$logEntry = sprintf("[%s] %s в %s:%d", $levelName, $errstr, $errfile, $errline);
error_log($logEntry, 3, __DIR__ . '/../logs/user_errors.log');
// Для тяжёлых ошибок отправка email администратору
if ($errno == E_USER_ERROR) {
mail('admin@example.com', 'Критическая ошибка пользователя', $logEntry);
echo "<div class='alert alert-danger'>Критическая ошибка. Обратитесь к администратору.</div>";
exit(1);
}
return true;
}
set_error_handler('customErrorHandler');
// Вызовы разных уровней
trigger_error("Неверный формат email", E_USER_NOTICE);
trigger_error("Попытка входа с заблокированного IP", E_USER_WARNING);
trigger_error("SQL-инъекция в поле username", E_USER_ERROR);
echo "Этот текст не будет выведен при E_USER_ERROR";
?>
(Вывод на экран при E_USER_ERROR) <div class='alert alert-danger'>Критическая ошибка. Обратитесь к администратору.</div> (В лог файл user_errors.log добавятся три строки) [Уведомление] Неверный формат email в /var/www/script.php:17 [Предупреждение] Попытка входа с заблокированного IP в /var/www/script.php:18 [Ошибка] SQL-инъекция в поле username в /var/www/script.php:19
Пример 2. Преобразование ошибок в исключения и фильтрация по уровню
Обработчик выбирает только определённые уровни ошибок для превращения в исключения, остальные игнорирует.
<?php
function selectiveExceptionHandler($severity, $message, $file, $line) {
// Превращаем только E_USER_ERROR в исключение
if ($severity === E_USER_ERROR) {
throw new ErrorException($message, 0, $severity, $file, $line);
}
// Для E_USER_WARNING и E_USER_NOTICE просто логируем
$log = sprintf("[%s] %s в %s:%d",
($severity === E_USER_WARNING ? 'WARNING' : 'NOTICE'),
$message, $file, $line
);
error_log($log);
return true;
}
set_error_handler('selectiveExceptionHandler');
try {
trigger_error("Попытка доступа без авторизации", E_USER_ERROR);
} catch (ErrorException $e) {
echo "Исключение: " . $e->getMessage();
// Дополнительно записать в отдельный лог
file_put_contents('access_errors.log', $e->getMessage() . PHP_EOL, FILE_APPEND);
}
trigger_error("Истекло время сессии", E_USER_NOTICE);
echo "Скрипт завершился";
?>
Исключение: Попытка доступа без авторизации (в стандартный лог ошибок добавлена запись для NOTICE) Скрипт завершился
Пример 3. Использование пользовательского класса ошибок с контекстом
Создаётся класс, который хранит дополнительную информацию о пользователе и времени.
<?php
class UserError extends Exception {
private $userId;
private $ip;
private $timestamp;
public function __construct($message, $userId = null, $ip = null) {
parent::__construct($message);
$this->userId = $userId;
$this->ip = $ip ?? $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$this->timestamp = date('Y-m-d H:i:s');
}
public function getContext() {
return [
'user_id' => $this->userId,
'ip' => $this->ip,
'time' => $this->timestamp,
'message' => $this->getMessage()
];
}
}
function loginUser($username, $password) {
// ... проверка
if (!$password) {
throw new UserError("Пустой пароль", null, $_SERVER['REMOTE_ADDR']);
}
// Ещё проверка
}
try {
loginUser('user', '');
} catch (UserError $e) {
$context = $e->getContext();
$logLine = json_encode($context) . PHP_EOL;
file_put_contents('user_errors.json', $logLine, FILE_APPEND);
echo "Ошибка: " . $e->getMessage() . " (IP: " . $context['ip'] . ")";
}
?>
Ошибка: Пустой пароль (IP: 192.168.1.100)
(в файл user_errors.json добавлена строка)
{"user_id":null,"ip":"192.168.1.100","time":"2025-04-07 14:32:00","message":"Пустой пароль"}
Пример 4. Работа с E_USER_DEPRECATED и обратной совместимостью
Уровень E_USER_DEPRECATED используется для предупреждения о том, что функция устарела. Применимо при миграции кода управления пользователями.
<?php
error_reporting(E_ALL);
function oldUserFunction() {
trigger_error("Функция oldUserFunction устарела, используйте newUserFunction", E_USER_DEPRECATED);
// старый код
}
set_error_handler(function($errno, $errstr) {
if ($errno === E_USER_DEPRECATED) {
error_log("[DEPRECATED] " . $errstr);
}
return true;
});
oldUserFunction();
echo "Вызов устаревшей функции не прерывает выполнение";
?>
Вызов устаревшей функции не прерывает выполнение (в лог ошибок добавлена запись о deprecated)