Improving Symfony ChoiceType Error Messages

Symfony Choice Type Error Messages

The Symfony ChoiceType is a complex, interesting beast. By far my biggest complaint about it is the error messages shown to the user during validation are not great. Defaulting to, “This value is not valid,” with no help for the user on what values are actually allowed.

That’s okay for plain HTML interfaces where the form field is rendered as a select input or a set of choice/radio fields. But using the choice type in an API endpoint means that error message is a lot less useful for end users.

How ChoiceTypes Errors

Interestingly, with the validator form extension the ChoiceType always shows as valid when a form is submitted.

Symfony has the concept of form validity and form synchronization. A form may be valid (have no errors), but not have its data (the stuff passed to Form::submit or Form::handleRequest) synchronized with the fields defined in the form (and accessible via Form::getData).

Without the validator extension, ChoiceType fields submitted with invalid values will be seen as valid but not synchronized.

use Symfony\Component\Form\Forms;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;

/** @var FormFactoryInterface $forms */
$forms = Forms::createFormFactory(); // no ValidatorExtension!

$form = $forms->createBuilder()
    ->add('test', ChoiceType::class, [
        'choices' => [
            'One' => 'one',
            'Two' => 'two',
        ],
    ])
    ->getForm();

$form->submit(['test' => 'three']);

var_dump($form->isValid()); // true
var_dump($form->isSynchronized()); // true
var_dump($form['test']->isValid()); // true!
var_dump($form['test']->isSynchronized()); // false
var_dump($form->getData()); // empty array

Compare that to submitting valid data:

// setup as in the previous example

$form->submit(['test' => 'one']);

var_dump($form->isValid()); // true
var_dump($form->isSynchronized()); // true
var_dump($form['test']->isValid()); // true!
var_dump($form['test']->isSynchronized()); // true
var_dump($form->getData()); // ['test' => 'one']

Providing a Better Error Message

Remember that Form::isSynchronized method discussed above? That comes into play in the Form constraint and its constraint validator.

If the ChoiceType field ends up not synchronized, a violation is added with the invalid_message option as its message.

To provide a real, useful error message pass invalid_message to your ChoiceType field. invalid_message_parameters is also useful.

use Symfony\Component\Form\Forms;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
use Symfony\Component\Validator\Validation;

$validator = Validation::createValidator();
$forms = Forms::createFormFactoryBuilder()
    ->addExtension(new ValidatorExtension($validator))
    ->getFormFactory();

$choices = [
    'One' => 'one',
    'Two' => 'two',
];
$form = $forms->createBuilder()
    ->add('test', ChoiceType::class, [
        'choices' => $choices,
        'invalid_message' => '"{{ value }}" is not valid. Valid choices: {{ choices }}.',
        'invalid_message_parameters' => [
            '{{ choices }}' => implode(', ', $choices),
        ],
    ])
    ->getForm();

$form->submit(['test' => 'three']);

var_dump($form->isValid()); // true
// outputs '"three" is not valid. Valid choices: one, two.
foreach ($form->getErrors(true) as $err) {
    echo $err->getMessage(), PHP_EOL;
}