Комментарии 5
С годами я понял одну вещь. Чем больше я смотрю на ООП, на все эти потуги с DDD, отловом исключений и т.п, тем больше понимаю, что через ООП нужно пройти. Сначала осознать, потом обуздать, потом стать евангелистом ООП, потом начать сомневаться что бы потом в итоге осознать - что тебе это не нужно.
Выглядит интересно, но первая же мысль при взгляде на Packagist: почему вы разрешаете только Symfony 7.4? А как же 8.0?
Также, вам точно не помешает ознакомиться с тем, как оформляется bundle в Symfony. Сейчас то, что ставится через Composer - это не bundle, а ваш тестовый проект для его разработки.
Обратите внимание на то, что в устанавливаемом коде много лишнего: bin, config, public, tests - всё это вообще не имеет смысла тащить во внешние проекты, их нужно убирать через export-ignore в .gitattributes
Типовую конфигурацию стоит создавать через Symfony recipes.
Без всяких бандлов код типичного контроллера на симфони выглядит так:
public function create(
#[MapRequestPayload] CreateUserRequest $userRequest,
): JsonResponse {
$user = $this->userService->createService($userRequest);
return new JsonResponse($this->serializer->serialize($user , 'json'), Response::HTTP_CREATED, json: true);
}
MapRequestPayload самостоятельно вызывает валидацию. По умолчанию сериалайзер отвечает ошибками в формате RFC 7807. Однако и код ответа и формат легко меняются.
Перехват исключений в контроллере моветон с версии 2.0.0-BETA. Для централизованной обработки исключений существует событие kernel.exception. Если вы не хотите кидаться из сервисов http-исключениями, можно держать всю обработку исключений в одном сабскрайбере/листенере. Например:
final readonly class ExceptionResponseSubscriber implements EventSubscriberInterface
{
private const EXCEPTION_MAP = [
OrderNotFound::class => Response::HTTP_NOT_FOUND,
InvalidOrderData::class => Response::HTTP_UNPROCESSABLE_ENTITY,
AccessDenied::class => Response::HTTP_FORBIDDEN,
];
public static function getSubscribedEvents(): array
{
return [KernelEvents::EXCEPTION => 'onKernelException'];
}
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
$class = $exception::class;
if (!array_key_exists($class, self::EXCEPTION_MAP)) {
return; // Пусть стандартный ErrorListener обработает
}
$event->setResponse(new JsonResponse(
['error' => $exception->getMessage()],
self::EXCEPTION_MAP[$class]
));
}
}
Совершенно верно - и #[MapRequestPayload] с автовалидацией, и централизованная обработка через kernel.exception - это стандартные инструменты Symfony. Ваш пример хорошо показывает, что в рамках одного проекта можно обойтись без дополнительного бандла и держать всё под контролем. Собственно, я и сам делал именно так - через subscriber с маппингом исключений и единым JsonResponse.
Бандл решает немного другую задачу. Он не добавляет новую механику поверх Symfony, а упаковывает уже существующие практики в переиспользуемый слой. Когда проектов становится много (микросервисы, разные команды, разные подрядчики), важна не просто централизованная обработка ошибок, а гарантированная консистентность формата ответов между сервисами - без копирования listener’ов из проекта в проект и без постепенного "дрейфа" структуры.
В случае с ExceptionResponseSubscriber его действительно несложно написать. Сложнее - поддерживать синхронность изменений между несколькими репозиториями, когда формат ответа эволюционирует.
Кстати, по RFC 7807 - идея хорошая! Если кому-то нужен полноценный problem+json технически это реализуемо через ResponseFactoryInterface который уже есть в бандле. Можно добавить ProblemDetailsResponseFactory как альтернативную реализацию и переключать через конфиг (format: problem+json). Единственный нюанс - RFC 7807 стандартизирует только формат ошибок, для успешных ответов общего стандарта нет, поэтому это скорее опциональный режим, а не замена дефолтному формату. Если будет запрос - реализовать несложно.
В общем, ваш подход полностью корректен для проекта, где всё находится под контролем одной команды. А бандл может быть полезен там, где нужна стандартизация между несколькими проектами "из коробки".

ApiKit — чистый REST API в Symfony без шаблонного кода