Комментарии (2)
Ответ сгенерирован нейросетью и может содержать ошибки
Мои практики создания пользовательских исключений (Custom Exceptions)
Да, создавал и активно использую пользовательские исключения в production-проектах. Это неотъемлемая часть профессиональной архитектуры приложений, особенно для сложных систем с четким разделением ответственности и многоуровневой бизнес-логикой.
Зачем создавать собственные исключения?
Пользовательские исключения решают несколько ключевых задач:
- Семантическая ясность: Более точно описывают природу ошибки, чем стандартные
RuntimeExceptionилиInvalidArgumentException - Логическая группировка: Позволяют создавать иерархии исключений для разных слоев приложения (Domain, Application, Infrastructure)
- Упрощенная обработка: Можно ловить конкретные типы исключений, а не проверять сообщения или коды
- Добавление контекста: Возможность включать в исключение специфичные для бизнес-логики данные
- Согласованность: Унификация обработки ошибок во всей кодовой базе
Типичные примеры создания исключений
Базовый пример с наследованием от стандартных исключений
<?php
namespace App\Domain\User\Exception;
/**
* Бизнес-логика: пользователь не найден в системе
* Наследуем от DomainException, так как это нарушение доменных правил
*/
class UserNotFoundException extends \DomainException
{
private ?int $userId;
public function __construct(string $message = "User not found", int $code = 0, ?int $userId = null)
{
parent::__construct($message, $code);
$this->userId = $userId;
}
public function getUserId(): ?int
{
return $this->userId;
}
}
Иерархия исключений для слоеной архитектуры
В современных приложениях часто используют DDD (Domain-Driven Design) или гексагональную архитектуру, где исключения разделяются по слоям:
<?php
// Доменный слой (бизнес-правила)
namespace App\Domain\Order\Exception;
class OrderAlreadyPaidException extends \DomainException
{
// Нарушение инвариантов домена
}
// Прикладной слой (use cases)
namespace App\Application\Order\Exception;
class OrderProcessingFailedException extends \RuntimeException
{
// Ошибки в сценариях использования
}
// Инфраструктурный слой (внешние сервисы)
namespace App\Infrastructure\Payment\Exception;
class PaymentGatewayTimeoutException extends \RuntimeException
{
private \DateTimeImmutable $occurredAt;
public function __construct(string $gatewayName, int $timeoutSeconds)
{
parent::__construct(
sprintf('Payment gateway "%s" timeout after %d seconds', $gatewayName, $timeoutSeconds)
);
$this->occurredAt = new \DateTimeImmutable();
}
public function getOccurredAt(): \DateTimeImmutable
{
return $this->occurredAt;
}
}
Исключения с дополнительными данными для логов и мониторинга
<?php
namespace App\Application\Validation\Exception;
/**
* Исключение с детальной информацией о ошибках валидации
* Полезно для API, чтобы возвращать структурированные ошибки
*/
class ValidationFailedException extends \InvalidArgumentException
{
private array $errors;
private string $validationContext;
public function __construct(array $errors, string $validationContext = 'general')
{
$this->errors = $errors;
$this->validationContext = $validationContext;
$errorMessages = array_map(fn($error) => $error['message'], $errors);
parent::__construct(
sprintf('Validation failed in context "%s": %s',
$validationContext,
implode(', ', $errorMessages))
);
}
public function getErrors(): array
{
return $this->errors;
}
public function getValidationContext(): string
{
return $this->validationContext;
}
/**
* Преобразуем исключение в массив для API-ответа
*/
public function toArray(): array
{
return [
'type' => 'validation_error',
'context' => $this->validationContext,
'errors' => $this->errors,
'timestamp' => time()
];
}
}
Лучшие практики при создании исключений
1. Осмысленное наследование
LogicExceptionи её потомки (InvalidArgumentException,DomainException) - для ошибок, которые можно обнаружить до выполнения кодаRuntimeExceptionи её потомки - для ошибок, которые возникают во время выполнения
2. Четкое неймспейсирование
Исключения должны находиться в соответствующих неймспейсах, обычно:
App\Domain\{Entity}\Exception\App\Application\{UseCase}\Exception\App\Infrastructure\{Service}\Exception\
3. Добавление контекстной информации
Включайте в исключение данные, которые помогут:
- Разработчику понять причину
- Системе мониторинга классифицировать ошибку
- Пользователю получить понятное сообщение (через перевод)
4. Создание базовых классов для унификации
Часто создаю базовые исключения для всего приложения:
<?php
namespace App\Shared\Exception;
abstract class ApplicationException extends \RuntimeException
{
private string $errorCode;
private array $context;
public function __construct(
string $message,
string $errorCode,
array $context = [],
int $code = 0,
\Throwable $previous = null
) {
parent::__construct($message, $code, $previous);
$this->errorCode = $errorCode;
$this->context = $context;
}
public function getErrorCode(): string
{
return $this->errorCode;
}
public function getContext(): array
{
return $this->context;
}
}
5. Интеграция с системами мониторинга
Пользовательские исключения могут автоматически обогащаться данными для Sentry, Rollbar или других систем:
<?php
try {
$orderProcessor->process($order);
} catch (OrderProcessingFailedException $e) {
// Обогащаем контекст для Sentry
\Sentry\configureScope(function (\Sentry\State\Scope $scope) use ($e, $order) {
$scope->setContext('order', [
'id' => $order->getId(),
'amount' => $order->getAmount(),
'customer' => $order->getCustomerId()
]);
$scope->setTag('exception_type', 'order_processing');
});
throw $e;
}
Заключение
Создание пользовательских исключений — это признак зрелой кодовой базы. Это позволяет:
- Четко разделять ответственность между слоями приложения
- Улучшать диагностику ошибок в production
- Создавать более читаемый и поддерживаемый код
- Реализовывать специфичные для бизнеса сценарии обработки ошибок
В современных PHP-
фреймворках (Laravel, Symfony) пользовательские исключения также интегрируются с механизмами обработки ошибок HTTP, преобразуясь в соответствующие статус -коды и структурированные JSON-ответы для API.