Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Form Type Extension

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 $12.00

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

Login Subscribe

Symfony's form system has a feature that gives us the massive power to modify any form or any field across our entire app! Woh! It's called "form type extensions" and working with them is super fun.

To see how this works, let's talk about the textarea field. Forget about Symfony for a moment. In HTML land, one of the features of the textarea element is that you can give it a rows attribute. If you set rows="10", it gets longer.

If we wanted to set that attribute in Symfony, we could, of course, pass an attr option with rows set to some value. But, here's the real question: could we automatically set that option for every textarea across our entire app? Absolutely! We can do anything!

Creating the Form Type Extension

In your Form/ directory, create a new directory called TypeExtension, then inside a class called TextareaSizeExtension. Make this implement FormTypeExtensionInterface. As the name implies, this will allow us to extend existing form types.

... lines 1 - 6
use Symfony\Component\Form\FormTypeExtensionInterface;
... lines 8 - 10
class TextareaSizeExtension implements FormTypeExtensionInterface
{
... lines 13 - 36
}

Next, go to the Code -> Generate menu, or Command+N on a Mac, and choose "Implement Methods" to implement everything we need. Woh! We know these methods! These are almost the exact same methods that we've been implementing in our form type classes! And... that's on purpose! These methods work pretty much the same way.

... lines 1 - 10
class TextareaSizeExtension implements FormTypeExtensionInterface
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
// TODO: Implement buildForm() method.
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
// TODO: Implement buildView() method.
}
public function finishView(FormView $view, FormInterface $form, array $options)
{
// TODO: Implement finishView() method.
}
public function configureOptions(OptionsResolver $resolver)
{
// TODO: Implement configureOptions() method.
}
public function getExtendedType()
{
// TODO: Implement getExtendedType() method.
}
}

Registering the Form Type Extension

The only new method is getExtendedType() - we'll talk about that in a second. To tell Symfony that this form type extension exists and to tell it that we want to extend the TextareaType, we need a little bit of config. This might look confusing at first. Let's code it up, then I'll explain.

Open config/services.yaml. And, at the bottom, we need to give our service a "tag". First, put the form class and below, add tags. The syntax here is a bit ugly: add a dash, open an array and set name to form.type_extension. Then I'll create a new line for my own sanity and add one more option extended_type. We need to set this to the form type class that we want to extend - so TextareaType. Let's cheat real quick: I'll use TextareaType, auto-complete that, copy the class, then delete that. Go paste it in the config. Oh, and I forgot my comma!

... lines 1 - 6
services:
... lines 8 - 38
App\Form\TypeExtension\TextareaSizeExtension:
tags:
- { name: form.type_extension,
extended_type: Symfony\Component\Form\Extension\Core\Type\TextareaType }

As soon as we do this, every time a TextareaType is created in the system, every method on our TextareaSizeExtension will be called. It's almost as if each of these methods actually lives inside of the TextareaType class! If we add some code to buildForm(), it's pretty much identical to opening up the TextareaType class and adding code right there!

The form.type_extension Tag & autoconfigure

Now, two important things. If you're using Symfony 4.2, then you do not need to add any of this code in services.yaml. Whenever you need to "plug into" some part of Symfony, internally, you do that by registering a service and giving it a "tag". The form.type_extension tag says:

Hey Symfony! This isn't just a normal service! It's a form type extension! So make sure you use it for that!

But these days, you don't see "tags" much in Symfony. The reason is simple: for most things, Symfony looks at the interfaces that your service implements, and adds the correct tags automatically. In Symfony 4.1 and earlier, this does not happen for the FormTypeExtensionInterface. But in Symfony 4.2... it does! So, no config needed... at all.

But then, how does Symfony know which form type we want to extend in Symfony 4.2? The getExtendedType() method! Inside, return TextareaType::class. And yea, we also need to fill in this method in Symfony 4.1... it's a bit redundant, which is why Symfony 4.2 will be so much cooler.

Tip

Since Symfony 4.2 getExtendedType() method is deprecated in favor of getExtendedTypes() but you still need a dummy implementation of getExtendedType()

public function getExtendedType()
{
   return '';
}

public static function getExtendedTypes(): iterable
{
   return [SomeType::class];
}

... lines 1 - 11
class TextareaSizeExtension implements FormTypeExtensionInterface
... lines 13 - 30
public function getExtendedType()
{
return TextareaType::class;
}
}

Filling in the Form Type Extension

Ok! Let's remove the rest of the TODOs in here and then get to work! We can fill in whichever methods we need. In our case, we want to modify the view variables. That's easy for us: in buildView(), say $view->vars['attr'], and then add a rows attribute equal to 10.

... lines 1 - 17
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['attr']['rows'] = 10;
}
... lines 22 - 36

Done! Move over, refresh and... yea! I think it's bigger! Inspect it - yes: rows="10". Every <textarea> on our entire site will now have this.

Modifying "Every" Field?

By the way, instead of modifying just one field type, sometimes you may want to modify literally every field type. To do that, you can choose to extend FormType::class. That works because of the form field inheritance system. All field types ultimately extend FormType::class, except for a ButtonType that I don't usually use anyways. So if you override FormType, you can modify everything. Just keep in mind that this will also include your entire form classes, like ArticleFormType.

Adding a new Field Option

But wait, there's more! Instead of hardcoding 10, could we make it possible to configure this value each time you use the TextareaType? Why, of course! In ArticleFormType, pass null to the content field so it keeps guessing it. Then add a new option: rows set to 15.

... lines 1 - 14
class ArticleFormType extends AbstractType
{
... lines 17 - 23
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 27 - 29
->add('content', null, [
'rows' => 15
])
... lines 33 - 36
;
}
... lines 39 - 45
}

Try this out - refresh! Giant error!

The option "rows" does not exist

It turns out that you can't just "invent" new options and pass them: each field has a concrete set of valid options. But, in TextareaSizeExtension, we can invent new options. Do it down in configureOptions(): add $resolver->setDefaults() and invent a new rows option with a default value of 10.

... lines 1 - 11
class TextareaSizeExtension implements FormTypeExtensionInterface
{
... lines 14 - 26
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'rows' => 10
]);
}
... lines 33 - 37
}

Now, up in buildView(), notice that almost every method is passed the final array of $options for this field. Set the rows attribute to $options['rows'].

... lines 1 - 17
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['attr']['rows'] = $options['rows'];
}
... lines 22 - 39

Done. The rows will default to 10, but we can override that via a brand, new shiny form field option. Try it! Refresh, inspect the textarea and... yes! The rows attribute is set to 15.

How CSRF Protection Works

This is the power of form type extensions. And these are even used in the core of Symfony to do some cool stuff. For example, remember how every form automatically has an _token CSRF token field? How does Symfony magically add that? The answer: a form type extension. Press Shift+Shift and look for a class called FormTypeCsrfExtension.

Cool! It extends an AbstractTypeExtension class, which implements the same FormTypeExtensionInterface but prevents you from needing to override every method. We also could have used this same class.

Anyways, in buildForm() it adds an "event listener", which activates some code that will validate the _token field when we submit. We'll talk about events in a little while.

In finishView() - which is very similar to buildView() - it adds a few variables to help render that hidden field. And finally, in configureOptions(), it adds some options that allow us to control things. For example, inside the configureOptions() method of any form class - like ArticleFormType - we could set a csrf_protection option to false to disable the CSRF token.

Next: how could we make our form look or act differently based on the data passed to it? Like, how could we make the author field disabled, only on the edit form? Let's find out!

Leave a comment!

20
Login or Register to join the conversation
Adam M. Avatar
Adam M. Avatar Adam M. | posted 4 years ago | edited

For 4.3+ versions of Symfony..


namespace App\Form\TypeExtension;

use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;

class TextareaSizeExtension extends AbstractTypeExtension
{

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        
    }

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        $view->vars['attr']['rows'] = $options['rows'];
    }

    public function finishView(FormView $view, FormInterface $form, array $options)
    {
        
    }

    public function configureOptions(OptionsResolver $resolver)
    {
       $resolver->setDefaults([
           'rows' => 10
       ]) ;
    }
    public static function getExtendedTypes(): iterable
    {
        return [TextareaType::class];
    }

}

Also, <b>do not</b> copy this code from the scripts page under the "Tip" section, as it has a typo..


public function getExtendedType()
{
   return '';
}

public function getExtentedTypes(): iterable
{
   return [SomeType::class];
}

<i>Notice the 't' in getExten<b>t</b>edTypes()</i>

1 Reply

Hey Adam!

Ah, great catch! Thank you for reporting it! It was fixed in: https://github.com/knpunive... , so now people may copy code from the Tip :)

Cheers!

Reply
Lijana Z. Avatar
Lijana Z. Avatar Lijana Z. | posted 3 years ago

Are there really no other way to add new option besides creating extension type? It looks way too much work for such simple thing. I have got error "The option "x" does not exist." many times and it was really wtf moments - I used to end up to find some different solution but not creating an extension class.

If there is no other way, do you know why it is made that way? What is the point of this? If so I wish the error message would be more helpful - by telling how to solve - to create a form extension and give a link to the documentation or this tutorial for example. It would same so much time for many developers. There are lot of stack overflow questions, so it really shows there is a big problem. I saw SO question from 2015 so 4-5 years and nobody said to create a form extension over so much time.

After watching next video - I saw you are inventing new option without creating an extension. So looks like there is a way, but now I guess the reason we here needed an extension is that because the TextAreaFormType is vendor class which we cannot modify.

Reply

Yo Lijana Z.!

Excellent question. The answer is.... it depends on what you are trying to do ;). Let's look at the error:

The option "x" does not exist

There are 3 different situations where you might get this error:

A) You might get this error if you simply have a "typo" on an option name - e.g. you use requured instead of required. In that case, you definitely don't want to create a form type extension - you just want to fix your typo. That's something I like a lot about the "options" system: you can't typo something and have it "silently" fail: you will get a clear error that the option does not exist.

B) You might get this error in a situation where you are trying to pass a custom option to a "form type" class that you created. For example, suppose you have a "RegistrationFormType" that you use in 2 different pages on your site. On one page, you DO want a "subscribe to newsletter" checkbox but on another screen (for some reason) you want the same form but without that checkbox. One way to do that is to "invent" a new option that you pass when creating your RegistrationFormType in the controller - e.g. 'with_newsletter' => true. In this case, you are "inventing" an option. But you do not need a form type extension if you just want to add an option to one form type (and also if that form type is your class). Instead, you create the option by adding it to the configureOptions section of your RegistrationFormType.

C) You might get this error in a situation where you are trying to pass a custom option to a "field" that you did not create - e.g. TextareaType. OR because you want to be able to pass an option to any (or at least many) different fields. This is where you need a form type extension: because you want to add a custom option to a form field that you did not create (something in vendor/) or because you want to add a custom option to many/all form fields in your system.

Creating a form type extension is really a pretty rare thing to need to do - it's very powerful, but it's not that common that you need to add a custom option to a vendor "type" or to "all" types. The options system in general can be a confusing, so let me know if this helps... or just make things more confusing ;).

Cheers!

Reply
Lijana Z. Avatar

Thanks, it helps. I hope those videos and comments will not be gone while symfony will be used just in case I need to read/watch again :)

Reply
Stephan Avatar
Stephan Avatar Stephan | posted 3 years ago

Hi,

I have this error after developing the TextareaSizeExtension class and configuration the services.yaml file:


The extended type
specified for the service "0" does not match the actual extended type.
Expected "Symfony\Component\Form\Extension\Core\Type\TextareaType",
given "App\Form\TypeExtension\TextareaType".


How can I solve that please?

Reply

Hey @stephansav!

Hmm. My guess is that you may have an indentation problem in your services.yaml file. Any extra (or missing) indentation will have a different "meaning" when it's parsed - so look very closely at your indentation levels and compare them with mine :). For example, I think you might have a - that is maybe only indented 2 or 4 spaces from the root of the file (so 2 or 4 spaces in total) and this is looking like an array with a "0" index under "services" to Symfony.

Let me know if that helps!

Cheers!

Reply
Stephan Avatar
Stephan Avatar Stephan | weaverryan | posted 3 years ago | edited

Hi,

I checked what you told me and there is no problem with the indentation in services.yaml. Here my code:


App\Form\TypeExtension\TextareaSizeExtension:
    tags:
        - { name: form.type_extension,
            extended_type: Symfony\Component\Form\Extension\Core\Type\TextareaType }

How can I solve that?

Reply

Hey @stephansav!

Ah, thanks for posting this! I mis-read the error completely! I think you might be missing a "use" statement for the TextareaType inside your TextareaSizeExtension. Let me know if that's it :).

Cheers!

Reply
Stephan Avatar

Hi,
that's it. Thank you very much! :)

Reply
Christoph Avatar
Christoph Avatar Christoph | posted 4 years ago

Hello
I am using 4.3
i extended the example and tried to play around:
to disable ALL fields (just for testing)

public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['attr']['rows'] = 10;
$view->vars['attr']['disabled'] = true;
}

public static function getExtendedTypes() :iterable
{
return [
FormType::class,
TextareaType::class
];
}

I expected all fields to be disabled and have a rows attribute set to 10
But only the textarea had rows=10 and the disabled attribute set

When I CHANGED the NAME of the class (and corresponding the name of the file for autoloader) from TextareaSizeExtension (like in the example) to something else: BlubbiSizeExtension.
It worked!
There must be some very weird "magic" in symfony which detects from the NAME of the class, what i COULD HAVE meant and blocks everything else.
Just in case, someone else wants to add something to an extension.
Dont use the class name given in the examples.

IF u use the name: TextareaSizeExtension, nothing else will work. Only textarea-related things.

Greetings
Chris

Reply

Hey Christoph

That's odd, it should have detected your extension, the name of the file (IIRC) doesn't do anything. Symfony calls the getExtendedTypes method in order to detect on which field types the extension should be applied. I think you just had to clear your cache. If you rename the extension to its original name, does the problem re-appears?

Cheers!

Reply
akincer Avatar
akincer Avatar akincer | posted 4 years ago | edited

I'm afraid everything you said about 4.2 does NOT hold true on my 4.2.1 installation.

Setting getExtendedAreaType to static and adding the "s" on the end gave this area:

FatalErrorException<br />Error: Class App\Form\TypeExtension\TextareaSizeExtension contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (Symfony\Component\Form\FormTypeExtensionInterface::getExtendedType)

After changing it back to the way it was, I got this error:

InvalidArgumentException<br />"form.type_extension" tagged services have to implement the static getExtendedTypes() method. The class for service "App\Form\TypeExtension\TextareaSizeExtension" does not implement it.

After adding the config bit in services.yaml that you explicitly say isn't necessary, all errors go away. Am I missing something?

Reply

Hey @Aaron!

Hmm, I think our description of this might be inaccurate indeed. Because, in this video, we're implementing the interface directly, I believe you will need both methods until Symfony 5. However, the getExtendedType should not actually be called anymore - you need to have it to make the interface happy, but it can be blank. If you implement the public static function getExtendedTypes() method, then you should not need the config.

So, have both methods, leave getExtendedType blank, and add the new "s" version (as static). Then you should not need the config.

Sorry about the confusion!

Cheers!

1 Reply
Adsasd Avatar

There is a type inside the code block of this page: As you said, getExtendedTypes() as to be declared as a public static function. On SymfonyCast it is declared just as public function. "static" is missing on the code block

Reply

Hey Mike,

Thank you for reporting it! getExtendedTypes() really should be declared as "public *static* function". We'll fix it soon in the tutorial.

Cheers!

Reply

Hello Ryan,

After implement all the method, I have warning of PhpStorm about getExtendedTypes method.

I read on interface FormTypeExtensionInterface that public function getExtendedType() has @deprecated since Symfony 4.2.

Finally, leaving the configurations in the services.yaml file the form type extension works but if I delete the configurations there is error.

Can you enlighten me. Thank in advance.

Reply

Hey Stephane

Thanks for highlighting this new change. Have you tried to implement getExtendedTypes method (notice an ending S on the method name) and then remove the configuration? I believe it should work

Cheers!

Reply
Fouad B. Avatar

Hey Diego,

This solution does not work on Symfony 4.2.1, it only works when we code as if we are using Symfony 4.1 in a Symfony 4.2.1 environment. Is there a reason for this?

////UPDATE --> 15Min Later...\\\

The caveat here is that the static function "getExtendedTypes" (yes I noticed the 'S' at the end) should be returning an Array.

SO for everyone who is running into trouble with this part here is the complete code:

vars['attr']['rows'] = 3;
}

public function finishView(FormView $view, FormInterface $form, array $options)
{
}

public function configureOptions(OptionsResolver $resolver)
{
}

public function getExtendedType()
{
}

public static function getExtendedTypes()
{
return [TextareaType::class];
}

}

Reply

Oh, yes, you are totally right. I forgot to mention that it must return an array (or been more precise, an iterable) now

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.1
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.6
        "symfony/console": "^4.0", // v4.1.6
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/form": "^4.0", // v4.1.6
        "symfony/framework-bundle": "^4.0", // v4.1.6
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.6
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.6
        "symfony/validator": "^4.0", // v4.1.6
        "symfony/web-server-bundle": "^4.0", // v4.1.6
        "symfony/yaml": "^4.0", // v4.1.6
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.6
        "symfony/dotenv": "^4.0", // v4.1.6
        "symfony/maker-bundle": "^1.0", // v1.8.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.6
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.6
    }
}
userVoice