Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Form Theming & Variables

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

We now know that when Symfony renders any part of your form, it looks for a specific block in this core form_div_layout.html.twig template. For example, to render the "row" part of any field, it looks for form_row. We also learned that this system has some hierarchy to it: to render the label part of a TextType field, it first looks for text_label and then falls back to using form_label.

Heck, there is even a form_start block that controls the open form tag!

We used this new knowledge to create our first form theme: we told Twig to look right inside this template for blocks to use when rendering the registration form. Our form_row block is now hooked into the form rendering process.

The Bizarre World of a Form Theme Block

When you're inside of a block that's used by the form theming system... your world is... weird. You really need to pretend like this block doesn't even exist in this template - like it lives all by itself in its own, isolated template. Why? Because these blocks are passed a completely different set of variables that come from the form system: this block doesn't work like any of the other blocks in this template.

I mean, look inside: there is apparently a help variable and a form variable. So, the big question is: when you're in a form theme block, what variables do you have access to?

The easiest answer is just to dump() inside one of these blocks.

... lines 1 - 3
{% block form_row %}
... lines 5 - 9
{{ dump() }}
... lines 11 - 14
{% endblock %}
... lines 16 - 62

Move over and refresh. Woh! Yes - we see giant dumps for each row that's rendered! There's attr, id and full_name. Do these... look familiar? These are the exact variables that we have been overriding when rendering our fields!

Look back at article_admin/_form.html.twig. We learned earlier that there is a variable called label and that the second argument of form_row() is an array of variables that you want to override. You can see this in the docs: when I search for form_row(), the second argument is variables.

Here's the point: when a field is rendered, the form system creates a bunch of variables to help that process, and we can override them. And those variable are ultimately passed... as variables, to your form theme blocks!

For example, remember how we passed a method variable to the form_start() function? Check out the form_start block in the bootstrap theme. Surprise! There is a local method variable that it uses to render. We literally override these variables via the form rendering functions.

The point is: when you're inside a form theme block, you have access to a lot of variables... which is great, because we can use those variables to do, well, whatever we need to!

Adding a label_attr

Back in register.html.twig, remove the dump(). On the old form, each label had an sr-only class. That stands for "screen reader only" and it makes the labels invisible.

How can we make our label tag have this? Hmm. Well, inside our block, we call form_label() and pass in the form object - which represents the form object for whatever field is currently being rendered.

Look back at the form function reference and search for form_label(). Ah yes: the second argument is the label itself. But the third argument is an array of variables! And, apparently, there is a variable called label_attr! If we set that, we can control the attributes on the label tag.

In fact, we can see this: open form_div_layout.html.twig and search for form_label to find that block. There it is! It does some complex processing, but it does use this variable.

Actually, this is a great example of one, not-so-great thing about these templates: they can be crazy complex!

Anyways, back on register.html.twig, let's customize the label attributes! Pass null as the label text so it continues to use whatever the normal label is. Then pass an array with label_attr set to another array, and class equals sr-only.

... lines 1 - 3
{% block form_row %}
... lines 5 - 9
{{- form_label(form, null, {
label_attr: { class: 'sr-only' }
}) -}}
... lines 13 - 15
{% endblock %}
... lines 17 - 63

Phew! Let's try that. Move over refresh and... yes! They're gone! They now have an sr-only class! But, hmm... we now have no idea what these fields are! No worries: that was handled before via a placeholder attribute. New question: how can we set this for each field? Well... it's kind of the same thing: we want a custom attribute on each input.

The form_widget() function is being passed this widget_attr variable as its array of variables. So, we could add an attr key to it! Except... we don't know what the label should be! You might think that we could use the label variable. This does exist, but, unless you set the label explicitly, at this point, it's null. The form_label block holds the logic that turns the field name into a humanized label, if it wasn't set explicitly.

No problem: there's another simple solution. Refactor the form_widget() call into three, separate form_row() calls. Let me close a few files and - that's right! The fields are email plainPassword and agreeTerms. Use .email, copy those, paste twice, then plainPassword and agreeTerms.

For email pass a second argument with attr then placeholder set to Email. Do the same thing for the one other text field: placeholder set to "Password".

... lines 1 - 29
{{ form_start(registrationForm, {
... lines 31 - 33
{{ form_row(registrationForm.email, {
attr: { placeholder: 'Email' }
}) }}
{{ form_row(registrationForm.plainPassword, {
attr: { placeholder: 'Password' }
}) }}
{{ form_row(registrationForm.agreeTerms) }}
... lines 41 - 44
{{ form_end(registrationForm) }}
... lines 46 - 69

That should be it! And yea, we could have been less fancy and also passed this label_attr variable directly to form_row(). That would have worked fine.

Anyways, let's try it! Move over, refresh and... woohoo! The placeholders pop into place. And other than my obvious typo... I think it looks pretty good!

Next: there's one field left that isn't rendering correctly: the terms checkbox. Let's learn how to customize how a single field renders.

Leave a comment!

Login or Register to join the conversation

Hey :)

At 8:00 i'm struggling to understand how the placeholder attr is passed to the form_widget call in our custom form theme block, as the only array of variables that is passed is widget_attr and it seems to be empty...

Any clarification?

Thanks for your time!


Does form rendering functions variables "injected" in the form theme block means they are also accessible in any subsequent function scope without specifically passing them during the function call?

I realize that my understanding of twig variables is limited and that's why i can't seem to understand...



That is not directly twig variables, but how the form system works. When it renders, it has access to the form variable and it has a bunch of values inside. You can try to print {% dump %} inside a theme block and you will see everything that was passed inside.


Cyril Avatar
Cyril Avatar Cyril | posted 2 years ago | edited

I need to customise the aspect of some items of a collection depending on values I have in a custom variable sent by the controller to the form view. Is there a way to pass a custom variable from the "parent" template to a form theme ? Thanks!


{# Here, myVar is defined, sent by controller #}
{{ form_widget(form.mycollection) }}
{% form_theme form 'my_custom_theme.html.twig' %}


{% block _mycollection_entry_widget %}
{# Here, myVar is not defined and I need it to customise the widget #}
{{ form_widget(form) }}
{% endblock %}

Hey Cyril S.!

Sorry for the slow reply! I would do that by passing a "var" into form_widget:

{{ form_widget(form.mycollection, {
    myVar: myVar
}) }}

That's it! As you noticed, the main template and the form theme template and totally isolated. But anything you pass as the 2nd argument of form_widget (or form_row) becomes available as a local variable in the form theme template.


Cyril Avatar

Mmm... I tried that previously but it seems not working in my case :-(

I oversimplified my issue to ask my question and, you're right, it works well in that simple case.

But, in fact, I have 4 files and 2 includes in my form:
file1 - main template (here defining theme path) > file2 - include > file 3 - include (here form_widget)
file4 - form theme template

When defining the form theme template in the included template, the theme doesn't apply and when defining the form theme template in the main template, the variable isn't transfered to the form theme template...

Hope my explanations are clear :-\


Hey Cyril S.!

Hmm. I'll admit that I can't remember how defining form themes works when you have included templates. I can say this, however: assuming you *are* able to get the correct form theme to be called for your form, passing a variable (via the 2nd argument of form_widget()) *will* be passed to the form theme template. That's a very basic mechanism to how the form theme system works. The logic is this:

A) Symfony determines which form theme to use (and, as you know, it has a few mechanisms to do that, like global form themes and per-template form themes. This is the part where I can't remember how it works when you use include()).

B) Symfony calls the blocks it needs from the form theme template. It exposes all of the field "vars" as local variables to that block. The vars are a combination of "vars" that were set in the form class for that field (e.g. via buildView() or finishView()) + whatever you pass as the 2nd argument into form_widget().

So, I'm can't say why it's not working... except to focus first on seeing if you can get the correct form theme to be executed. Then, the vars just "should work"... hopefully ;).


Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago

I want to make a form with an undefiened amount of input fields. For example: TODO List A, might have 3 items (input fields). TODO List B, might have 5 items.
What is the best approach? Is there a tutorial / chapter that explains this?

My think process right now is this (but I might have the wrong think process): WIth Javscript I try to make a onClick that appends new input fields / rows on the form. Then it somehow needs to get connected with the right amount of persist() methods. It then uses flush() on form submit.

But currently I have not started coding this, because I am thinking wether there is a 'official Symfony way' of doing this?


Hey Farry,

I think what you need is the ability to add more fields of a specific type into your form.
To get a better idea you can read the docs here: https://symfony.com/doc/cur...
or watch this video: https://symfonycasts.com/sc...


Farshad Avatar
Farshad Avatar Farshad | posted 2 years ago

I have a general question. We usually try to minimize the data by for example, compressing our css and javascript files. But on the other side, Symfony uses a giant dump for each row rendered (like you mentioned). Doesn't that make compressing our javascript and css almost obsolete? It feels like the amount of data that is being used in the background is a lot. It makes me wonder how it impacts the performance compared to compressing js and css?

Ralf B. Avatar
Ralf B. Avatar Ralf B. | posted 2 years ago

Was it only meant as showcase to use the attr directly in the template in this particular case or is there a resaon why not just using 'attr' => ['placeholder' => 'Test'], in the UserRegistrationFormType for those two fields?


Hey @Ralf,

It depends on situation, there is a possibility that you will need to reuse form but with different placeholder or label. In this case it will be more practical to put it in template, also important case is that it's a page styling option and it will be better to put in template so if someone will need to adjust styling or test will not search the form class. Probably it may be person who know nothing about symfony structure and how to guess where it can be.


caglar Avatar
caglar Avatar caglar | posted 4 years ago | edited

I am using different form themes for admin panel and front pages. Is there an easy way to define it seperately? When I try to define in base.html.twig, it gives "Variable "form" does not exist." runtime error if there is no form in page. Do I must to define in all twig files that has forms like that: {% form_theme form with ['form_themes/admin.html.twig'] %}

caglar Avatar
caglar Avatar caglar | caglar | posted 4 years ago | edited

I've solved:
`{% if form is defined %}

{% form_theme form with ['form_themes/admin.html.twig'] %}

{% endif %}`


Hey @Caglar

You can have a default form_theme for you entire page and only change it where you need it by defining the form_theme to use inside the template



Typo error into code block : {{ form_row(registrationForm.agreedTermsAt) }}


Hey Stéphane,

Thank you for this report, but I think you're wrong - it should be "{{ form_row(registrationForm.agreeTerms) }}". If you look at UserRegistrationFormType - you will see exactly "agreeTerms" field that is not mapped, i.e. "mapped = false" that means it's a virtual field in form that is not mapped to User entity at all. We can't use agreedTermsAt because it's not a boolean field but a Datetime object.



Hey Victor,

You are right. I have written 'agreedTermsAt' into buildForm fonction of UserRegistrationFormType class.
Sorry for that.


No problem!

Glad it was not on our side, easy win for us :)


2 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