-
Notifications
You must be signed in to change notification settings - Fork 11
Description
Originally posted by @gquemener :
Let's define domain related validation once and for all within well-defined VO and expose those errors to the world. > https://gist.github.com/gquemener/09b2bc303e63dfc3123f7d540e9891a0
It prevents to add extra constraint metadata!
Is this too much magic?
@matthiasnoback and @AntonStoeckl joined the discussion.
@AntonStoeckl also blogged about a solution in GO:
On our case:
We decided to work w/ the #symfonymessenger usind the mentioned validation middleware based on symfony constraints.
VOs are created from command getters. Every domain exception is caught by an listener and converted into human-readable messages.
Example:
# config/packages/messenger.yaml
framework:
messenger:
default_bus: messenger.bus.commands
buses:
messenger.bus.commands:
middleware:
- validation# config/services.yaml
services:
Acme\Common\Infrastructure\Symfony\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
arguments: ['@translator.default']
<?php
namespace Acme\Common\Infrastructure\Symfony\EventListener;
use DomainException;
use Prooph\EventStore\Exception\ConcurrencyException;
use Acme\Common\Presentation\Model\NotFoundException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\RuntimeException;
use Symfony\Component\Messenger\Exception\ValidationFailedException;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Handles bad requests and returns a 400 status with human-readable errors to the client.
* Returns a 500 status otherwise.
*/
final class ExceptionListener
{
/** @var TranslatorInterface */
private $translator;
private $translationDomain = 'exception';
private $locale = 'de';
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function onKernelException(ExceptionEvent $event)
{
// You get the exception object from the received event
$exception = $event->getThrowable();
if (!$exception instanceof RuntimeException) {
return;
}
$response = new JsonResponse();
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
if ($exception instanceof ValidationFailedException) {
// Handle domain specific exceptions - public to the client
$errors = [];
/** @var ConstraintViolation $violation */
foreach ($exception->getViolations() as $violation) {
$errors[] = [
'message' => $violation->getMessage(),
'path' => $violation->getPropertyPath()
];
}
$response->setContent(json_encode(['errors' => $errors]));
$event->setResponse($response);
return;
}
if ($exception instanceof HandlerFailedException) {
// Handle presentation and domain specific exceptions - public to the client
$errors = [];
if ($exception->getPrevious() instanceof NotFoundException) {
$errors = [
'message' => $exception->getMessage(),
'path' => null
];
$response->setContent(json_encode(['errors' => $errors]));
$response->setStatusCode(Response::HTTP_NOT_FOUND);
$event->setResponse($response);
return;
}
if ($exception->getPrevious() instanceof DomainException) {
$errors[] = [
'message' => $this->translator->trans(
$exception->getPrevious()->getMessage(), [], $this->translationDomain, $this->locale
),
'path' => null
];
$response->setContent(json_encode(['errors' => $errors]));
$event->setResponse($response);
return;
}
// Handle individual server errors if relevant to the client
switch (get_class($exception->getPrevious())) {
case ConcurrencyException::class:
$errors = [
'message' => 'Duplicate entry',
'path' => null
];
$response->setContent(json_encode(['errors' => $errors]));
$event->setResponse($response);
return;
break;
}
}
}
}The listener can be expanded for individual exceptions. Exception messages are simply translated.
If it is a Symfony Constraint error message it normally was already translated. In addtion the path will be added.
The JSON result normally looks like this:
errors: {
message: "Some error in the command DTO"
path: "firstName"
}errors: {
message: "Some error in the domain e.g. thrown inside value object"
path: null
}AFAIK Symfony Messenger can be used without Symfony. You can use it as a standalone service bus.
The Validation middleware is included too. Putting the "logic" of the listener elsewhere should be no problem.
WDYT?
Older possibly related issues: