Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Rendering Fields Manually

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

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

Login Subscribe

Finally, let's look at the Swiss Army knife of form rendering: instead of using the form-rendering functions, we'll build the field entirely by hand.

For example, suppose we need to do something crazy with the "year" drop-down field. That's fine! We'll still render the label and errors normally, but let's handle the widget ourselves. Yep, I literally mean: create a select tag and start filling in the details.

The first detail is the id attribute. Every field has a unique id, which ties that field to its label. And this is where form variables help us out big.

Referencing Field Variables Directly

Go back into the Form tab of the web profiler and click the year field. There are a lot of variables, but there are a few that are especially important, like id and full_name, which normally becomes the name attribute.

In your template, reference the id variable with: genusForm.firstDiscoveredAt.year.vars.id. Repeat that for the name attribute set to genusForm.firstDiscoveredAt.year.vars.full_name:

{{ form_start(genusForm) }}
... lines 2 - 20
{{ form_label(genusForm.firstDiscoveredAt.year) }}
<select id="{{ genusForm.firstDiscoveredAt.year.vars.id }}"
name="{{ genusForm.firstDiscoveredAt.year.vars.full_name }}">
... lines 24 - 26
</select>
{{ form_errors(genusForm.firstDiscoveredAt.year) }}
{{ form_row(genusForm.firstDiscoveredAt.month) }}
{{ form_row(genusForm.firstDiscoveredAt.day) }}
... lines 32 - 33
{{ form_end(genusForm) }}

Now that we understand the FormView tree and how variables are stored, this actually makes sense.

Printing the Options

Next, what about the options that should go inside the select tag? Head back to the web profiler to see which variable might help us. Ah, here's one called choices, and each item is a ChoiceView object. Use the Shift+Shift shortcut to open that file from Symfony:

... lines 1 - 11
namespace Symfony\Component\Form\ChoiceList\View;
... lines 13 - 18
class ChoiceView
{
/**
* The label displayed to humans.
*
* @var string
*/
public $label;
/**
* The view representation of the choice.
*
* @var string
*/
public $value;
/**
* The original choice value.
*
* @var mixed
*/
public $data;
/**
* Additional attributes for the HTML tag.
*
* @var array
*/
public $attr;
/**
* Creates a new choice view.
*
* @param mixed $data The original choice
* @param string $value The view representation of the choice
* @param string $label The label displayed to humans
* @param array $attr Additional attributes for the HTML tag
*/
public function __construct($data, $value, $label, array $attr = array())
{
$this->data = $data;
$this->value = $value;
$this->label = $label;
$this->attr = $attr;
}
}

Cool! Each ChoiceView is a simple object, with a public label property and a public value property:

... lines 1 - 18
class ChoiceView
{
/**
* The label displayed to humans.
*
* @var string
*/
public $label;
/**
* The view representation of the choice.
*
* @var string
*/
public $value;
... lines 34 - 63
}

That's exactly what we need.

Add a loop: for choice in genusForm.firstDiscoveredAt.year.vars.choices:

{{ form_start(genusForm) }}
... lines 2 - 20
{{ form_label(genusForm.firstDiscoveredAt.year) }}
<select id="{{ genusForm.firstDiscoveredAt.year.vars.id }}"
name="{{ genusForm.firstDiscoveredAt.year.vars.full_name }}">
{% for choice in genusForm.firstDiscoveredAt.year.vars.choices %}
... line 25
{% endfor %}
</select>
{{ form_errors(genusForm.firstDiscoveredAt.year) }}
{{ form_row(genusForm.firstDiscoveredAt.month) }}
{{ form_row(genusForm.firstDiscoveredAt.day) }}
... lines 32 - 33
{{ form_end(genusForm) }}

Inside, add <option value=""> then print choice.value.

We also need to know if this option should be currently selected. We can do that by comparing the value to the data variable that's attached to the year field. Why not do this in one big giant line: choice.value == genusForm.firstDiscoveredAt.year.vars.data. Wow. Then, ? ' selected' or empty quotes. Finally, for the option text, use choice.label:

{{ form_start(genusForm) }}
... lines 2 - 20
{{ form_label(genusForm.firstDiscoveredAt.year) }}
<select id="{{ genusForm.firstDiscoveredAt.year.vars.id }}"
name="{{ genusForm.firstDiscoveredAt.year.vars.full_name }}">
{% for choice in genusForm.firstDiscoveredAt.year.vars.choices %}
<option value="{{ choice.value }}" {{ choice.value == genusForm.firstDiscoveredAt.year.vars.data ? 'selected' : '' }}>{{ choice.label }}</option>
{% endfor %}
</select>
{{ form_errors(genusForm.firstDiscoveredAt.year) }}
{{ form_row(genusForm.firstDiscoveredAt.month) }}
{{ form_row(genusForm.firstDiscoveredAt.day) }}
... lines 32 - 33
{{ form_end(genusForm) }}

That's it! Go back to your browser, then refresh. Ah, error!

That's me being careless: the sub-field is called year, not years:

{{ form_start(genusForm) }}
... lines 2 - 21
<select id="{{ genusForm.firstDiscoveredAt.year.vars.id }}"
name="{{ genusForm.firstDiscoveredAt.year.vars.full_name }}">
{% for choice in genusForm.firstDiscoveredAt.year.vars.choices %}
<option value="{{ choice.value }}" {{ choice.value == genusForm.firstDiscoveredAt.year.vars.data ? 'selected' : '' }}>{{ choice.label }}</option>
{% endfor %}
</select>
... lines 28 - 33
{{ form_end(genusForm) }}

Refresh again.

It works! It's not styled because we've taken complete control of rendering it. But you can see the errors, and the options look correct. Cool!

Marking Fields as Rendered

So, we're done! Wait... except for this random field at the bottom of my form. What the heck!? That's my year field! What's going on?

See that form_end() at the bottom of our form?

... lines 1 - 33
{{ form_end(genusForm) }}

Remember how it renders any field that we forgot to render? Well, now it thinks that we forgot to render the year field. The nerve!

So, could we just tell it that the field was actually rendered? Yep, and the code is both simple and strange. Use a rare do tag from Twig and say genusForm.firstDiscoveredAt.year.setRendered():

{{ form_start(genusForm) }}
... lines 2 - 28
{% do genusForm.firstDiscoveredAt.year.setRendered() %}
... lines 30 - 34
{{ form_end(genusForm) }}

Whaaaat? Well, every field is a FormView object. And if you open that class, it has a setRendered() method!

... lines 1 - 18
class FormView implements \ArrayAccess, \IteratorAggregate, \Countable
{
... lines 21 - 86
/**
* Marks the view as rendered.
*
* @return FormView The view object
*/
public function setRendered()
{
$this->rendered = true;
return $this;
}
... lines 98 - 161
}

And by calling it, we're saying:

Yo, we rendered this already. So, you know, don't try to render it again.

Refresh now! Whoops! Another Ryan mistake - make sure your variable is genusForm, not genus:

{{ form_start(genusForm) }}
... lines 2 - 28
{% do genusForm.firstDiscoveredAt.year.setRendered() %}
... lines 30 - 34
{{ form_end(genusForm) }}

Now that extra field is gone.

Wrap it Up!

Congrats team: you have the power to render your forms in whatever crazy, insane, creative way you want! But with power, comes great responsibility. I'll delete all the code we just added and go back to simply rendering genusForm.firstDiscoveredAt:

{{ form_start(genusForm) }}
... lines 2 - 20
{{ form_row(genusForm.firstDiscoveredAt) }}
... line 22
{{ form_end(genusForm) }}

... lines 1 - 15
class GenusFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
... lines 21 - 38
->add('firstDiscoveredAt', DateType::class, [
'widget' => 'single_text',
'attr' => ['class' => 'js-datepicker'],
'html5' => false,
])
;
}
... lines 46 - 52
}

Don't use your new skills unless you actually need to.

Ok guys, that's it! If you still have some questions, or want to tell me about something really cool you did, or share vacation photos, whatever, you can do it in the comments - it's always great to hear from you.

All right guys, see you next time.

Leave a comment!

5
Login or Register to join the conversation

Hello Ryan,
Thank for this cool stuff. Yours tutorials are very interesting.

Reply
Juan carlos Avatar
Juan carlos Avatar Juan carlos | posted 5 years ago

Hi Ryan, first id like to say your videos are great!. Also, i think that there is a mistake in the if statement on line 25 app/Resources/views/admin/genus/_form.html.twig. (choice.value == genusForm.firstDiscoveredAt... ? 'selected' : '' )

Reply

Hey Juan,

Nice catch in such a long string! Thank you, fixed in https://github.com/knpunive...

Cheers!

Reply
Default user avatar
Default user avatar Kebabra Abdessamad | posted 5 years ago

just finished the course and i love it, thanks man :) already feeling dangerous hhhh

Reply
Cat in space

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

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": "^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
    }
}
userVoice