Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Bind Your Form to a Class

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

We created a form type class, used it in the controller to process the form submit and rendered it. This is pretty basic, but the form system is already doing a lot for us!

But... I think the form component can do more! Heck, I think it's been downright lazy. $data = $form->getData() gives us an associative array with the submitted & normalized data. That's cool... but it does mean that we need to set all of that data onto the Article object manually. Lame!

Setting the data_class Option

But, no more! Open ArticleFormType. Then, go back to the Code -> Generate menu - or Cmd+N on a Mac - select "Override Methods" and choose configureOptions(). Just like with buildForm(), we don't need to call the parent method because it's empty. Inside add $resolver->setDefaults() and pass an array. This is where you can set options that control how your form behaves. And, well... there aren't actually very many options. The most important, by far, is data_class. Set it to Article::class. This binds the form to that class.

... lines 1 - 9
class ArticleFormType extends AbstractType
{
... lines 12 - 19
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Article::class
]);
}
}

And... yep! That little option changes everything. Ready to see how? Back in your controller, dd($data).

... lines 1 - 14
class ArticleAdminController extends AbstractController
{
... lines 17 - 20
public function new(EntityManagerInterface $em, Request $request)
{
... lines 23 - 25
if ($form->isSubmitted() && $form->isValid()) {
... line 27
dd($data);
... lines 29 - 39
}
... lines 41 - 44
}
... lines 46 - 66
}

Now, move back to your browser. Watch closely: right now both fields are simple text inputs... because we haven't configured them to be anything else. But, refresh!

Form Field Type Guessing

Whoa! The content is now a textarea! We haven't talked about it yet, but we can, of course, configure how each field is rendered. By default, if you do nothing, everything renders as a text input. But, when you bind your form to a class, a special system - called the "form type guessing" system - tries to guess the proper "type" for each field. It notices that the $content property on Article is a longer text Doctrine type. And so, it basically says:

Hey peeps! This content field looks pretty big! So, let's use a textarea field type by default.

Anyways, form field type guessing is a cool feature. But, it is actually not the super important thing that just happened.

What was? Create another breaking news story:

Orion's Belt: for Fashion or Function?

Click Create and... yes! Check it out! $form->getData() is now an Article object! And the title and content properties are already set! This is the power of the data_class option.

When the form submits, it notices the data_class and so creates a new Article() object for us. Then, it uses the setter methods to populate the data. For example, the form has two fields: title and content. When we submit the form, it calls setTitle() and then setContent(). It's basically just an automatic way to do what we are already doing manually in our controller. This is awesome because we can remove code! Just say $article = $form->getData(), done. To help PhpStorm I'll add some inline documentation that says that this is an Article.

... lines 1 - 25
if ($form->isSubmitted() && $form->isValid()) {
/** @var Article $article */
$article = $form->getData();
... lines 29 - 36
}
... lines 38 - 65

That's great! Our controller is tiny and, when we submit, bonus! It even works!

Model Classes & Complex Forms

In most cases, this is how I use the form system: by binding my forms to a class. But! I do want you to remember one thing: if you have a super complex form that looks different than your entity, it's perfectly okay to not use data_class. Sometimes it's simpler to build the form exactly how you want, call $form->getData() and use that associative array in your controller to update what you need.

Oh, and while we usually see form types bound to an entity class, that's not required! This class could be any PHP class. So, if you have a form that doesn't match up well with any of your entities, you can still use data_class. Yep! Create a new model class that has the same properties as your form, set the data_class to that class, submit the form, get back that model object from the form, and use it inside your controller to do whatever you want!

Oh, and if this isn't quite making sense: no worries - we'll practice this later.

Form Theme: Making your Form Beautiful

Before we keep going, let's take 30 seconds to make our ugly form... beautiful! So far, we're not controlling the markup that's rendered in any way: we call a few form rendering functions and... somehow... we get a form!

Behind the scenes, all of this markup comes from a set of special Twig templates called "form themes". And yea, we can and totally will mess with these. If you're using Bootstrap CSS or Foundation CSS, ah, you're in luck! Symfony comes with a built-in form theme that makes your forms render exactly how these systems want.

Open config/packages/twig.yaml. Add a new key called form_themes with one element that points to a template called bootstrap_4_layout.html.twig.

twig:
... lines 2 - 4
form_themes:
- bootstrap_4_layout.html.twig

This template lives deep inside the core of Symfony. And we'll check it out later when we talk more about form themes. Because right now... we get to celebrate! Move over and refresh. Ha! Our form is instantly pretty! The form system is now rendering with Bootstrap-friendly markup.

Next: let's talk about customizing the "type" of each field so we can make it look and act exactly how we need.

Leave a comment!

16
Login or Register to join the conversation
Giacomo V. Avatar
Giacomo V. Avatar Giacomo V. | posted 3 years ago | edited

Hallo, i can't see the form template.

twig.yaml:

twig:
    default_path: '%kernel.project_dir%/templates'
    form_themes: ['bootstrap_4_layout.html.twig']```


base.html.twig:

<!DOCTYPE html>
<html lang="it">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <title>{% block title %}Welcome!{% endblock %}</title>
    {% block head_css %}

    {% endblock %}
    {% block head_js %}
        
    {% endblock %}
    {% block stylesheets %}{% endblock %}
</head>
<body>
    {% block body %}
    {% endblock %}
    {% block javascripts %}{% endblock %}
</body>

</html>



new.html.twig:

{% extends 'base.html.twig' %}

{% block body %}

{{ form_start(newGruppo) }}
<h1>Nuovo gruppo</h1>
{{ form_widget(newGruppo) }}
<button type="submit" class="btn btn-primary">Crea</button>
{{ form_end(newGruppo) }}

{% endblock %}



I just can see it if i use <u><b>both</b></u> the form_themes in twig.yaml and edit base.html.twig this way:

<!DOCTYPE html>
<html lang="it">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <title>{% block title %}Welcome!{% endblock %}</title>
    {% block head_css %}
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    {% endblock %}
    {% block head_js %}
        <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>
    {% endblock %}
    {% block stylesheets %}{% endblock %}
</head>
<body>
    {% block body %}
    {% endblock %}
    {% block javascripts %}{% endblock %}
</body>

</html>`

I'm using symfony 5 with my own project (not spacebar).

Any suggestions?

Thanks,

Giacomo.

1 Reply

Hey Giacomo,

> i can't see the form template.

What do you mean exactly? You don't see styles for the bootstrap4 styled form? If so - then yes, you have to include "bootstrap.min.css" css file in order to apply those styles. Symfony form theme gives you only CSS classes, but actual styles should be included on the page by you.

Cheers!

Reply
Giacomo V. Avatar
Giacomo V. Avatar Giacomo V. | Victor | posted 3 years ago

Thank you!

I included boostrap CDN and that worked!

just out of curiosity, how can use boostrap after install with composer, instead of link che CDN?

Reply

Hey Giacomo,

Fairly speaking I don't know if Bootstrap can be installed with Composer :p Even if it could - you can't just link it from vendor/ dir, you have to copy it to the public/ dir somehow. Usually, there's a special bundles for it like symfony/assetic-bundle: https://github.com/symfony/... - but it was deprecated in favor of brand new WebpackEncoreBundle. You can learn more about Webpack Encore in our course here: https://symfonycasts.com/sc... - this is an amazing tool for your assets with bunch of features.

Cheers!

Reply

Hi SymfonyCasts! I have a completely custom form rendered in my twig:


{{ form_start(form) }}
    <div class="mt-3">
    {% for question in question %}
        <input type="hidden" name="response[question][]" value="{{ question.id }}">
        {% set name = 'response[answer]['~ question.id ~']' %}
        {% if question.type == 'checkbox' %}
            {% set name = name ~ '[]' %}
        {% endif %}

        {#    {% if question.replies is defined %}#}
        <div class="mb-3 mt-3 border">
        <h3 class="mb-0" id="question-{{ question.id }}">{{ loop.index }}. {{ question.label }}</h3>
        </div>
        {% if question.type == 'checkbox' or question.type == 'radio' %}
            <div class="answers p-1">
            {% for answer in question.replies %}

                {% set id = 'answer-' ~ answer.id  %}

                <label for="{{ id }}">{{ answer.answer }}</label>
                <input type="{{ question.type }}" name="{{ name }}" id="{{ id }}">
            {% endfor %}
            </div>
        {% elseif question.type == 'textarea' %}
            <textarea name="{{ name }}" aria-labelledby="question-{{ question.id }}" cols="30" rows="5" class="form-control"></textarea>
        {% elseif question.type == 'number' %}
            <input type="number" name="{{ name }}" aria-labelledby="question-{{ question.id }}">
        {% else %}
            <input type="text" name="{{ name }}" aria-labelledby="question-{{ question.id }}" class="form-control">
        {% endif %}
        {#    {% endif %}#}

    {% endfor %}
    </div>

    <button class="btn btn-primary">{{ button_label|default('Submit') }}</button>
{#{{ form_end(form, {render_rest: false }) }}#}
{{ form_end(form) }}

I have no idea on how to submit the form response!

Any help?

Reply

Hey Shane,

Ha, good question! :) The button should have type="submit" to be able to submit the form, try to add this special type to your button and browser will know that it should send the form on that button hit, i.e.:


<button type="submit" class="btn btn-primary">{{ button_label|default('Submit') }}</button>

Cheers!

Reply
Oliver-W Avatar
Oliver-W Avatar Oliver-W | posted 2 years ago | edited

Hi,
is there a way to add an "a"-tag, or any other, to the label in the FormType??

[ 'label' => '&lt;a href="#"&gt;....']```
 just prints exactly what I typed in instead of transforming.

Thx
Oliver
Reply

Hey Oliver W.

By default Twig escapes your strings to avoid any HTML injection, if you really want to print some HTML you need to use the raw filter.
Just do this: [ 'label' => '<a href="#">....']|raw

Docs reference: https://twig.symfony.com/doc/2.x/filters/raw.html

Cheers!

Reply
Oliver-W Avatar
Oliver-W Avatar Oliver-W | MolloKhan | posted 2 years ago | edited

Hi MolloKhan

thx for your assistance but I need to do this in my form declaration not in the template!?

For now I made it in the template with a manual <label> combined with {{ form_widget() }} and {{ form_errors() }}. That's more work but I know what to do.

Reply

That may work but there are other 2 options depending on your needs. If you want to write the HTML inside the FormType class, then you will have to override the label value inside the template so you can use the raw filter. Something like this:


// form_template.html.twig

{{ form_label(form.some_field, null, {
    label: form.some_field.vars.label | raw
}) }}

or, just write the HTML inside the form template and you wouldn't have to use the raw filter

Cheers!

1 Reply
Jacek Avatar
Jacek Avatar Jacek | posted 3 years ago | edited

Hi, after adding in ArticleFormType:


    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('title')
            ->add('content')
        ;
    }

in ArticleAdminController->new in


$form->handleRequest($request);

when i try to submit form i'm getting error:
<br />The property "App\Entity\Article::$title" is not readable because it is typed "string". You should initialize it or declare a default value instead.<br />

Reply
Jacek Avatar

ok, I have managed to fixed it. I added previously types to fields in Article Entity and they just needed to be initialized.

Reply

Hey Jacek

That's great that you fixed it. How do you think is it true that sometimes you can fix anything after asking about it? :) For example I'm always find a way to fix issue, after asking colleague :)))

Cheers!

Reply
Jacek Avatar

Yep, it's the rubber duck debugging method :) https://en.wikipedia.org/wi...
Thanks for great tutorial :)

Reply
Benoit L. Avatar
Benoit L. Avatar Benoit L. | posted 3 years ago | edited

Hello, I have a drop down list of EntityType, i want to display a list grouped by family (so using form group) :
` ->add('idEtatMatriceDetails', EntityType::class,[

            'class' => EtatMatriceDetails::class,
            'choice_label' => 'LibelleEtatMatriceDetails',
            'group_by' => function($choice,$key,$val){ // $choice is an instance of EtatMatriceDetails
                switch($choice->getIdEtatMatrice()->getIdEtatMatrice())
                {
                    case 1:
                        return 'FAMILY_ONE';
                        break; 
                    case 2:
                        return 'FAMILY_TWO';
                        break;

....`
under FAMILY_ONE, i can have several options, however, some of these options are disabled (there is a column for that flag), so I want to filter these options, how can I do this in the closure? can I craft a special getter in the entity that filter those values? Thank you

Reply

Hey Yvon,

I see you mentioned that $choice in that closure is an instance of EtatMatriceDetails entity, so yes, I think you can create a new method in that entity with the logic and use it inside the closure to group them, or you can do the same logic directly in that closure. Have you tried it? Did you have any problems with this implementation?

Cheers!

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
    }
}
userVoice