Обработка пользовательских ошибок при разработке на PHP

Раздел: Разработка на 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)

Ошибка пользователя в PHP - comments

En
Php user error (php)