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; }