Создание пользовательского класса ошибки (error class 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
)