How to Use Data Transformers

How to Use Data Transformers

Data transformers are used to translate the data for a field into a format that can be displayed in a form (and back on submit). They’re already used internally for many field types. For example, the DateType field can be rendered as a yyyy-MM-dd-formatted input text box. Internally, a data transformer converts the starting DateTime value of the field into the yyyy-MM-dd string to render the form, and then back into a DateTime object on submit.

Caution

When a form field has the inherit_data option set to true, data transformers are not applied to that field.

See also

If, instead of transforming the representation of a value, you need to map values to a form field and back, you should use a data mapper. Check out When and How to Use Data Mappers.

Example #1: Transforming Strings Form Data Tags from User Input to an Array

Suppose you have a Task form with a tags text type:

  1. // src/Form/Type/TaskType.php
  2. namespace App\Form\Type;
  3. use App\Entity\Task;
  4. use Symfony\Component\Form\Extension\Core\Type\TextType;
  5. use Symfony\Component\Form\FormBuilderInterface;
  6. use Symfony\Component\OptionsResolver\OptionsResolver;
  7. // ...
  8. class TaskType extends AbstractType
  9. {
  10. public function buildForm(FormBuilderInterface $builder, array $options): void
  11. {
  12. $builder->add('tags', TextType::class);
  13. }
  14. public function configureOptions(OptionsResolver $resolver): void
  15. {
  16. $resolver->setDefaults([
  17. 'data_class' => Task::class,
  18. ]);
  19. }
  20. // ...
  21. }

Internally the tags are stored as an array, but displayed to the user as a plain comma separated string to make them easier to edit.

This is a perfect time to attach a custom data transformer to the tags field. The easiest way to do this is with the Symfony\Component\Form\CallbackTransformer class:

  1. // src/Form/Type/TaskType.php
  2. namespace App\Form\Type;
  3. use Symfony\Component\Form\CallbackTransformer;
  4. use Symfony\Component\Form\Extension\Core\Type\TextType;
  5. use Symfony\Component\Form\FormBuilderInterface;
  6. // ...
  7. class TaskType extends AbstractType
  8. {
  9. public function buildForm(FormBuilderInterface $builder, array $options): void
  10. {
  11. $builder->add('tags', TextType::class);
  12. $builder->get('tags')
  13. ->addModelTransformer(new CallbackTransformer(
  14. function ($tagsAsArray) {
  15. // transform the array to a string
  16. return implode(', ', $tagsAsArray);
  17. },
  18. function ($tagsAsString) {
  19. // transform the string back to an array
  20. return explode(', ', $tagsAsString);
  21. }
  22. ))
  23. ;
  24. }
  25. // ...
  26. }

The CallbackTransformer takes two callback functions as arguments. The first transforms the original value into a format that’ll be used to render the field. The second does the reverse: it transforms the submitted value back into the format you’ll use in your code.

Tip

The addModelTransformer() method accepts any object that implements Symfony\Component\Form\DataTransformerInterface - so you can create your own classes, instead of putting all the logic in the form (see the next section).

You can also add the transformer, right when adding the field by changing the format slightly:

  1. use Symfony\Component\Form\Extension\Core\Type\TextType;
  2. $builder->add(
  3. $builder
  4. ->create('tags', TextType::class)
  5. ->addModelTransformer(...)
  6. );

Example #2: Transforming an Issue Number into an Issue Entity

Say you have a many-to-one relation from the Task entity to an Issue entity (i.e. each Task has an optional foreign key to its related Issue). Adding a list box with all possible issues could eventually get really long and take a long time to load. Instead, you decide you want to add a text box, where the user can enter the issue number.

Start by setting up the text field like normal:

  1. // src/Form/Type/TaskType.php
  2. namespace App\Form\Type;
  3. use App\Entity\Task;
  4. use Symfony\Component\Form\Extension\Core\Type\TextareaType;
  5. use Symfony\Component\Form\Extension\Core\Type\TextType;
  6. // ...
  7. class TaskType extends AbstractType
  8. {
  9. public function buildForm(FormBuilderInterface $builder, array $options): void
  10. {
  11. $builder
  12. ->add('description', TextareaType::class)
  13. ->add('issue', TextType::class)
  14. ;
  15. }
  16. public function configureOptions(OptionsResolver $resolver): void
  17. {
  18. $resolver->setDefaults([
  19. 'data_class' => Task::class,
  20. ]);
  21. }
  22. // ...
  23. }

Good start! But if you stopped here and submitted the form, the Task’s issue property would be a string (e.g. “55”). How can you transform this into an Issue entity on submit?

Creating the Transformer

You could use the CallbackTransformer like earlier. But since this is a bit more complex, creating a new transformer class will keep the TaskType form class simpler.

Create an IssueToNumberTransformer class: it will be responsible for converting to and from the issue number and the Issue object:

  1. // src/Form/DataTransformer/IssueToNumberTransformer.php
  2. namespace App\Form\DataTransformer;
  3. use App\Entity\Issue;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use Symfony\Component\Form\DataTransformerInterface;
  6. use Symfony\Component\Form\Exception\TransformationFailedException;
  7. class IssueToNumberTransformer implements DataTransformerInterface
  8. {
  9. private $entityManager;
  10. public function __construct(EntityManagerInterface $entityManager)
  11. {
  12. $this->entityManager = $entityManager;
  13. }
  14. /**
  15. * Transforms an object (issue) to a string (number).
  16. *
  17. * @param Issue|null $issue
  18. */
  19. public function transform($issue): string
  20. {
  21. if (null === $issue) {
  22. return '';
  23. }
  24. return $issue->getId();
  25. }
  26. /**
  27. * Transforms a string (number) to an object (issue).
  28. *
  29. * @param string $issueNumber
  30. * @throws TransformationFailedException if object (issue) is not found.
  31. */
  32. public function reverseTransform($issueNumber): ?Issue
  33. {
  34. // no issue number? It's optional, so that's ok
  35. if (!$issueNumber) {
  36. return null;
  37. }
  38. $issue = $this->entityManager
  39. ->getRepository(Issue::class)
  40. // query for the issue with this id
  41. ->find($issueNumber)
  42. ;
  43. if (null === $issue) {
  44. // causes a validation error
  45. // this message is not shown to the user
  46. // see the invalid_message option
  47. throw new TransformationFailedException(sprintf(
  48. 'An issue with number "%s" does not exist!',
  49. $issueNumber
  50. ));
  51. }
  52. return $issue;
  53. }
  54. }

Like the first example, the transformer has two directions. The transform() method is responsible for converting the data used in your code to a format that can be rendered in your form (e.g. an Issue object to its id, a string). The reverseTransform() method does the reverse: it converts the submitted value back into the format you want (e.g. convert the id back to the Issue object).

To cause a validation error, throw a Symfony\Component\Form\Exception\TransformationFailedException. But the message you pass to this exception won’t be shown to the user. You’ll set that message with the invalid_message option (see below).

Note

When null is passed to the transform() method, your transformer should return an equivalent value of the type it is transforming to (e.g. an empty string, 0 for integers or 0.0 for floats).

Using the Transformer

Next, you need to use the IssueToNumberTransformer object inside of TaskType and add it to the issue field. No problem! Add a __construct() method and type-hint the new class:

  1. // src/Form/Type/TaskType.php
  2. namespace App\Form\Type;
  3. use App\Form\DataTransformer\IssueToNumberTransformer;
  4. use Symfony\Component\Form\Extension\Core\Type\TextareaType;
  5. use Symfony\Component\Form\Extension\Core\Type\TextType;
  6. // ...
  7. class TaskType extends AbstractType
  8. {
  9. private $transformer;
  10. public function __construct(IssueToNumberTransformer $transformer)
  11. {
  12. $this->transformer = $transformer;
  13. }
  14. public function buildForm(FormBuilderInterface $builder, array $options): void
  15. {
  16. $builder
  17. ->add('description', TextareaType::class)
  18. ->add('issue', TextType::class, [
  19. // validation message if the data transformer fails
  20. 'invalid_message' => 'That is not a valid issue number',
  21. ]);
  22. // ...
  23. $builder->get('issue')
  24. ->addModelTransformer($this->transformer);
  25. }
  26. // ...
  27. }

Whenever the transformer throws an exception, the invalid_message is shown to the user. Instead of showing the same message every time, you can set the end-user error message in the data transformer using the setInvalidMessage() method. It also allows you to include user values:

  1. // src/Form/DataTransformer/IssueToNumberTransformer.php
  2. namespace App\Form\DataTransformer;
  3. use Symfony\Component\Form\DataTransformerInterface;
  4. use Symfony\Component\Form\Exception\TransformationFailedException;
  5. class IssueToNumberTransformer implements DataTransformerInterface
  6. {
  7. // ...
  8. public function reverseTransform($issueNumber): ?Issue
  9. {
  10. // ...
  11. if (null === $issue) {
  12. $privateErrorMessage = sprintf('An issue with number "%s" does not exist!', $issueNumber);
  13. $publicErrorMessage = 'The given "{{ value }}" value is not a valid issue number.';
  14. $failure = new TransformationFailedException($privateErrorMessage);
  15. $failure->setInvalidMessage($publicErrorMessage, [
  16. '{{ value }}' => $issueNumber,
  17. ]);
  18. throw $failure;
  19. }
  20. return $issue;
  21. }
  22. }

New in version 4.3: The setInvalidMessage() method was introduced in Symfony 4.3.

That’s it! If you’re using the default services.yaml configuration, Symfony will automatically know to pass your TaskType an instance of the IssueToNumberTransformer thanks to autowire and autoconfigure. Otherwise, register the form class as a service and tag it with the form.type tag.

Now, you can use your TaskType:

  1. // e.g. somewhere in a controller
  2. $form = $this->createForm(TaskType::class, $task);
  3. // ...

Cool, you’re done! Your user will be able to enter an issue number into the text field, which will be transformed back into an Issue object. This means that, after a successful submission, the Form component will pass a real Issue object to Task::setIssue() instead of the issue number.

If the issue isn’t found, a form error will be created for that field and its error message can be controlled with the invalid_message field option.

Caution

Be careful when adding your transformers. For example, the following is wrong, as the transformer would be applied to the entire form, instead of just this field:

  1. // THIS IS WRONG - TRANSFORMER WILL BE APPLIED TO THE ENTIRE FORM
  2. // see above example for correct code
  3. $builder->add('issue', TextType::class)
  4. ->addModelTransformer($transformer);

Creating a Reusable issue_selector Field

In the above example, you applied the transformer to a normal text field. But if you do this transformation a lot, it might be better to create a custom field type. that does this automatically.

First, create the custom field type class:

  1. // src/Form/IssueSelectorType.php
  2. namespace App\Form;
  3. use App\Form\DataTransformer\IssueToNumberTransformer;
  4. use Symfony\Component\Form\AbstractType;
  5. use Symfony\Component\Form\Extension\Core\Type\TextType;
  6. use Symfony\Component\Form\FormBuilderInterface;
  7. use Symfony\Component\OptionsResolver\OptionsResolver;
  8. class IssueSelectorType extends AbstractType
  9. {
  10. private $transformer;
  11. public function __construct(IssueToNumberTransformer $transformer)
  12. {
  13. $this->transformer = $transformer;
  14. }
  15. public function buildForm(FormBuilderInterface $builder, array $options): void
  16. {
  17. $builder->addModelTransformer($this->transformer);
  18. }
  19. public function configureOptions(OptionsResolver $resolver): void
  20. {
  21. $resolver->setDefaults([
  22. 'invalid_message' => 'The selected issue does not exist',
  23. ]);
  24. }
  25. public function getParent(): string
  26. {
  27. return TextType::class;
  28. }
  29. }

Great! This will act and render like a text field (getParent()), but will automatically have the data transformer and a nice default value for the invalid_message option.

As long as you’re using autowire and autoconfigure, you can start using the form immediately:

  1. // src/Form/Type/TaskType.php
  2. namespace App\Form\Type;
  3. use App\Form\DataTransformer\IssueToNumberTransformer;
  4. use Symfony\Component\Form\Extension\Core\Type\TextareaType;
  5. // ...
  6. class TaskType extends AbstractType
  7. {
  8. public function buildForm(FormBuilderInterface $builder, array $options): void
  9. {
  10. $builder
  11. ->add('description', TextareaType::class)
  12. ->add('issue', IssueSelectorType::class)
  13. ;
  14. }
  15. // ...
  16. }

Tip

If you’re not using autowire and autoconfigure, see How to Create a Custom Form Field Type for how to configure your new IssueSelectorType.

About Model and View Transformers

In the above example, the transformer was used as a “model” transformer. In fact, there are two different types of transformers and three different types of underlying data.

../_images/data-transformer-types.png

In any form, the three different types of data are:

  1. Model data - This is the data in the format used in your application (e.g. an Issue object). If you call Form::getData() or Form::setData(), you’re dealing with the “model” data.
  2. Norm Data - This is a normalized version of your data and is commonly the same as your “model” data (though not in our example). It’s not commonly used directly.
  3. View Data - This is the format that’s used to fill in the form fields themselves. It’s also the format in which the user will submit the data. When you call Form::submit($data), the $data is in the “view” data format.

The two different types of transformers help convert to and from each of these types of data:

Model transformers:

  • transform(): “model data” => “norm data”
  • reverseTransform(): “norm data” => “model data”

View transformers:

  • transform(): “norm data” => “view data”
  • reverseTransform(): “view data” => “norm data”

Which transformer you need depends on your situation.

To use the view transformer, call addViewTransformer().

So why Use the Model Transformer?

In this example, the field is a text field, and a text field is always expected to be a simple, scalar format in the “norm” and “view” formats. For this reason, the most appropriate transformer was the “model” transformer (which converts to/from the norm format - string issue number - to the model format - Issue object).

The difference between the transformers is subtle and you should always think about what the “norm” data for a field should really be. For example, the “norm” data for a text field is a string, but is a DateTime object for a date field.

Tip

As a general rule, the normalized data should contain as much information as possible.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.