Form Type Extension Magic

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $10.00

The only way to make an option valid for a field is to, well, hack the core class and add it! For example, since funFact is a TextType, I could - if I were feeling crazy - open TextType, scroll down, and hack that help option into configureOptions():

... lines 1 - 16
use Symfony\Component\OptionsResolver\OptionsResolver;
class TextType extends AbstractType implements DataTransformerInterface
{
... lines 21 - 35
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'compound' => false,
));
}
... lines 42 - 67
}

With a few other hacks, we could have this feature working!

Form Plugins: Type Extensions

Obviously, this is not the right path to take, and that's ok, but there is another way to add an option to a field. It's called a form type extension, and it's basic a plugin to the Symfony form field system. By leveraging a form type extension, you can modify any field in the system. You could, I don't know, add a new attribute to literally every single field on your entire site.

Let's find out how.

Creating a Form Type Extension

In your Form directory, create a new directory called TypeExtension and then a new class called HelpFormExtension:

... lines 1 - 2
namespace AppBundle\Form\TypeExtension;
... lines 4 - 6
class HelpFormExtension extends AbstractTypeExtension
{
... lines 9 - 12
}

The goal of this class will be to to allow for a help option to be passed to any field, and to turn that help option into a help variable.

First, all form type extensions should extend AbstractTypeExtension:

... lines 1 - 2
namespace AppBundle\Form\TypeExtension;
use Symfony\Component\Form\AbstractTypeExtension;
class HelpFormExtension extends AbstractTypeExtension
{
... lines 9 - 12
}

Next, use the "Code"->"Generate" menu, or Command+N on a Mac, and click "Implement Methods". This abstract class requires us to have one method: getExtendedType():

... lines 1 - 2
namespace AppBundle\Form\TypeExtension;
use Symfony\Component\Form\AbstractTypeExtension;
class HelpFormExtension extends AbstractTypeExtension
{
public function getExtendedType()
{
// TODO: Implement getExtendedType() method.
}
}

You see, when you create a form type extension, you could make it modify every field in your entire system, or just one type, like the FileType. To modify every field, return FormType::class:

... lines 1 - 5
use Symfony\Component\Form\Extension\Core\Type\FormType;
... lines 7 - 9
class HelpFormExtension extends AbstractTypeExtension
{
... lines 12 - 16
public function getExtendedType()
{
return FormType::class;
}
}

Because remember, FormType is the parent for all fields.

Tip

Technically, FormType is the parent type for all fields, except for buttons. But I don't like adding buttons to my form anyways!

Overriding Field Behavior

Here's the cool thing about these classes: they have all the same functions as a normal form class, like buildForm() or configureOptions(). The difference is that whatever modifications we make to this class will literally be applied to every field in the system.

For example, go back to the "Code"->"Generate" menu, click "Override Methods", then select buildView():

... lines 1 - 6
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
class HelpFormExtension extends AbstractTypeExtension
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
... line 14
}
... lines 16 - 20
}

When we're done setting things up, whenever any field is transformed into a FormView object, this method will be called and we will be able to add variables to anything!

Try it: add $view - which represents whatever one field is being setup - $view->vars['help'] set to TURTLES!

... lines 1 - 9
class HelpFormExtension extends AbstractTypeExtension
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['help'] = 'TURTLES!';
}
... lines 16 - 20
}

That's a ridiculous, and yet, fully-functional type extension.

Registering a Type Extension

To tell Symfony about this, you guys can probably guess, we need to register this as a service. In app/config/services.yml, add a new service: call it app.form.help_form_extension. Set its class to HelpFormExtension and then I'll set autowire to true... even though the class doesn't have any constructor arguments, at least not yet:

... lines 1 - 5
services:
... lines 7 - 27
app.form.help_form_extenion:
class: AppBundle\Form\TypeExtension\HelpFormExtension
autowire: true
... lines 31 - 33

Then, to actually tell Symfony: "Hey! This is a form type extension!", add a tag, set to form.type_extension. Also give this an extended_type option. This needs to match whatever you're returning from getExtendedType(). FormType::class returns the long string in the use statement, so copy that, and paste it into your service:

... lines 1 - 5
services:
... lines 7 - 27
app.form.help_form_extenion:
class: AppBundle\Form\TypeExtension\HelpFormExtension
autowire: true
tags:
- { name: form.type_extension, extended_type: Symfony\Component\Form\Extension\Core\Type\FormType }

That's it team! Temporarily remove the help option from GenusFormType, ya know, so the page doesn't explode. Then, refresh! OMG, everyone is screaming about TURTLES! Well, everyone except for the isPublished field, because we're overriding that help variable at the last possible second: from inside the template.

Using the Option to Fuel the help Variable

Finally, uncomment the help option. So, how can we make this a valid option? Go back to HelpFormExtension, use the "Code"->"Generate" menu one last time, click "Override Methods", and select configureOptions():

... lines 1 - 8
use Symfony\Component\OptionsResolver\OptionsResolver;
class HelpFormExtension extends AbstractTypeExtension
{
... lines 13 - 19
public function configureOptions(OptionsResolver $resolver)
{
... line 22
}
... lines 24 - 28
}

Our job here is so simple: $resolver->setDefault('help', null):

... lines 1 - 10
class HelpFormExtension extends AbstractTypeExtension
{
... lines 13 - 19
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('help', null);
}
... lines 24 - 28
}

Just by doing that, you are now allowed to have a help option on any field. It also means that when buildView() is called, the $options array will have a key called help. All we need to say is if $options['help'], then set the help variable to $options['help']:

... lines 1 - 10
class HelpFormExtension extends AbstractTypeExtension
{
public function buildView(FormView $view, FormInterface $form, array $options)
{
if ($options['help']) {
$view->vars['help'] = $options['help'];
}
}
... lines 19 - 28
}

And that takes care of it. Try this puppy out.

And consider yourself very, very dangerous.

Leave a comment!

This tutorial is built on Symfony 3 but form theming hasn't changed much in Symfony 4 and Symfony 5. Other than some path differences - this tutorial should work fine.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.5.9",
        "symfony/symfony": "3.1.*", // v3.1.4
        "doctrine/orm": "^2.5", // v2.7.2
        "doctrine/doctrine-bundle": "^1.6", // 1.6.4
        "doctrine/doctrine-cache-bundle": "^1.2", // 1.3.0
        "symfony/swiftmailer-bundle": "^2.3", // v2.3.11
        "symfony/monolog-bundle": "^2.8", // 2.11.1
        "symfony/polyfill-apcu": "^1.0", // v1.2.0
        "sensio/distribution-bundle": "^5.0", // v5.0.22
        "sensio/framework-extra-bundle": "^3.0.2", // v3.0.16
        "incenteev/composer-parameter-handler": "^2.0", // v2.1.2
        "knplabs/knp-markdown-bundle": "^1.4", // 1.4.2
        "doctrine/doctrine-migrations-bundle": "^1.1", // 1.1.1
        "stof/doctrine-extensions-bundle": "^1.2" // v1.2.2
    },
    "require-dev": {
        "sensio/generator-bundle": "^3.0", // v3.0.7
        "symfony/phpunit-bridge": "^3.0", // v3.1.3
        "nelmio/alice": "^2.1", // 2.1.4
        "doctrine/doctrine-fixtures-bundle": "^2.3" // 2.3.0
    }
}