Untested Updates


The previous video was all about ensuring our data fixtures behaved properly after our most recent changes, ensuring we could revert to a known basic state for development.

With our system reverted back to a known state, let's continue on with implementing Wallpaper Update.

Currently, thanks to EasyAdminBundle, we do have a working Update form, where we can change parts of the saved data relating to the current Wallpaper entity.

What I would like to do is enhance this further by displaying a little preview of the image currently uploaded / associated with this wallpaper entity.

To do this, we will need to use a few of the concepts we saw during our test-driven approach, including form themes, and a custom form type. There will also be some new things to cover including Doctrine's lifecycle callbacks, and Symfony form Data Transformers.

Custom Upload Form Type

We will start with something we have already covered: creating, and using a custom form type with EasyAdminBundle.

The reason we need to do this is because we will need to pass through some data to the 'update' form view, so that we can correctly display our Wallpaper's image file.

<?php

// /src/AppBundle/Form/Type/CustomFileType.php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;

class CustomFileType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('file', FileType::class);
    }
}

Feel free to call the form type anything you like.

At this stage there should be nothing new to you here. Sure, it may look a little funky / scary, but this is a typical Symfony form class. Over time, you will come to feel comfortable here :)

We have a file property on our entity. Heck, you just need to take a peek at our previous videos to know all about that :)

We're telling Symfony the our file field will be of a File upload form field, by way of the FileType::class type argument.

We can immediately start using this form by updating our EasyAdminBundle config:

easy_admin:
    entities:
        Wallpaper:
            class: AppBundle\Entity\Wallpaper
            form:
                fields:
                    - { property: "file", type: AppBundle\Form\Type\CustomFileType, label: "File" }
                    - "slug"

I've removed bits there for brevity.

The gist is this is entirely the same process as we did during our test-driven approach. As a result, this has broken the site in the same way that we saw in that video: the upload field now expects to receive an array.

For me, this is where tests start to come into play on a larger scale.

In my head I'm ok with one small part of the system being complicated, but untested. In this instance, this would have been the whole "create a wallpaper" section of the site. I feel worried about this, but still confident enough that it's just about small enough to stick in my head, or be relatively easy to 'grok' it again at a future date.

And then we immediately need to make the whole process just-that-little-bit-more complex.

And so on, and so on, and about 5 or 6 of these little extra bits of complexity down the line, I'm suddenly overwhelmed by the system as a whole, and start to lose confidence in my ability to change stuff without breaking existing features.

That's pretty much the main reason I started testing - to address that problem.

Anyway, back to the show.

We already know a way to fix this.

We will start by defining our own custom form theme:

<!-- /app/Resources/views/Form/fields.html.twig -->

{% block custom_file_widget %}
    {% spaceless %}
        <div class="custom-file">
            {{ form_widget(form.file) }}
        </div>
    {% endspaceless %}
{% endblock %}

Don't forget to add this new form theme to the list of ones that Symfony know or cares about:

# app/config/config.yml

twig:
    # stuff symfony provides by default
    debug: '%kernel.debug%'
    strict_variables: '%kernel.debug%'
    # add this in
    form_themes:
        - 'Form/fields.html.twig'

Let's just cover some interesting bits here.

Our form type is called CustomFileType. Our widget, by convention, needs to be in a corresponding twig block named custom_file_widget.

This custom_file_widget name is made up by taking the form's block prefix, and then concatenating it with _widget.

If you think custom_file_widget is a horrible name, or you want to be more explicit, you can override the block prefix in your form type, and then you must update the block name in your twig template:

<?php

// /src/AppBundle/Form/Type/CustomFileType.php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;

class CustomFileType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('file', FileType::class);
    }

    public function getBlockPrefix()
    {
        return 'wuggles'
    }
}

Enjoyable wuggles for everyone.

<!-- /app/Resources/views/Form/fields.html.twig -->

{% block wuggles_widget %}
    {% spaceless %}
        <div class="custom-file">
            {{ form_widget(form.file) }}
        </div>
    {% endspaceless %}
{% endblock %}

Enough detour.

Our form now looks right.

Don't be fooled, this is only superficially good looking.

If we try to submit the form, it's still got the same error message:

Expected argument of type "Symfony\Component\HttpFoundation\File\UploadedFile", "array" given 500 Internal Server Error - InvalidArgumentException

Let's look at a different approach to solving this problem.

Our Wallpaper entity expects to be given just a single instance of an UploadedFile. Remember we are tying ourselves quite heavily to Symfony in this version of the code.

Because of the way we are using EasyAdminBundle's form.fields (by using a custom form), somehow our form actually ends up thinking we accept an array of $files for this input. This is confusing. I believe [this ticket] may relate to the future of this problem.

We therefore need a way to pro-actively convert (or, transform) a form submission from what we get, to what we want.

To me, this feels like an elegant solution to a problem we shouldn't have.

We can use a Data Transformer to convert the data used in code to a format that can be rendered in the form.

Equally, we will provide a reversing function for that process. We will provide a function that converts the data from the form to a format that can be used in code.

<?php

// /src/AppBundle/Form/DataTransformer/FileTransformer.php

namespace AppBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\HttpFoundation\File\File;

class FileTransformer implements DataTransformerInterface
{
    /**
     * converts the data used in code to a format that can be rendered in the form
     */
    public function transform($file)
    {
        return [
            'file' => null,
        ];
    }

    /**
     * converts the data from the form submission to a format that can be used in code
     */
    public function reverseTransform($data)
    {
        return $data['file'];
    }
}

Even though there's very little code here, the possibility for confusion is high.

The current name of the file (FileTransformer) does not help.

If you take a look at the official docs, they have a decent example of a Data Transformer from which to copy (or at least, read through) and use as a good starting point. Their example covers more, so be sure to at least give it a skim, if you haven't already done so.

Then, at the very bottom, there's a section giving a little more insight into the data transformer. They split into more specific transformers - 'Model Transformers', and 'View Transformers'.

From the docs:

The two different types of transformers help convert to and from each of these types of data:

Model transformers:

  • transform(): "model data" => "norm data"
  • reverseTransform(): "norm data" => "model data"

View transformers:

  • transform(): "norm data" => "view data"
  • reverseTransform(): "view data" => "norm data"

Looking at our problem, the definition for when to use a View Transformer makes more sense to me.

Our data in our model (our code) is right. We want to work with one file object, not an array.

We need to make Symfony bend to do our bidding, not the other way round. Therefore, let's push the problem back towards the framework. We're going to fix this as a problem with the view layer, not within our internal domain.

Let's rename our class accordingly:

FileTransformer > UploadedFileViewTransformer

Let's also add in some extra detail to our docs, to flesh out what's happening:

<?php

// /src/AppBundle/Form/DataTransformer/UploadedFileViewTransformer.php

namespace AppBundle\Form\DataTransformer;

use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\HttpFoundation\File\File;

class UploadedFileViewTransformer implements DataTransformerInterface
{
    /**
     * converts the data used in code to a format that can be rendered in the form
     *
     * @var $file mixed|null
     */
    public function transform($file = null)
    {
        return [
            'file' => $file,
        ];
    }

    /**
     * converts the data from the form submission to a format that can be used in code
     *
     * @var $data array
     */
    public function reverseTransform($data)
    {
        return $data['file'];
    }
}

Whether creating, or updating, we are never going to repopulate the file field. This won't mean an existing image for this Wallpaper gets deleted when we submit the form. This is kinda confusing behaviour, I feel.

When our form is submitted, the reverseTransform function will ensure that from the provided $data array (remember, our form is using an array) we only return the one field we care about - file.

To me, even though it's very little code, it's confusing to work with. Watch the video for a deeper dive into this concept.

Now we need to add this Data Transformer to our form definition:

<?php

// /src/AppBundle/Form/Type/CustomFileType.php

namespace AppBundle\Form\Type;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use use AppBundle\Form\DataTransformer\UploadedFileViewTransformer;

class CustomFileType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('file', FileType::class);

        $builder->addViewTransformer(new UploadedFileViewTransformer());
    }

    public function getBlockPrefix()
    {
        return 'custom_file'
    }
}

The good news at this point is that our "Create" process still works.

Wait, weren't we working on 'Update' this whole time?

Yes! And that's a key point - if we have no automated tests, we have to check everything manually each time we change stuff that's at all related.

If we try the update process now, it allows a form submission, it even updates all the 'text'-y fields. But does it upload and replace the original image? Does it pump!

We kinda already knew this though. Our WallpaperUploadListener so far only has the prePersist method implemented.

We are going to need to add preUpdate to the mix as well. But that's not quite as straightforward as it might seem.

And that, and more are what we are going to cover in the very next video.

Code For This Course

Get the code for this course.

Code For This Video

Get the code for this video.

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