We should be feeling pretty happy with ourselves right now. We've seemingly significantly simplified our code by using this interface based approach.

Our tests reliably and reproducibly prove that so long as we provide the expected inputs, we should get the expected outputs / outcomes. This is a nod to the fact that this function is not pure, and is therefore much harder to test.

What we now need to do is create an implementation of our FileInterface that is compatible with Symfony's view of the world.

We're now relying on receiving a FileInterface, but as we know, Symfony's FileType form field type will give us a pesky UploadedFile instance instead.

We're going to need a way to transform Symfony's UploadedFile instance into some class that we control.

In order for our system to perform as expected, this new class that we control will also need to implements FileInterface.

The way I am going to approach this problem is to leverage the options available in Symfony's form component.

One of the concepts available by way of using the form component is that of Data Transformers.

What this will allow us to do is to accept a Symfony UploadedFile instance, and as part of the form submission, transform this into some class that we control.

We will make sure that this class implements FileInterface and we should have just nailed our first requirement.

Please welcome your own, your very own, SymfonyUploadedFile

Let's get PhpSpec to provide the needful:

php vendor/bin/phpspec desc AppBundle/File/SymfonyUploadedFile

Specification for AppBundle\File\SymfonyUploadedFile created in /path/to/my/wallpaper/spec/AppBundle/File/SymfonyUploadedFileSpec.php.

And we can immediately run that new spec to make sure our SymfonyUploadedFile implementation is added for us, too:

php vendor/bin/phpspec run

AppBundle/File/SymfonyUploadedFile
  11  - it is initializable
      class AppBundle\File\SymfonyUploadedFile does not exist.

                                 85%                                     14%     7
3 specs
7 examples (6 passed, 1 broken)
80ms


  Do you want me to create `AppBundle\File\SymfonyUploadedFile` for you?
                                                                         [Y/n]
y

Class AppBundle\File\SymfonyUploadedFile created in /Users/Shared/Development/wallpaper/src/AppBundle/File/SymfonyUploadedFile.php.

                                      100%                                       7
3 specs
7 examples (7 passed)
30ms

Nice.

Open up the spec and let's make some quick changes, based on what we already know:

<?php

// /spec/AppBundle/File/SymfonyUploadedFileSpec.php.

namespace spec\AppBundle\File;

use AppBundle\File\SymfonyUploadedFile;
use AppBundle\File\FileInterface;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class SymfonyUploadedFileSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(SymfonyUploadedFile::class);
        $this->shouldImplement(FileInterface::class);
    }
}

And we run this spec, and it creates the file for us, and we need to make sure we go into the newly created class and implements FileInterface.

But here we hit on a problem.

When implementing the expected methods from FileInterface on our SymfonyUploadedFile concept, things get messy:

<?php

namespace AppBundle\File;

use AppBundle\Model\FileInterface;

class SymfonyUploadedFile implements FileInterface
{
    public function getExistingFilePath()
    {
        // TODO: Implement getExistingFilePath() method.
    }

    public function getNewFilePath()
    {
        // TODO: Implement getNewFilePath() method.
    }
}

Why would a File know anything about how to get its own new path?

The name getExistingFilePath also doesn't make sense - what else could this path be in the context of a file? It will always have a current path...

This isn't bad. It's really, really good.

Useful knowledge is being revealed to us. This knowledge will help us better model our system.

Let's rethink these method names and update the interface:

<?php

// src/AppBundle/Model/FileInterface.php

namespace AppBundle\Model;

interface FileInterface
{
    public function getPathname();
    public function getFilename();
}

We weren't overly specific on the implementation for SymfonyUploadedFileSpec, so there's not test changes to make. Inside the implementation, things now make a little more sense:

<?php

namespace AppBundle\File;

use AppBundle\Model\FileInterface;

class SymfonyUploadedFile implements FileInterface
{
    public function getPathname()
    {
        // TODO: Implement getPathname() method.
    }

    public function getFilename()
    {
        // TODO: Implement getFilename() method.
    }
}

In order to get access to these two pieces of information, our SymfonyUploadedFile will need access to Symfony's UploadedFile.

It would be really nice here to be able to use a constructor to guarantee that we always have a FileInterface implementation set when working with this class.

Unfortunately, this class needs to interact with Symfony's form component, and that involves us using a setter.

<?php

// /src/AppBundle/File/SymfonyUploadedFile.php

namespace AppBundle\File;

use AppBundle\Model\File;
use Symfony\Component\HttpFoundation\File\UploadedFile;

class SymfonyUploadedFile implements FileInterface
{
    /**
     * @var UploadedFile
     */
    private $uploadedFile;

    /**
     * @return UploadedFile
     */
    public function getFile()
    {
        return $this->uploadedFile;
    }

    public function setFile(UploadedFile $uploadedFile)
    {
        $this->uploadedFile = $uploadedFile;
    }

    public function getPathname()
    {
        return $this->uploadedFile->getPathname();
    }

    public function getFilename()
    {
        return $this->uploadedFile->getClientOriginalName();
    }
}

An improvement we might make here would be to try / catch the call to $this->uploadedFile in our two interface requirement methods.

EasyAdminBundle Custom Upload Form

We are a good way through our task, but still, there is work to be done.

EasyAdminBundle provides us with a way to add an "upload" input. We already have this:

# /app/config/config/easy_admin_bundle.yml

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

However, now we have a new problem.

We need a way to add our Data Transformer to our form, in order to intercept the process involved in getting from form submission, to entity we can work with in our code.

Behind the configuration, EasyAdminBundle is managing the creation of the various forms we desire.

We can customise this process. And indeed we shall, by creating a new custom Form Field Type of our own to represent uploads in a way we control.

<?php

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

namespace AppBundle\Form\Type;

use AppBundle\File\SymfonyUploadedFile;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

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

    public function configureOptions(OptionsResolver $optionsResolver)
    {
        $optionsResolver->setDefaults([
            'data_class' => SymfonyUploadedFile::class,
        ]);
    }
}

Our SymfonyUploadedFile object contains a single property: file.

We expect to be able to set a file of type UploadedFile - the Symfony variant:

    public function setFile(UploadedFile $uploadedFile)

This tiny object enables us to transfer data in a manner we control, rather than being beholden to Symfony.

To use this new UploadedFileType in our EasyAdminBundle configuration, we do need to make a change:

# /app/config/config/easy_admin_bundle.yml

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

What's fairly incredible to me - pretty much every time I write tests like this - is that when we finally come to hook everything together, things tend to work correctly the first time, more often that not.

After all this, we haven't even tried the front end code for a long time now, and we've certainly made our fair share of change.

Yet hooking up this new uploaded file type gets us almost all of the way there:

An exception occurred while executing 'INSERT INTO wallpaper (filename, slug, width, height, category_id) VALUES (?, ?, ?, ?, ?)' with params [null, "some-slug", 11, 11, null]:

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'filename' cannot be null

Just like when we did things without tests, we still need to bring in some extra data such as the width and height and set the filename, and all that.

That's what we will get on to, in the very next video.


Code For This Course

Get the code for this course.

Code For This Episode

Get the code for this episode.

Share This Episode

If you have found this video helpful, please consider sharing. I really appreciate it.


Episodes in this series

# 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:56
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:32
19 No Tests - Part 1 - Uploading Files in EasyAdminBundle 11:02
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:50
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:44
36 Test Driven Wallpaper Delete - Part 1 11:11
37 Test Driven Wallpaper Delete - Part 2 11:57
38 EasyAdminBundle Login Form Tutorial 08:01