Buy

Symfony 4 Forms: Build, Render & Conquer!

0%
Buy

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)
{
$resolver->setDefaults([
... 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(
$this->userRepository,
$options['finder_callback']
));
}
... lines 28 - 44

Ok! Let's make sure it works. I'll hit enter on the URL to reload the page. Then, change to spacebar2@example.com, 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!