Symfony 4 Forms: Build, Render & Conquer!


Leveraging Custom Field Options

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Our UserSelectTextType field work great! I've been high-fiving people all day about this! But now, imagine that you want to use this field on multiple forms in your app. That part is easy. Here's the catch: on some forms, we want to allow the email address of any user to be entered. But on other forms, we need to use a custom query: we only want to allow some users to be entered - maybe only admin users.

To make this possible, our field needs to be more flexible: instead of looking for any User with this email, we need to be able to customize this query each time we use the field.

Adding a finderCallback Option

Let's start inside the transformer first. How about this: add a new argument to the constructor a callable argument called $finderCallback. Hit the normal Alt+Enter to create that property and set it.

... lines 1 - 9
class EmailToUserTransformer implements DataTransformerInterface
... line 12
private $finderCallback;
... line 14
public function __construct(UserRepository $userRepository, callable $finderCallback)
... line 17
$this->finderCallback = $finderCallback;
... lines 20 - 48

Here's the idea: whoever instantiates this transformer will pass in a callback that's responsible for querying for the User. Down below, instead of fetching it directly, say $callback = $this->finderCallback and then, $user = $callback(). For convenience, let's pass the function $this->userRepository. And of course, it will need the $value that was just submitted.

... lines 1 - 33
public function reverseTransform($value)
... lines 36 - 39
$callback = $this->finderCallback;
$user = $callback($this->userRepository, $value);
... lines 42 - 47

Cool! We've now made this class a little bit more flexible. But, that doesn't really help us yet. How can we allow this $finderCallback to be customized each time we use this field? By creating a brand new field option.

Check this out: we know that invalid_message is already an option in Symfony and we're changing its default value. But, we can invent new options too! Add a new option called finder_callback and give it a default value: a callback that accepts a UserRepository $userRepository argument and the value - which will be a string $email. Inside return the normal $userRepository->findOneBy() with ['email' => $email].

... lines 1 - 11
class UserSelectTextType extends AbstractType
... lines 14 - 33
public function configureOptions(OptionsResolver $resolver)
... line 37
'finder_callback' => function(UserRepository $userRepository, string $email) {
return $userRepository->findOneBy(['email' => $email]);

Next, check out the build() method. See this array of $options? That will now include finder_callback, which will either be our default value, or some other callback if it was overridden.

Let's break this onto multiple lines and, for the second argument to EmailToUserTransformer, pass $options['finder_callback'].

... lines 1 - 20
public function buildForm(FormBuilderInterface $builder, array $options)
$builder->addModelTransformer(new EmailToUserTransformer(
... lines 28 - 44

Ok! Let's make sure it works. I'll hit enter on the URL to reload the page. Then, change to, submit and... yes! It saves!

The real power of this is that, in ArticleFormType, when we use UserSelectTextType, we can pass a finder_callback option if we need to do a custom query. If we did that, it would override the default value and, when we instantiate EmailToUserTransformer, the second argument would be the callback that we passed from ArticleFormType.

Investigating the Core Field Types

This is how options are used internally by the core Symfony types. Oh, and you probably noticed by now that every field type in Symfony is represented by a normal, PHP class! If you've ever want to know more about how a specific field or option works, just open up the class!

For example, we know that this field is a DateTimeType. Press Shift+Shift and look for DateTimeType - open the one from the Form component. I love it - these classes will look a lot like our own custom field type class! This one has a build() method that adds some transformers. And if you scroll down far enough, cool! Here is the configureOptions() method where all of the valid options are defined for this field.

Want to know how one of these options is used? Copy its name and find out! Search for the with_seconds option. No surprise: it's used in buildForm(). If you looked a little further, you'd see that this is eventually used to configure how the data transformer works.

These core classes are a great way to figure out how to do something advanced or to get inspiration for your own custom field type. Don't' be afraid to dig!

Next: let's hook up some auto-complete JavaScript to this field.

Leave a comment!

  • 2019-01-21 Gabriel Mustiere

    Thank you, the good exception is thrown in.

  • 2019-01-21 Diego Aguiar

    I just sent you a PR! (BTW, I didn't try the code but I'm 99% sure it will fix it)

  • 2019-01-21 Gabriel Mustiere

    Hi Diego Aguiar , thanks for the reply, i added the Valid Assert, same message is returned :/
    You can find my code here

  • 2019-01-21 Diego Aguiar

    Hey Gabriel Mustiere

    About the $email, good catch, you have to allow passing nulls in order to avoid errors. And about the wrong constraint message, I'm not totally sure, I would need to see your code but first try adding this into your Article class

    class Author
    * @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="articles")
    * @ORM\JoinColumn(nullable=false)
    * @Assert\NotNull(message="Please set an author")
    * @Assert\Valid() # Add this line
    private $author;


  • 2019-01-20 Gabriel Mustiere

    I changed the signature of the callable with string $email = null, however it is the invalid_message that is returned instead of the NotNull message, an idea?

  • 2019-01-18 Gabriel Mustiere

    Hi, by adding the callback mechanics to my code, I lost the mechanics of NotNull on the author, the callBack necessarily expects a string in second argument, and if I don't provide any author to my form, an exception is thrown. Thank you.