Why I Prefer Not To Use Twig


By the end of the previous video we had a tested and working method for creating new Wallpapers, including uploading a Wallpaper image file. As part of the upload process, our image's dimensions would be found programmatically, and our image's filename would also be set for us.

The downside: the form that is rendered for us, courtesy of the configuration we've provided to EasyAdminBundle, looks a little messy. We've come too far to fall at the final hurdle. Let's cover one way to address this. There will likely be many others, go with what works best for you.

Before I start here I want to cover why this video is separated from the other sections. What we are about to do feels like a hack. There very well may be a defacto way to solve this problem, but unfortunately, I am not aware of it.

Being completely honest, I rarely use Symfony with Twig anymore. I use Symfony, and I like Twig, but I believe they are the wrong solution beyond the basics.

If I don't use Symfony with Twig, then you may be wondering what I use instead?

The answer is I use Symfony as a JSON API, and then a front end framework, or library, such as a React for everything the site visitor sees. This works extremely well for me, and if you're at all interested in this concept then I would recommend you watch this course next.

The reasoning for this is that I do not believe Twig is the right solution for a modern, dynamic front end experience. There are too many areas where it will force me to adapt the way I work to meet its requirements. This is most evident when using Symfony's form component.

This is where things get really confusing though. I really, really like the form component. It's up there with the parts of Symfony I most frequently use, and gain the most from using. It is truly a powerful and flexible component.

Why I think this may be confusing is in the mindset I had when first venturing into the Symfony framework - surely the form and the HTML it generates are very tightly coupled, right?

The form component can generate a HTML representation of your form(s) for you.

But it needn't.

You do not need to use the Twig helper functions (e.g. {{ form(myForm) }}) at all. You can type out all the HTML for your forms by hand, if you are so inclined. Indeed, that's pretty much the way you do it if you don't use Twig.

The other eye opening thing for me was when I realised I could POST in JSON to my forms. That was a real game changer.

Ok, but we are straying somewhat from the point.

I find the task of forcing Symfony's form component and it's associated Twig blocks to output my forms exactly as I need them to be overly complex. It's really that simple.

There is nothing more frustrating to me than when I know how to do something in a manner that works, and then being forced to change that approach so I can do the same thing but in a different way.

Let's use our current code to give a much more concrete example of this very problem.

Wallpaper upload form visual bug

When we go to view our "Create Wallpaper" form now, we see the File input is formatted strangely.

The issue here is that our custom UploadedFileType form is being rendered out as though it were part of a collection. This might be useful if we accepted multiple images per Wallpaper entity. But we don't. And so it's not.

Why this happens, as best I understand it, is because we've subtly changed the way our form works.

Before we needed to use the custom UploadedFileType, we had a single field of type FileType. We didn't need to do very much to make this work, if you remember the config was quite straightforward:

# app/config/config/easy_admin_bundle.yml

easy_admin:
    entities:
        Wallpaper:
            form:
                fields:
                    - { property: "file", type: "file", label: "File" }
                    - "slug"

In using this configuration, Symfony's FileType::class form field type will be used.

When it comes to rendering out the HTML representation of a given form field, the FileType is considered a 'simple' form field. Don't let your guard down on behalf of the name, I think they are initially more complicated to understand than the more complex form types we normally work with.

A simple form field will output as a standard HTML element:

<!-- /src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig#L11 -->

{%- block form_widget_simple -%}
    {%- set type = type|default('text') -%}
    <input type="{{ type }}" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %}/>
{%- endblock form_widget_simple -%}

Essentially here we are going to end up with some form of input rendered out as HTML. This might be <input type="text"... or <input type="file"... etc.

Where things get more complicated is when using our own custom form field types.

Our custom form field types are not 'simple'. They are seen as compound.

Why this matters is because when form fields are rendered, they will go through a variety of defined Twig blocks. One of the foundational blocks is form_widget:

<!-- /src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig#L3 -->

{%- block form_widget -%}
    {% if compound %}
        {{- block('form_widget_compound') -}}
    {% else %}
        {{- block('form_widget_simple') -}}
    {% endif %}
{%- endblock form_widget -%}

It's important to understand that even though our form is seen as 'compound', eventually the field we added - file - will be rendered out as a 'simple' field.

<?php

// /src/AppBundle/Form/Type/UploadedFileType.php

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('file', FileType::class, [
                'multiple' => false,
                'label'    => false,
            ])
        ;
    }

The issue we're hitting on is that the array of Twig templates in between this process are coming to the wrong conclusion when our HTML is being put together behind the scenes. Because we're using EasyAdminBundle, we are compounding the problem. Ho ho, there's a joke in there somewhere.

Notice that I've put in label => false - what happens if we don't have that?

Wallpaper upload form visual bug

Oh lordy, it's even worse.

Assuming we are using our own custom UploadedFileType, let's get forensic and peek at the generated HTML:

<div class="col-xs-12 ">
    <div class="form-group  field-uploaded_file">
        <label class="col-sm-2 control-label required">
            File
        </label>
        <div class="col-sm-10">
            <div class="empty collection-empty">
                <span class="label label-empty">
                    Empty
                </span>
            </div>
            <div id="wallpaper_file" 
                 data-empty-collection="    <div class=&quot;empty collection-empty&quot;>
<span class=&quot;label label-empty&quot;>Empty</span>

</div>">
            <div class="form-group  field-file">
                <div class="col-sm-2"></div>
                <div class="col-sm-10">
                    <input type="file" id="wallpaper_file_file" name="wallpaper[file][file][file]" required="required">
                </div>
            </div>
        </div>
    </div>
</div>

Believe it or not I've actually (attempted to) cleaned up this HTML significantly.

There's a bunch of stuff happening here that we don't want.

We start off with a div with the class of col-xs-12. This is standard Bootstrap stuff. This will be a 12 column / full width view even on the smallest of screens.

Inside this we have our form-group wrapper. Again, a typical Bootstrap construct, suggesting that everything nested in this div is related to one input.

Notice here also the inclusion of the class of field-uploaded_file. This is added because we are using the UploadedFileType. The naming of this class is directly tied to the name of our form type.

Inside this we have the first, outer label. This is the label we want, the one that lines up with Slug. This label is guessed from the name of our field and capitalised for us. We could change this, but to do so we would need to edit the easy_admin_bundle.yml file:

# app/config/config/easy_admin_bundle.yml

easy_admin:
    entities:
        Wallpaper:
            form:
                fields:
                    - { property: "file", type: "file", label: "A different label here" }
                    - "slug"

Notice also that the label is set to the CSS class of col-sm-2. It has some additional Bootstrap-y helpers for styling purposes.

So far, so good.

Next a div of col-sm-10. Remember Bootstrap has a 12-column grid. We use 2 columns for the labels, and 10 for the content.

        <div class="col-sm-10">
            <div class="empty collection-empty">
                <span class="label label-empty">
                    Empty
                </span>
            </div>
            <div id="wallpaper_file" 
                 data-empty-collection="    <div class=&quot;empty collection-empty&quot;>
<span class=&quot;label label-empty&quot;>Empty</span>

</div>">
            <div class="form-group  field-file">
                <div class="col-sm-2"></div>
                <div class="col-sm-10">
                    <input type="file" id="wallpaper_file_file" name="wallpaper[file][file][file]" required="required">
                </div>
            </div>
        </div>

Inside this div, things start to get weird.

We have this div representing an empty collection. This leads to the dotted 'EMPTY' section on our form. I don't want this, but here it is all the same.

Next up is a div with an id of wallpaper_file.

This div contains some HTML that might (though seemingly wouldn't) be helpful too us if we really were working with a collection. If you haven't ever done so, working with collections in Symfony can be a little confusing so I'd recommend checking out the Symfony2 Form Collection Tutorial series I did here on CodeReviewVideos. It's for Symfony 2, but the concepts are still valid.

With all that said, we can totally disregard this as we don't need it.

Where things go really out of whack is in the nested <div class="form-group field-file">.

Here we have another 12 column arrangement being added inside our earlier col-sm-10.

This is how we end up with that unusual element-inside-an-element problem.

Note though that where the label could go, we have a blank - but still space consuming <div class="col-sm-2"></div> element.

Below this we, finally, have our form input field. The name of this field is incorrect. If we try to use this, Symfony would become very confused.

All in all, not so good so far.

I want to point out that this is not a problem specific to EasyAdminBundle. I encountered this problem a good few months ago (at the time of writing) when covering Bootstrap 3 and Symfony 3 form integration. Unfortunately, there doesn't seem to be a defacto way to address this problem just yet.

Fixing Our Form

As it stands, I am not aware of a single correct way to solve this problem. I'm sure there likely is one, and if you know it, please do share.

There's a bunch of hacky ways to solve it.

The way I'm going to solve this is two fold.

First, I will create a new Twig template to contain a single new block. This will be the theme for whenever an UploadedFileType is rendered as HTML.

This is pretty simple to achieve. It involves creating a new Twig template, and then add in a simpler variation of the form_widget_simple we saw earlier:

<!-- /app/Resources/views/form/image_upload.html.twig -->

{% block uploaded_file_widget %}
    <input type="file" {{ block('widget_attributes') }}/>
{% endblock %}

To make this work we need to update the Twig Configuration section of config.yml:

# /app/config/config.yml

# Twig Configuration
twig:
    debug: '%kernel.debug%'
    strict_variables: '%kernel.debug%'
    form_themes:
        - 'form/image_upload.html.twig'

Second, I'm going to force the name given to the generated input element to ensure that the data from the form correctly maps back to the entity structure I'm using.

By this I mean when our input type is rendered, I want to ensure it has the expected name:

<input type="file" id="wallpaper_file_file" name="wallpaper[file][file]">

Note earlier it was being rendered as name="wallpaper[file][file][file]".

Why this matters is because of how Symfony will expect the submitted form data to map back on to the underlying entity structure.

To do this we need to alter the form's full_name variable.

// /src/AppBundle/Form/Type/UploadedFileType.php

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        parent::buildView($view, $form, $options);

        $view->vars['full_name'] = "wallpaper[file][file]";
    }

Although that feels very hardcoded. What if we want to use this UploadedFileType in other forms?

Well, I haven't had the need to test that just yet, but with a nod towards the possibility of this, we might choose to concatenate whatever the existing full_name variable is with our single field name:

// /src/AppBundle/Form/Type/UploadedFileType.php

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        parent::buildView($view, $form, $options);

        $view->vars['full_name'] = $view->vars['full_name'] . "[file]";
    }

If you're wondering how I figured that out:

// /src/AppBundle/Form/Type/UploadedFileType.php

    public function buildView(FormView $view, FormInterface $form, array $options)
    {
        parent::buildView($view, $form, $options);

        dump($view);
        dump($form);
        dump($options);

        $view->vars['full_name'] = $view->vars['full_name'] . "[file]";
    }

Then, reload your form, and have a look in the dump output on the web debug toolbar. After that, it's just a case of finding the best looking bit of data from everything available :)

If you're thinking that was a ridiculous amount of work to achieve such a simple task then I am in agreement with you. This should not, and need not have been so difficult.

Maybe there is a simpler solution.

Truthfully though, it's because of problems like this that I began seriously investigating Angular, and then React. Use the right tool for the job.

Anyway, the good news after all that is that we have a working - and nice looking - Wallpaper Upload Form.

Right, time for a cup of tea and lie down. We've still got to figure out how to edit existing Wallpapers yet, and then it would be super nice to make sure the Delete operation actually deletes the uploaded image.

Work work. Done Building Ship! (it's a Warcraft 2 quote, if you're wondering / confused).

Code For This Course

Get the code for this course.

Episodes

# Title Duration
1 Introduction and Site Demo 02:14
2 Setup and a Basic Wallpaper Gallery 08:43
3 Pagination 08:24
4 Adding a Detail View 04:47
5 Creating a Home Page 11:14
6 Creating our Wallpaper Entity 07:50
7 Wallpaper Setup Command - Part 1 - Symfony Commands As a Service 05:57
8 Wallpaper Setup Command - Part 2 - Injection Is Easy 08:53
9 Wallpaper Setup Command - Part 3 - Doing It With Style 05:37
10 Doctrine Fixtures - Part 1 - Setup and Category Entity Creation 08:52
11 Doctrine Fixtures - Part 2 - Relating Wallpapers with Categories 05:56
12 EasyAdminBundle - Setup and Category Configuration 06:02
13 EasyAdminBundle - Wallpaper Setup and List View 07:46
14 EasyAdminBundle - Starting with Wallpaper Uploads 05:57
15 Testing with PhpSpec to Guide Our Implementation 03:39
16 Using PhpSpec to Test our FileMover 05:34
17 Symfony Dependency Testing with PhpSpec 08:47
18 Defensive Counter Measures 06:33
19 No Tests - Part 1 - Uploading Files in EasyAdminBundle 11:01
20 No Tests - Part 2 - Uploading Files in EasyAdminBundle 07:05
21 Don't Mock What You Don't Own 09:36
22 You've Got To Take The Power Back 07:36
23 Making Symfony Work For Us 08:56
24 Testing The Wallpaper File Path Helper 15:11
25 Finally, It Works! 14:56
26 Why I Prefer Not To Use Twig 16:51
27 Fixing The Fixtures 11:20
28 Untested Updates 14:30
29 Untested Updates Part Two - Now We Can Actually Update 06:33
30 Adding an Image Preview On Edit 12:31
31 Delete Should Remove The Wallpaper Image File 11:02
32 Getting Started Testing Wallpaper Updates 10:02
33 Doing The Little Before The Big 08:13
34 Tested Image Preview... Sort Of :) 07:36
35 Finishing Up With a Tested Wallpaper Update 10:41
36 Test Driven Wallpaper Delete - Part 1 11:06
37 Test Driven Wallpaper Delete - Part 2 11:57
38 EasyAdminBundle Login Form Tutorial 08:01