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.