This course is still being released! Check back later for more chapters.

Get Notified About this Course!

We will send you messages regarding this course only
and nothing else, we promise.
You can unsubscribe anytime by emailing us at:
privacy@symfonycasts.com
Login to bookmark this video
Buy Access to Course
08.

Organizing Form Fields

|

Share this awesome video!

|

Keep on Learning!

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

Login Subscribe

Let's take a moment to discuss the organization of fields in our form, particularly that Notes field sitting right in the middle of everything. Ideally, you'd want the required fields to come first, with the optional ones patiently waiting their turn at the end.

By default, Symfony renders form fields in the order they're defined inside the form type:

58 lines | src/Form/StarshipPartType.php
// ... lines 1 - 15
class StarshipPartType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', null, [
// ... lines 22 - 24
])
->add('price')
->add('notes')
->add('starship', EntityType::class, [
// ... lines 29 - 40
])
->add('createAndAddNew', SubmitType::class, [
// ... lines 43 - 46
])
;
}
// ... lines 50 - 56
}

No magic, no surprises. So, if you want a different order, the simplest solution, as you may have already guessed, is also the least exciting one: just reorder the fields in the form type.

Let's give it a go. Move the Notes field to the end, right before the submit button:

58 lines | src/Form/StarshipPartType.php
// ... lines 1 - 15
class StarshipPartType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name', null, [
// ... lines 22 - 24
])
->add('price')
->add('starship', EntityType::class, [
// ... lines 28 - 39
])
->add('notes')
->add('createAndAddNew', SubmitType::class, [
// ... lines 43 - 46
])
;
}
// ... lines 50 - 56
}

Refresh the page and voilà! The Notes field is now at the end. Problem solved!

The priority Form Field Option

But, what if you don't want to physically move the fields around, especially when the order needs to change dynamically under certain conditions? That's where the priority option comes to the rescue.

Every form field has a priority setting — the default is 0. If we add priority to the Starship field and set it to 10, Symfony will render that field earlier:

59 lines | src/Form/StarshipPartType.php
// ... lines 1 - 15
class StarshipPartType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// ... lines 21 - 26
->add('starship', EntityType::class, [
// ... lines 28 - 39
'priority' => 10,
])
// ... lines 42 - 48
;
}
// ... lines 51 - 57
}

A higher number means it's rendered first, while a lower number means it's rendered later. You can even assign a negative priority if you want a field to be rendered closer to the end.

This trick is super handy when field order depends on conditions that can't easily be solved by simply rearranging your code... Ooor when you want to avoid large Git diffs.

Customizing Field Layouts

But what if ordering isn't enough? What if you want a custom layout? Can we put the short Name and Price fields on the same line, for instance? The answer is yes! Symfony provides several helper functions for rendering forms. We already know some of them.

Up till now, we've been relying on {{ form_widget(form) }} to render all the fields automatically. But when you want more control, you can render fields manually, one by one.

Instead of rendering the entire form at once, let's render the first field using form_row() and pass form.starship — we still want that field rendered first:

// ... lines 1 - 6
{% block body %}
<div class="max-w-4xl mx-auto">
// ... lines 9 - 10
{{ form_start(form) }}
{{ form_row(form.starship) }}
// ... lines 13 - 20
{{ form_end(form) }}
</div>
{% endblock %}

After that, add a little HTML to create a grid using Tailwind CSS classes: grid, grid-cols-2 and gap-4. Inside, render form_row() again for form.name and form.price:

// ... lines 1 - 6
{% block body %}
<div class="max-w-4xl mx-auto">
// ... lines 9 - 10
{{ form_start(form) }}
{{ form_row(form.starship) }}
<div class="grid grid-cols-2 gap-4">
{{ form_row(form.name) }}
{{ form_row(form.price) }}
</div>
// ... lines 17 - 20
{{ form_end(form) }}
</div>
{% endblock %}

Don't forget the remaining fields before the hardcoded button — simply add form_rest() and pass the form variable to render any fields we haven't rendered yet:

// ... lines 1 - 6
{% block body %}
<div class="max-w-4xl mx-auto">
// ... lines 9 - 10
{{ form_start(form) }}
{{ form_row(form.starship) }}
<div class="grid grid-cols-2 gap-4">
{{ form_row(form.name) }}
{{ form_row(form.price) }}
</div>
{{ form_rest(form) }}
<button type="submit" class="text-white bg-green-700 hover:bg-green-800 rounded-lg px-5 py-2.5 me-2 mb-2 cursor-pointer">Create and close</button>
{{ form_end(form) }}
</div>
{% endblock %}

Refresh the page, and you'll see that the Name and Price fields are now sitting happily next to each other.

Aligning Submit Buttons on the Same Line

But wait, can we do the same for the buttons? Absolutely! Back to the template, render the Notes field with form_row() below the grid, passing form.notes. Next, right before the hardcoded button, render form_row() again passing form.createAndAddNew:

// ... lines 1 - 6
{% block body %}
<div class="max-w-4xl mx-auto">
// ... lines 9 - 10
{{ form_start(form) }}
// ... line 12
<div class="grid grid-cols-2 gap-4">
// ... lines 14 - 15
</div>
{{ form_row(form.notes) }}
{{ form_row(form.createAndAddNew) }}
<button type="submit" class="text-white bg-green-700 hover:bg-green-800 rounded-lg px-5 py-2.5 me-2 mb-2 cursor-pointer">Create and close</button>
// ... lines 22 - 27
{{ form_end(form) }}
</div>
{% endblock %}

This form_end() function renders any remaining fields and closes the form tag. To choose where we want to render the remaining fields, we can use form_rest(form). I'll add a comment to explain this:

Anything you want to add after all fields are rendered, but before the closing </form>

// ... lines 1 - 6
{% block body %}
<div class="max-w-4xl mx-auto">
// ... lines 9 - 10
{{ form_start(form) }}
// ... lines 12 - 17
{{ form_row(form.notes) }}
{{ form_row(form.createAndAddNew) }}
<button type="submit" class="text-white bg-green-700 hover:bg-green-800 rounded-lg px-5 py-2.5 me-2 mb-2 cursor-pointer">Create and close</button>
{{ form_rest(form) }}
{#
Anything you want to add after all fields are rendered
but before the closing </form>
#}
{{ form_end(form) }}
</div>
{% endblock %}

Field Display and Error Handling

Refresh the page again! Hmm, the buttons are still not aligned. If you inspect the HTML, you'll see why: form_row() renders a full container, including the actual field, its label, also errors if there are any. All that is wrapped with this div tag.

Usually, that's great for fields, but not in our case. We want to get rid of that extra div wrapper and render only the field itself, the button in this case. The solution? In Symfony, form fields, that is the input, button, select HTML elements, are called widgets. So, to render just the button element, replace form_row() for the button with form_widget():

// ... lines 1 - 6
{% block body %}
<div class="max-w-4xl mx-auto">
// ... lines 9 - 10
{{ form_start(form) }}
// ... lines 12 - 19
{{ form_widget(form.createAndAddNew) }}
<button type="submit" class="text-white bg-green-700 hover:bg-green-800 rounded-lg px-5 py-2.5 me-2 mb-2 cursor-pointer">Create and close</button>
// ... lines 22 - 27
{{ form_end(form) }}
</div>
{% endblock %}

Refresh again, and — nice! The buttons are finally in line.

You can manually render all the components of a form_row() this way. form_widget() renders the field element, form_label() renders the label element, form_errors() renders a list of errors (if any), and form_help() renders help text (if any).

Render Missing Global Form Errors

But be careful — we've introduced a problem. Corrupt the CSRF token again... and submit the form... We see the errors for name and price, but not the expected CSRF error message.

In the web debug toolbar, the validator tab shows 2 errors, but the form tab shows 3. What's up with that? Open the validator panel. Ok, we see our name and price errors.

Now go to the forms tab. Here we see 3 errors - the 2 field errors, and the CSRF error attached to the form itself. This one's a form-level validation error, that's why it doesn't show up in the validator panel.

Back in our template, when we previously used form_widget(form), Symfony automatically rendered the form-level, or global errors, for us. But now that we're rendering fields manually, we need to remember to render those global errors manually too. So, right after the form_start(), add form_errors(form):

// ... lines 1 - 6
{% block body %}
<div class="max-w-4xl mx-auto">
// ... lines 9 - 10
{{ form_start(form) }}
{{ form_errors(form) }}
// ... lines 13 - 29
{{ form_end(form) }}
</div>
{% endblock %}

Ok, let's try this again. Back in the browser, refresh the page... corrupt the CSRF token... and submit.

Sweet! All the expected errors are back.

Making the Form More User-Friendly

To make the form more user-friendly and help users avoid validation errors in advance, you can add hints directly to the field using a special help option. For instance, let's tell users that free parts are not allowed by adding a help option to the Price field with a meaningful message:

We don't allow free parts! Please set a price

61 lines | src/Form/StarshipPartType.php
// ... lines 1 - 15
class StarshipPartType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
// ... lines 21 - 25
->add('price', null, [
'help' => 'We don\'t allow free parts! Set up a price',
])
// ... lines 29 - 50
;
}
// ... lines 53 - 59
}

When you refresh the form, you'll see this helpful message displayed in subtle grey text under the field.

Adding Form Field Attributes in the Template

Remember when we added CSS classes to the Submit button in the form type? That works, but it's not ideal — designers probably don't want to touch your PHP code, and they might not even know what a form type is, or how it works in a Symfony application. So, styling decisions don't really belong there.

Instead, let's move those styles to the template so that our designers can easily change it. I'll comment it out in the form type. Copy the long line of CSS classes, and go to the template. For the form_widget(form.createAndAddNew) call, add a second argument, an options hash.

Here you can pass the same options you pass in the form type. So, we want an attr option set to another hash. Inside, add the class option... and paste the CSS classes we copied earlier:

// ... lines 1 - 6
{% block body %}
<div class="max-w-4xl mx-auto">
// ... lines 9 - 10
{{ form_start(form) }}
// ... lines 12 - 21
{{ form_widget(form.createAndAddNew, {
'attr': {
'class': 'text-white bg-blue-700 hover:bg-blue-800 rounded-lg px-5 py-2.5 me-2 mb-2 cursor-pointer',
},
}) }}
// ... lines 27 - 33
{{ form_end(form) }}
</div>
{% endblock %}

When you refresh the page - you'll see the same styling, but with a cleaner separation of concerns. Just remember that options defined in Twig templates overwrite everything set in the form type.

Highlighting Required Form Fields with CSS

Help messages are great, but sometimes you want required fields to stand out visually. Let's add a tiny CSS trick to show a red asterisk next to every required field on our form. Open assets/styles/app.css and at the end, copy/paste the following snippet from the script below:

10 lines | assets/styles/app.css
// ... lines 1 - 6
input, textarea, select {
background-color: inherit;
}

Refresh the form... Cool! Now every required field gets a nice red asterisk. Not just for this form, but for all forms in our app. Neat!

And just like that, you’ve gone from a default Symfony form to a fully customized, designer-friendly, error-safe form layout. Not bad for one chapter, right?

Next, let’s speed up our form creation for different CRUD operations on our entities by leveraging MakerBundle again. Stay tuned!