'ConstraintViolationListInterface to Exception in Symfony

I need to convert an object of type ConstraintViolationListInterface to a single exception for further logging, where the message is a concatenation of the messages from each constraint violation on the list, when the validation fails.

Obviously I can't repeat a foreach loop in every bundle using validation to achieve this, therefore I was thinking about creating one more bundle providing a simple service accepting ConstraintViolationListInterface and returning a single exception. Is there a standard solution for this in Symfony? Seems weird that I need to write this service, the problem seems to be common to me.



Solution 1:[1]

I also was surprised that symfony has nothing helpful for this, that's why I've created my custom exception:

class ValidationException extends \Exception
{
    private $violations;

    public function __construct(array $violations)
    {
        $this->violations = $violations;
        parent::__construct('Validation failed.');
    }

    public function getMessages()
    {
        $messages = [];
        foreach ($this->violations as $paramName => $violationList) {
            foreach ($violationList as $violation) {
                $messages[$paramName][] = $violation->getMessage();
            }
        }
        return $messages;
    }

    public function getJoinedMessages()
    {
        $messages = [];
        foreach ($this->violations as $paramName => $violationList) {
            foreach ($violationList as $violation) {
                $messages[$paramName][] = $violation->getMessage();
            }
            $messages[$paramName] = implode(' ', $messages[$paramName]);
        }
        return $messages;
    }
}

All code available here.

And I use this exception in a next way:

try {
    $errors = $validator->validate(...);
    if (0 !== count($errors)) {
        throw new ValidationException($errors);
    }
} catch (ValidationException $e) {
    // Here you can obtain your validation errors. 
    var_dump($e->getMessages());
}

Solution 2:[2]

This solution works fine for me:

protected function violationsToArray(ConstraintViolationListInterface $violations)
{
    $messages = [];

    foreach ($violations as $constraint) {
        $prop = $constraint->getPropertyPath();
        $messages[$prop][] = $constraint->getMessage();
    }

    return $messages;
}

Note that using $violations array keys as property names will not work:

    $messages = [];

    foreach ($violations as $prop => $constraint) {
        // $prop will not contain any value and this will not work as expected
        $messages[$prop][] = $constraint->getMessage();
    }

Solution 3:[3]

Maybe you can create a ConstraintViolationsEvent like this :

namespace AppBundle\Event;

use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\Validator\ConstraintViolationListInterface;

/**  
 * The order.placed event is dispatched each time an order is created
 * in the system.
 */
class ConstraintViolationsEvent extends Event
{
    const VIOLATIONS_DETECTED = 'constraint_violations.detected';

    protected $constraintViolationList;

    public function __construct(ConstraintViolationListInterface $constraintViolationList)
    {
        $this->constraintViolationList = $constraintViolationList;
    }

    public function getConstraintViolationList()
    {
        return $this->constraintViolationList;
    }
}

Then you can create a listener for this event, and inside this listener, you create your Exception based on all the violations found. Each time that you will find violations you just dispatch your event inside your controller like this :

class MyController extends Controller
{
    public function myFormAction(Request $request)
    {
        /** handle the request, get the form data, validate the form...etc. **/
        $event = new ConstraintViolationsEvent($constraintViolationList);
        $dispatcher->dispatch(ConstraintViolationsEvent::VIOLATIONS_DETECTED, $event);
    }
}

In fact, you can manage the creation of your Exception inside a service and call the service in the listener. It is up to you.

Solution 4:[4]

I don't know if it was the case before, but, with Symfony 5.4 (and probably for awhile) you can use a string cast on the ConstraintViolationList object returned:

/** @var ConstraintViolationList $violations */
$violations = $this->validator->validate($object);
if ($violations->count()) {
    throw new \Exception('Validation failed: '.$violations);
} 

In this case, the cast is automatically done when concatening $violations with the previous string. Of course, you have no control over the output.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 cn007b
Solution 2 Miguel A. A.
Solution 3 fgamess
Solution 4 COil