Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Form Theme Block Naming & Creating our Theme!

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

When Symfony renders the "label" part of a password field type... it should be looking for a password_label block name. And... it is. But... that block doesn't exist! What's going on?

Here's the situation: the label will look the same for probably every field type: there's no difference between how a label should render for a text field versus a choice drop-down. To avoid duplicating the label code over and over again, the block system has a fallback mechanism.

Block Prefixes

Go back to your browser, click on the form icon on the web debug toolbar and select plainPassword. Go check out the "View Variables". Ah, here it is: the very special block_prefixes variable! This is an array that Symfony uses when trying to find which block to use. For example, to render the "widget" for this field, Symfony first looks for a block named _user_registration_form_plainPassword_widget.

This super specific block name will allow us to change how the widget looks for just one field of the form. We'll do this a bit later. If it does not find that block, it next looks for password_widget, then text_widget, and finally form_widget. There is a password_widget block but, when the label is being rendered, there is not a password_label block. Ok, so it next looks for text_label. Let's see if that exists. Nope! Finally, it looks for form_label. Search for that. Got it!

This is the block that used to render every label for every field type.

The Form Rendering Big Picture

Open up register.html.twig: let's back up and make sure this all makes sense. When we call form_widget(registrationForm), that's a shortcut for calling form_row() on each field. That means that the "row" part of each field is rendered. Not surprisingly, the "row" looks exactly the same for all field types. In other words, in bootstrap_4_layout.html.twig, you probably won't find a password_row block, but you will find a form_row block. Keep searching until you find it... there it is!

Ah, I love it! It has some special logic on top, but then! Yes: it renders a div with a form-group class then calls the form_label(), form_widget() and form_help() functions! The reason you don't see form_errors() here is that it's called from inside of form_label() so we can get the correct Bootstrap markup.

Creating our Form Theme

We now know enough to be dangerous! If we could override this form_row block just for the registration form, we could simplify the markup to match what we need. How do we do that? By creating our own form theme... which is just a template that contains these fancy blocks.

If you create a form theme in its own template file - like bootstrap_4_layout.html.twig - you can reuse it across your entire app by adding it to twig.yaml after bootstrap. Or, you can add some code to your Twig template to use a specific form theme template only on certain forms.

But, we actually will not create a separate template for our form theme. Why not? If you only need to customize a single form, there's an easier way. At the top of the template where you form lives, add {% form_theme %}, the name of your form variable - registrationForm - and then _self.

... line 1
{% form_theme registrationForm _self %}
... lines 3 - 63

This says:

Yo form system! I want to use this template as a form theme template for the registrationForm object.

As soon as we do this, when Symfony renders the form, it will first look for form theme blocks right inside of this template. Yep, we could copy that form_row block from Bootstrap, paste it, and start customizing!

Let's do that! But, actually, the Bootstrap form_row block is a bit fancier than I need. Instead, open form_div_layout.html.twig and find the block there. Copy that and, in register.html.twig, paste this anywhere.

... lines 1 - 3
{% block form_row %}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
{{- form_label(form) -}}
{{- form_errors(form) -}}
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
{% endblock %}
... lines 17 - 63

Hmm - let's remove the wrapping <div> and see if this works! Deep breath - refresh! I saw something move! Inspect the form and... yes! That wrapping div is gone!

... lines 1 - 3
{% block form_row %}
{%- set widget_attr = {} -%}
{%- if help is not empty -%}
{%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}
{{- form_label(form) -}}
{{- form_errors(form) -}}
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}
{% endblock %}
... lines 15 - 61

When Symfony looks for the form_row() block it finds our block and uses it. All the other parts - like the widget and label blocks - are still coming from the Bootstrap theme. It's perfect.

But, we have more work to do! Next, let's learn a lot more about what we can do inside of these form theme blocks.

Leave a comment!

Login or Register to join the conversation
Milica S. Avatar
Milica S. Avatar Milica S. | posted 2 years ago | edited

I have a problem with theme block code actually I have duplicate validation errors:

{%- block form_row -%}

{%- set widget_attr = {} -%}
{%- if help is not empty -%}
    {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%}
{%- endif -%}

{{- form_label(form, null, {
    label_attr: { class: 'sr-only' }
}) -}}
{{- form_errors(form) -}}
{{- form_widget(form, widget_attr) -}}
{{- form_help(form) -}}

{%- endblock form_row -%}

I searched for symfony documentation <a href="https://symfony.com/doc/current/form/form_customization.html#form-form-view-variables&quot;&gt;https://symfony.com/doc/current/form/form_customization.html#form-form-view-variables&lt;/a&gt; and I find next:

remove {{- form_errors(form) -}} from form_row block and add it above form_row(s) rendering like:

{{ form_start(registrationForm, {

    attr: { class: 'form-signin' }
}) }}
    <h1 class="h3 mb-3 font-weight-normal">Register</h1>
    {{ form_errors(registrationForm) }}
    {{ form_row(registrationForm.email, {
        attr: { placeholder: 'Email' }
    }) }}
    {{ form_row(registrationForm.plainPassword, {
        attr: { placeholder: 'Password' }
    }) }}
    {{ form_row(registrationForm.agreeTerms) }}
    <button class="btn btn-lg btn-primary btn-block" type="submit">
{{ form_end(registrationForm) }}


This code works for me and it doesn't have duplicated validation error messages.

I am using to Symfony 5.1 and "symfony/form": "5.1.*".
My question: Am I doing something wrong and what is explanation for duplicate error messages.


@Vladimir Žarčanin,

Hey that looks weird. It shouldn't be duplicated in this case. Can you create a gist with related files or maybe github repo for more information?



Same here ;)
This happens because the errors are rendered on every form_label and not on the form_row.
Not sure if this has changed since in the bootstrap_4_layout.

Just remove the error rendering from the custom override.

1 Reply

That sounds very possible - thanks for posing the solution elkuku :).


Actually while following the tutorial and digging through the symfony docs I found a big red note here: https://symfony.com/doc/cur...


I love documentation :D :D :D

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

"There is a password_widget block but, when the label is being rendered, there is not a password_label block."

How does it know that it needs password_label block?

In here I dont see:

`{%- block password_widget -%}

{%- set type = type|default('password') -%}
{{ block('form_widget_simple') }}

{%- endblock password_widget -%}`
{%- block form_widget_simple -%}

{%- set type = type|default('text') -%}
{%- if type == 'range' or type == 'color' -%}
    {# Attribute "required" is not supported #}
    {%- set required = false -%}
{%- endif -%}
<input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>

{%- endblock form_widget_simple -%}`


Hey Lijana Z.!

Sorry for the slow reply!

There is a password_widget block but, when the label is being rendered, there is not a password_label block."
How does it know that it needs password_label block?

Good question :). Here's what the process looks like internally:

A) The form system starts to render the "label" for a "password" field. It asks: "which block should I use to render this"?

B) To determine that, it first looks for a block called "password_label". It always first looks for a block that is the "field type" an underscore and then "label" (because it's rendering a label). For example, when the form system needs to render the label for a "textarea" it also looks for textarea_label first. By default, pretty much no field types have a custom "TYPE_label" block.

C) Because there is no "password_label" block, the form system automatically then looks for "form_label". That's just how the system works - it always falls back to using "form".

This is a slight over-simplification, but the general idea is accurate: it looks for password_label first and if it is not found, falls back to form_label. The reason you don't see this logic inside the form_widget_simple or password_widget blocks is that the logic lives one level higher: this logic lives at the layer that is responsible for rendering the blocks. It's a crazy function, but the logic that renders the blocks and has all this "fallback logic" is this one: https://github.com/symfony/symfony/blob/b1bee601197e2a07ef3fd3274e46231b26e28824/src/Symfony/Component/Form/FormRenderer.php#L130


Lijana Z. Avatar


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