If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeRemove that dump. Then refresh to look at our nice, normal form. In the template, all we do is call form_row()
and then - magically - a whole lot of markup is printed onto the page. So here's the million dollar question: where the heck is that markup coming from? I mean, somewhere deep in the core of Symfony, there must be a file that decides what the HTML for an input
field is, or what markup to use when printing errors? So, where is that?
The answer: a single file that - indeed - lives in the deepest, darkest corners of Symfony. I'll use the Navigate->File shortcut to look for form_div_layout.html.twig
:
{# Widgets #} | |
{%- block form_widget -%} | |
{% if compound %} | |
{{- block('form_widget_compound') -}} | |
{% else %} | |
{{- block('form_widget_simple') -}} | |
{% endif %} | |
{%- endblock form_widget -%} | |
... lines 10 - 209 | |
{%- block form_label -%} | |
{% if label is not same as(false) -%} | |
{% if not compound -%} | |
{% set label_attr = label_attr|merge({'for': id}) %} | |
{%- endif -%} | |
{% if required -%} | |
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} | |
{%- endif -%} | |
{% if label is empty -%} | |
{%- if label_format is not empty -%} | |
{% set label = label_format|replace({ | |
'%name%': name, | |
'%id%': id, | |
}) %} | |
{%- else -%} | |
{% set label = name|humanize %} | |
{%- endif -%} | |
{%- endif -%} | |
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>{{ translation_domain is same as(false) ? label : label|trans({}, translation_domain) }}</label> | |
{%- endif -%} | |
{%- endblock form_label -%} | |
... lines 231 - 243 | |
{%- block form_row -%} | |
<div> | |
{{- form_label(form) -}} | |
{{- form_errors(form) -}} | |
{{- form_widget(form) -}} | |
</div> | |
{%- endblock form_row -%} | |
... lines 251 - 263 | |
{%- block form -%} | |
{{ form_start(form) }} | |
{{- form_widget(form) -}} | |
{{ form_end(form) }} | |
{%- endblock form -%} | |
{%- block form_start -%} | |
{% set method = method|upper %} | |
{%- if method in ["GET", "POST"] -%} | |
{% set form_method = method %} | |
{%- else -%} | |
{% set form_method = "POST" %} | |
{%- endif -%} | |
<form name="{{ name }}" method="{{ form_method|lower }}"{% if action != '' %} action="{{ action }}"{% endif %}{% for attrname, attrvalue in attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}{% if multipart %} enctype="multipart/form-data"{% endif %}> | |
{%- if form_method != method -%} | |
<input type="hidden" name="_method" value="{{ method }}" /> | |
{%- endif -%} | |
{%- endblock form_start -%} | |
{%- block form_end -%} | |
{%- if not render_rest is defined or render_rest -%} | |
{{ form_rest(form) }} | |
{%- endif -%} | |
</form> | |
{%- endblock form_end -%} | |
{%- block form_errors -%} | |
{%- if errors|length > 0 -%} | |
<ul> | |
{%- for error in errors -%} | |
<li>{{ error.message }}</li> | |
{%- endfor -%} | |
</ul> | |
{%- endif -%} | |
{%- endblock form_errors -%} | |
... lines 299 - 372 |
This is probably the weirdest Twig template you'll ever see. It defines a bunch of blocks that, together, contain every little bit or markup that's used for any part of a form.
And here's how it works: when you render a piece of your form, Symfony opens this template looks for a specific block, which varies depending on what you're rendering, and then renders just that block to get that one little part of your form.
For example, when you render the widget for a TextareaType
field, it looks for a block called textarea_widget
and executes just its code:
... lines 1 - 32 | |
{%- block textarea_widget -%} | |
<textarea {{ block('widget_attributes') }}>{{ value }}</textarea> | |
{%- endblock textarea_widget -%} | |
... lines 36 - 372 |
Symfony never renders this whole template at once, just each block, as it needs them. It's almost like a list of small functions, where each function, or block, renders just a small part of the form.
But in reality, not all of our markup is coming from this one file. In an earlier tutorial, we started using the Bootstrap form theme. Open app/config/config.yml
and find the twig.form_themes
key:
... lines 1 - 36 | |
# Twig Configuration | |
twig: | |
... lines 39 - 42 | |
form_themes: | |
- bootstrap_3_layout.html.twig | |
... lines 45 - 74 |
By adding bootstrap_3_layout.html.twig
, we told Symfony to also look at this template, which again, lives deep dark in the core, black heart of Symfony. I'm kidding - the core is cool.
{% use "form_div_layout.html.twig" %} | |
{# Widgets #} | |
{% block form_widget_simple -%} | |
{% if type is not defined or type not in ['file', 'hidden'] %} | |
{%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%} | |
{% endif %} | |
{{- parent() -}} | |
{%- endblock form_widget_simple %} | |
... lines 11 - 246 |
This template overrides some blocks from form_div_layout.html.twig
. For example, form_widget_simple
in the bootstrap templates overrides the other one. It adds an extra form-control
class.
You see, there's a trick to these block names. There are three parts to every field: the label, the widget and the error. Well, four parts if you also count the "row".
And, every field has a type, like the entity type, the choice type or - for the name field - the text type:
... lines 1 - 6 | |
use Symfony\Bridge\Doctrine\Form\Type\EntityType; | |
use Symfony\Component\Form\AbstractType; | |
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; | |
... lines 10 - 13 | |
class GenusFormType extends AbstractType | |
{ | |
public function buildForm(FormBuilderInterface $builder, array $options) | |
{ | |
$builder | |
->add('name') | |
->add('subFamily', EntityType::class, [ | |
... lines 21 - 25 | |
]) | |
... lines 27 - 28 | |
->add('isPublished', ChoiceType::class, [ | |
... lines 30 - 33 | |
]) | |
... lines 35 - 39 | |
; | |
} | |
... lines 42 - 48 | |
} |
To render the widget for a text type, Symfony looks for a block called text_widget
. Or, to render the widget for a textarea type, Symfony uses textarea_widget
, which looks exactly like we'd expect!
... lines 1 - 32 | |
{%- block textarea_widget -%} | |
<textarea {{ block('widget_attributes') }}>{{ value }}</textarea> | |
{%- endblock textarea_widget -%} | |
... lines 36 - 372 |
What about rendering the label for a textarea? We'd expect this to be textarea_label
. Find that. Ooh, it's not here! This is because the field types follow a hierarchy. First, Symfony looks for textarea_label
. But if that's not there, it'll fallback to its parent type: text. So, text_label
. And if that doesn't exist, it'll finally look for - and find - form_label
:
... lines 1 - 209 | |
{%- block form_label -%} | |
{% if label is not same as(false) -%} | |
{% if not compound -%} | |
{% set label_attr = label_attr|merge({'for': id}) %} | |
{%- endif -%} | |
{% if required -%} | |
{% set label_attr = label_attr|merge({'class': (label_attr.class|default('') ~ ' required')|trim}) %} | |
{%- endif -%} | |
{% if label is empty -%} | |
{%- if label_format is not empty -%} | |
{% set label = label_format|replace({ | |
'%name%': name, | |
'%id%': id, | |
}) %} | |
{%- else -%} | |
{% set label = name|humanize %} | |
{%- endif -%} | |
{%- endif -%} | |
<label{% for attrname, attrvalue in label_attr %} {{ attrname }}="{{ attrvalue }}"{% endfor %}>{{ translation_domain is same as(false) ? label : label|trans({}, translation_domain) }}</label> | |
{%- endif -%} | |
{%- endblock form_label -%} | |
... lines 231 - 372 |
Form is the parent type for all fields.
And this system makes sense! The label for a textarea is no different from a label for any other field. So, all labels are rendered via this block.
Another way to see this fallback mechanism is back in the web profiler. Click the name
field and then find "View Variables". Every field will have a variable called block_prefixes
. This shows us the options: after trying text_label
, text_widget
or text_errors
- depending on which part of the field we're rendering, it'll fallback to form_label
, form_widget
or form_errors
.
And actually, there's also a way to override the block for just one field in your one form, by giving it a very specific name. In this case, if you had an _genus_form_name_label
block, that would override the label for only the name field in this form. Pretty cool.
With all this new fun stuff in mind, let's extend this by creating our own form theme. The goal: when a field has a validation error, add a cute "X" icon inside the text field. Let's do it!
"Houston: no signs of life"
Start the conversation!
// composer.json
{
"require": {
"php": "^7.1.3",
"symfony/symfony": "3.4.*", // v3.4.49
"doctrine/orm": "^2.5", // 2.7.5
"doctrine/doctrine-bundle": "^1.6", // 1.12.13
"doctrine/doctrine-cache-bundle": "^1.2", // 1.4.0
"symfony/swiftmailer-bundle": "^2.3", // v2.6.7
"symfony/monolog-bundle": "^2.8", // v2.12.1
"symfony/polyfill-apcu": "^1.0", // v1.23.0
"sensio/distribution-bundle": "^5.0", // v5.0.25
"sensio/framework-extra-bundle": "^3.0.2", // v3.0.29
"incenteev/composer-parameter-handler": "^2.0", // v2.1.4
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"knplabs/knp-markdown-bundle": "^1.4", // 1.9.0
"doctrine/doctrine-migrations-bundle": "^1.1", // v1.3.2
"stof/doctrine-extensions-bundle": "^1.2" // v1.3.0
},
"require-dev": {
"sensio/generator-bundle": "^3.0", // v3.1.7
"symfony/phpunit-bridge": "^3.0", // v3.4.47
"nelmio/alice": "^2.1", // v2.3.6
"doctrine/doctrine-fixtures-bundle": "^2.3" // v2.4.1
}
}