08/20/2014
invalid_message: what a failed transformer shows
A data transformer converts between what the form shows and what your
object holds. When it cannot — somebody typed a reference that matches no
record — it throws TransformationFailedException, and Symfony turns
that into a validation error on the field.
The message the user reads comes from the field's invalid_message
option, and its default says nothing useful. Set it where the field is
built:
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Doctrine\ORM\EntityManagerInterface;
class ShortlistChoiceType extends AbstractType
{
public function __construct(private EntityManagerInterface $entityManager)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('reference', TextType::class, [
'invalid_message' => 'No shortlist matches that reference.',
])
->get('reference')
->addModelTransformer(new ReferenceToShortlistTransformer($this->entityManager));
}
}
Why the message has to be set here
The exception the transformer throws is not shown to the user, deliberately
-- it usually says something like "could not find entity with id 4718",
which is an internal detail. invalid_message is the sanctioned way to
say what went wrong in the user's terms, and it is per field, because the
right sentence depends on what the field is for.
You can pass values into it:
'invalid_message' => 'No shortlist matches "{{ value }}".',
{{ value }} is filled in for you. For anything else, use
invalid_message_parameters.
The transformer itself
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class ReferenceToShortlistTransformer implements DataTransformerInterface
{
public function __construct(private EntityManagerInterface $entityManager)
{
}
// object -> what the form field displays
public function transform($shortlist): string
{
return $shortlist?->getReference() ?? '';
}
// what was submitted -> object
public function reverseTransform($reference): ?Shortlist
{
if (!$reference) {
return null;
}
$shortlist = $this->entityManager
->getRepository(Shortlist::class)
->findOneBy(['reference' => $reference]);
if (null === $shortlist) {
throw new TransformationFailedException(sprintf('No shortlist "%s"', $reference));
}
return $shortlist;
}
}
Two things that are easy to get wrong here.
transform() must tolerate null. It runs when the form is first
built, and on a new object the value is null. Returning '' rather than
letting it fatal is the whole of the fix.
Throw from reverseTransform(), do not return null. Returning null
says "the user left it empty", which is a different thing from "what they
typed does not exist" and skips your message entirely.
Model transformer or view transformer?
addModelTransformer() sits between the object and the normalized data;
addViewTransformer() sits between the normalized data and the string in
the input. For "turn a typed reference into an entity", the model
transformer is the right one, which is what the example above uses.
A view transformer is for presentation — formatting a number with separators, say — and putting a database lookup in one means it runs at points you did not intend.