Создание пользовательского класса ошибки (error class php)

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

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

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

Наиболее эффективный подход - создать абстрактный или конкретный базовый класс ошибки, который наследуется от Exception и расширяет его дополнительными свойствами: пользовательским сообщением, кодом ошибки, контекстом (ассоциативный массив). Такой класс позволяет единообразно обрабатывать все исключения приложения, автоматически логировать их и передавать нужные данные.


<?php

class AppError extends Exception
{
    protected ?string $userMessage = null;
    protected array $context = [];

    public function __construct(
        string $message = "",
        int $code = 0,
        ?Throwable $previous = null,
        ?string $userMessage = null,
        array $context = []
    ) {
        parent::__construct($message, $code, $previous);
        $this->userMessage = $userMessage;
        $this->context = $context;
        $this->logError();
    }

    public function getUserMessage(): string
    {
        return $this->userMessage ?? $this->getMessage();
    }

    public function getContext(): array
    {
        return $this->context;
    }

    protected function logError(): void
    {
        error_log(sprintf(
            "[%s] Code: %d | Message: %s | File: %s:%d",
            static::class,
            $this->code,
            $this->getMessage(),
            $this->getFile(),
            $this->getLine()
        ));
    }
}

Пример использования:


try {
    throw new AppError("Ошибка соединения", 1001, null, "Сервис временно недоступен", ['host' => 'db.example.com']);
} catch (AppError $e) {
    echo $e->getUserMessage(); // Сервис временно недоступен
    print_r($e->getContext());   // ['host' => 'db.example.com']
}

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

  • Забывают вызвать parent::__construct - это нарушает корректную установку сообщения и кода.
  • Не передают $previous при цепочке исключений - теряется информация о первопричине.
  • Переопределённый конструктор может не принимать аргументы, совместимые с родительским, что приводит к фатальным ошибкам при использовании стандартного throw.

Как добавить пользовательское сообщение к исключению?

Простой класс, наследующий Exception и добавляющий только одно свойство $userMessage. Подходит для случаев, когда не требуется сложная иерархия.


class SimpleUserError extends Exception
{
    private string $userMessage;

    public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null, string $userMessage = "")
    {
        parent::__construct($message, $code, $previous);
        $this->userMessage = $userMessage;
    }

    public function getUserMessage(): string
    {
        return $this->userMessage ?: $this->getMessage();
    }
}

Проблема: если не передать $userMessage, метод getUserMessage() вернёт пустую строку, а не сообщение исключения. Решение - проверка через ?: или использование null.

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

Создать иерархию: абстрактный базовый класс и конкретные подклассы (например, ValidationError, DatabaseError, AuthError). Каждый подкласс может иметь свои дополнительные методы и свойства.


abstract class DomainError extends Exception
{
    abstract public function getHttpStatusCode(): int;
}

class ValidationError extends DomainError
{
    private array $errors;

    public function __construct(array $errors, string $message = "Validation failed", int $code = 400)
    {
        parent::__construct($message, $code);
        $this->errors = $errors;
    }

    public function getErrors(): array
    {
        return $this->errors;
    }

    public function getHttpStatusCode(): int
    {
        return 422;
    }
}

Проблема: легко забыть реализовать абстрактный метод во всех подклассах. Рекомендуется использовать интерфейсы вкупе с абстрактными классами.

Как создать класс ошибки, не наследуя Exception?

PHP 7+ позволяет реализовать интерфейс Throwable напрямую. Это редко требуется, но может быть полезно, если нужно полностью переопределить механизм выбрасывания (например, для эмуляции исключений в среде без поддержки).


class CustomThrowable implements Throwable
{
    private string $message;
    private int $code;
    private ?Throwable $previous = null;
    private string $file;
    private int $line;

    public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
    {
        $this->message = $message;
        $this->code = $code;
        $this->previous = $previous;
        // Имитация trace - в реальном коде лучше использовать debug_backtrace()
        $this->file = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['file'] ?? __FILE__;
        $this->line = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['line'] ?? __LINE__;
    }

    public function getMessage(): string { return $this->message; }
    public function getCode(): int { return $this->code; }
    public function getFile(): string { return $this->file; }
    public function getLine(): int { return $this->line; }
    public function getTrace(): array { return []; }
    public function getTraceAsString(): string { return ""; }
    public function getPrevious(): ?Throwable { return $this->previous; }
    public function __toString(): string { return "CustomThrowable: {$this->message} in {$this->file}:{$this->line}"; }
}

Проблема: реализация всех методов Throwable обязательна. В большинстве случаев достаточно наследовать Exception, так как он уже предоставляет корректную трассировку стека.

Как добавить в класс ошибки функциональность логирования?

Можно внедрить логгер через конструктор или использовать статический метод. Лучше применять слабую связку - передавать PSR-3 LoggerInterface.


use Psr\Log\LoggerInterface;

class LoggableError extends Exception
{
    private LoggerInterface $logger;

    public function __construct(
        string $message = "",
        int $code = 0,
        ?Throwable $previous = null,
        LoggerInterface $logger = null
    ) {
        parent::__construct($message, $code, $previous);
        $this->logger = $logger ?? new NullLogger();
        $this->logger->error($message, ['exception' => $this]);
    }
}

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

Расширенные примеры создания классов ошибок

Пример 1. ValidationError с группировкой ошибок полей

Пример

<?php

class ValidationError extends Exception
{
    private array $fieldErrors;

    public function __construct(array $fieldErrors, string $message = 'Validation failed', int $code = 422)
    {
        parent::__construct($message, $code);
        $this->fieldErrors = $fieldErrors;
    }

    public function getFieldErrors(): array
    {
        return $this->fieldErrors;
    }

    public function getErrorForField(string $field): ?string
    {
        return $this->fieldErrors[$field] ?? null;
    }
}

// Использование
try {
    $errors = [
        'email' => 'Неверный формат',
        'password' => 'Минимум 8 символов'
    ];
    throw new ValidationError($errors);
} catch (ValidationError $e) {
    echo $e->getErrorForField('email'); // Неверный формат
}
Неверный формат

Пример 2. DatabaseError с SQL-запросом и параметрами

Пример

<?php

class DatabaseError extends Exception
{
    private string $query;
    private array $params;

    public function __construct(
        string $query,
        array $params,
        string $message = 'Database query error',
        int $code = 500,
        ?Throwable $previous = null
    ) {
        parent::__construct($message, $code, $previous);
        $this->query = $query;
        $this->params = $params;
    }

    public function getQuery(): string
    {
        return $this->query;
    }

    public function getParams(): array
    {
        return $this->params;
    }

    public function getFormattedMessage(): string
    {
        return sprintf("[DB Error] %s | Query: %s | Params: %s",
            $this->getMessage(),
            $this->query,
            json_encode($this->params)
        );
    }
}

// Пример
$query = 'SELECT * FROM users WHERE id = ?';
$params = [42];
try {
    throw new DatabaseError($query, $params, 'Table users not found');
} catch (DatabaseError $e) {
    echo $e->getFormattedMessage();
}
[DB Error] Table users not found | Query: SELECT * FROM users WHERE id = ? | Params: [42]

Пример 3. Иерархия HTTP-ошибок с собственными кодами и сообщениями

Пример

<?php

abstract class HttpError extends Exception
{
    abstract public function getHttpResponseCode(): int;
    abstract public function getHttpResponseBody(): string;
}

class NotFoundError extends HttpError
{
    public function __construct(string $message = 'Resource not found', int $code = 1001)
    {
        parent::__construct($message, $code);
    }

    public function getHttpResponseCode(): int
    {
        return 404;
    }

    public function getHttpResponseBody(): string
    {
        return json_encode(['error' => $this->getMessage(), 'code' => $this->getCode()]);
    }
}

class UnauthorizedError extends HttpError
{
    public function __construct(string $message = 'Authentication required', int $code = 2001)
    {
        parent::__construct($message, $code);
    }

    public function getHttpResponseCode(): int
    {
        return 401;
    }

    public function getHttpResponseBody(): string
    {
        return json_encode(['error' => $this->getMessage()]);
    }
}

// Использование в обработчике
try {
    throw new NotFoundError('Пользователь не найден');
} catch (HttpError $e) {
    http_response_code($e->getHttpResponseCode());
    echo $e->getHttpResponseBody();
}
('HTTP/1.1 404 Not Found'
{"error":"Пользователь не найден","code":1001})

Пример 4. Трейт для единообразного добавления контекста в разные классы

Пример

<?php

trait ContextTrait
{
    private array $context = [];

    public function setContext(array $context): void
    {
        $this->context = $context;
    }

    public function getContext(): array
    {
        return $this->context;
    }

    public function withContext(string $key, $value): self
    {
        $this->context[$key] = $value;
        return $this;
    }
}

class ApiError extends Exception
{
    use ContextTrait;

    public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null, array $context = [])
    {
        parent::__construct($message, $code, $previous);
        $this->setContext($context);
    }
}

// Использование
try {
    $error = new ApiError('Timeout', 504);
    $error->withContext('url', '/api/data')->withContext('duration', 3000);
    throw $error;
} catch (ApiError $e) {
    print_r($e->getContext());
}
Array
(
    [url] => /api/data
    [duration] => 3000
)

Создание класса ошибки в PHP - comments

En
Error class php (php)