A large part of our Wallpaper website involves us, unsurprisingly, working with images.

Whether you're working with images, PDFs, audios, movies, or any of the other types of file, the common thing is that they all need some way of being uploaded.

Previously I mentioned how tended to use a database GUI client as my "admin panel" for as long as I could get away with. When it's just you working on a project, maybe working with a database client is an acceptable alternative to a full-blown admin dashboard.

Likewise, if it's just you working on your project, maybe using some file transfer system is an acceptable way of uploading images.

However, sooner or later you will likely want to make your site that little bit nicer to work with, and implement a way to upload new Wallpapers from the back end.

Before we go further though - this is development. There are - in most cases - more than one way to fix a particular problem.

File uploads, or in our case, uploading Images is a very common problem.

As such, smart developers congregate together and come up with solutions to these problems.

I'm all for using these solutions but only if I understand - in some capacity - how they operate beneath the surface.

See, the thing is - from my experience - things like this become very frequently used parts of my applications. After all, frequently I need to upload images to keep my site fresh, and if one way is clunky, and another is streamlined, I'm going to not only do the streamlined one, but I'm likely going to start doing it more often.

Which will mean this code is going to get hammered.

Which almost certainly will mean more bugs are noticed, and they will need fixing.

I don't mind fixing bugs. It is a form of coding, if not the most enjoyable. And I love coding. Any excuse, and all that.

But the thing is, if I don't really know what this code is doing, then fixing those bugs is going to take longer and make me feel frustrated. I'd rather be working on the more interesting new features.

Why I'm telling you this is because there already exists a bundle which largely takes care of this process for you. It's called VichUploaderBundle.

EasyAdminBundle docs come with a guide to integrate with VichUploaderBundle.

We are not going to use this bundle.

We are going DIY.

Form A Line

We need to allow ourselves, and other admin users to post in (Create) new wallpapers.

This needs to include the slug, the width and the height, stuff like that.

Now, in truth, getting our user's to do most of this donkey work is pointless as this info can be determined in code.

This is an improvement we will make later.

For now, we need a rudimentary working system.

At this point we can boil this down to a form.

How the data gets into that form is not our concern. It might be from a phone. It might be from your car's dashboard. It might be from some front end code we write (hint: very likely this one).

If we aren't doing anything out of the ordinary, we can make use of some further easy_admin config to create a form for us.

We need to tell it what fields we have, and it will take care of the rest.

There are ways we can hook into this process.

We could override this method with our own implementation. We don't need to do any of that.

Our circumstances are normal for what this bundle expects.

# /app/config/config/easy_admin_bundle.yml

easy_admin:
    entities:
        Category:
            class: AppBundle\Entity\Category
        Wallpaper:
            class: AppBundle\Entity\Wallpaper
            list:
                fields:
                    - 'id'
                    - 'filename'
                    - 'slug'
                    - 'width'
                    - 'height'
                    - { property: 'image', type: 'image', base_path: '/images/' }
            form:
                fields:
                    - 'slug'
                    - 'width'
                    - 'height'

Note here that form comes under Wallpaper, not under list - indentation can be a source of bugs.

When we go to create a new Wallpaper now, we see only three fields:

  • slug
  • width
  • height

Trying to submit this form will result in a SQL constraint violation.

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

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'filename' cannot be null
500 Internal Server Error - NotNullConstraintViolationException
2 linked Exceptions: PDOException » PDOException »

We could add in a filename form field, and expect the user to provide this information. This isn't a bad way to test that this component behaves as expected.

It would be nicer to have a facility on the form which allowed the user to directly upload a new file.

Let's go at this top down.

To allow a user to upload a file from a HTML web page, we need to display an <input type="file"/> on a form. Likely we will have more properties etc, but that's the gist of it.

To do this if we were using our own Symfony form setup is a process you may or may not be familiar with. It's one of earliest Cookbook entries I remember reading - quite a few times over as I recall :)

We won't be setting up a bunch of new form types here though.

Instead we will provide our forms as YAML configuration.

We can easily create a form with an <input type="file"/> under list:

# /app/config/config/easy_admin_bundle.yml

easy_admin:
    entities:
        Category:
            class: AppBundle\Entity\Category
        Wallpaper:
            class: AppBundle\Entity\Wallpaper
            list:
                fields:
                    - 'id'
                    - 'filename'
                    - 'slug'
                    - 'width'
                    - 'height'
                    - { property: 'image', type: 'image', base_path: '/images/' }
            form:
                fields:
                    # new line below
                    - { property: 'file', type: 'file', label: 'File' }
                    - 'slug'
                    - 'width'
                    - 'height'

This introduces a new concept. Similar to when we added the image property to our list view, here we have just invented this concept of a file without any code to make it work.

The file property isn't on our Wallpaper yet. We need to add it.

    /**
     * @ORM\Column(type="string")
     */
    private $file;

    // also generate getters and setters

How we get to an @ORM\Column(type="string") is going to be more involved than if we were posting in a plain old text field.

The process will be this:

We will have a form that accepts submissions that contain files.

When the form is submitted, one of the fields (the file field) will contain the file data that is going to get set onto the Wallpaper entity.

Other stuff might happen here courtesy of events we may not directly control. This happens all the time in Symfony, btw.

We need to take ownership of this file data.

As part of the upload process, PHP will have stored off our file for us in a directory under its own control.

We will need to move this file from the directory that PHP has temporarily stored it in, into the web/images directory. Of course, this directory can be any other directory you need to use.

Following the guidelines set out in the file upload Cookbook entry, we will do this process using a Doctrine lifecycle callback.

When it comes time to save this entity off to the database, only then will we move the file from wherever that PHP is temporarily storing it, into a path on disk that we control. In our case this path will be:

"%kernel.root_dir%/../web/images/"

If you haven't yet seen, we covered both kernel.root_dir, and it's new-in-Symfony 3.3 more friendly brother, kernel.project_dir previously.

As part of this process we can set other properties - such as the filename - dynamically.

We're about to do a bunch of work here that could become a late night, stressed out headache if we don't get it right. Whenever I hit code like this, I bust out the unit tests.

In order to test this process I'm going to use PhpSpec.

composer require --dev phpspec/phpspec

Also be sure to update composer.json with the required autoload setting:

"autoload": {
    "psr-0": {
        "": "src/"
    }
}

Official installation docs.

Fortunately that's us done with installing PhpSpec. Nice and easy.

Next, we're going to start describing the way in which we expect this process to flow.

php vendor/bin/phpspec desc AppBundle/Service/FileMover

Specification for AppBundle\Service\FileUploader created in /path/to/project/wallpaper/spec/AppBundle/Service/FileMoverSpec.php.

PhpSpec has kindly generated this new Spec file for us. In this file we will write out the way in which we expect this class to work. This approach is somewhat different to most any test library I have ever used.

We could either jump into the IDE now and open up the test file, or we could run our new test straight away and let PhpSpec do just a little extra work for us:

php vendor/bin/phpspec run
AppBundle/Service/FileMover
  11  - it is initializable
      class AppBundle\Service\FileMover does not exist.

                                      100%                                       1
1 specs
1 example (1 broken)
24ms


  Do you want me to create `AppBundle\Service\FileMover` for you?
                                                                         [Y/n]

Even though our FileMover class doesn't yet exist, PhpSpec has written an assertion to check that it does.

When we run the test, PhpSpec spots that the file itself does not exist, and asks if it should create it for us.

Yes please! One less step for me.

I press y to accept:

Class AppBundle\Service\FileMover created in /path/to/project/wallpaper/src/AppBundle/Service/FileMover.php.

                                      100%                                       1
1 specs
1 example (1 passed)
13ms

The output is unfortunately not that nice when copy / pasted, but on your terminal it's quite pleasant.

Now we have both a spec:

/path/to/project/wallpaper/spec/AppBundle/Service/FileMover.php

and an implementation:

/path/to/project/wallpaper/src/AppBundle/Service/FileMover.php

The idea here is to write out the specification and let PhpSpec do as much of the hard work as possible.

We know - either from a prototype, or from reading the Cookbook - that we will need to move the uploaded file from the temporary location into the location we control.

As a spec, this might look like this:

<?php

// /spec/AppBundle/Service/FileMover.php

namespace spec\AppBundle\Service;

use AppBundle\Service\FileMover;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class FileMoverSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(FileMover::class);
    }

    function it_can_move_a_file_from_temporary_to_controlled_storage()
    {
    }
}

There's a lot to take in with PhpSpec. One of the immediately noticeable things with a spec is that the functions are in snake_case. Also, notice they have no visibility modifiers - no public, private, or protected.

The it_is_initializable function is provided for us. Any new spec generated by PhpSpec's desc command line utility will contain this for us.

This is the line responsible for helping generate the FileMover implementation we saw above.

Notice that we use $this, rather than a new instance of FileMover. Again, a potential source of confusion to begin with. However, I believe the way that PhpSpec works will become clearer as we progress through.

I've added in the new function:

it_can_move_a_file_from_temporary_to_controlled_storage

My assertion here is that given a path to a temporary file, and a permanent path to a location on disk that I want to move this temporary file into, then the FileMover should be able to move the file for me, and return true when done.

There are a whole bunch of ways we could interact with the file system. We could use:

  • PHP's built in functionality;
  • Guafrette / FlySystem
  • Symfony's Filesystem component

We're using the Symfony framework, so let's make use of what we already have and use the Filesystem component as our implementation.

At this point you may wish to abstract further and create your own interface to which the Symfony Filesystem is just one possible implementation. I'm not going to do that because in this instance it is overkill, in my opinion. However, this would be a fairly simple - and isolated - change should I need to do so in the future.

Given that we want to move a file from one location to another, I'm going to add this to the spec.

I expect to have a current temporary path where the file currently resides on disk.

I also expect to have a path I want to store the file in.

My test therefore looks as follows:

<?php

// /spec/AppBundle/Service/FileMover.php

namespace spec\AppBundle\Service;

use AppBundle\Service\FileMover;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;

class FileMoverSpec extends ObjectBehavior
{
    function it_is_initializable()
    {
        $this->shouldHaveType(FileMover::class);
    }

    function it_can_move_a_file_from_temporary_to_controlled_storage()
    {
        $temporaryPath = '/some/fake/temporary/path';
        $controlledPath = '/some/fake/real/path.ext';

        $this->move($temporaryPath, $controlledPath)->shouldReturn(true);
    }
}

Now I go back to the command line and re-run the test:

php vendor/bin/phpspec run

AppBundle/Service/FileMover
  28  - it can move a file from temporary to controlled storage
      method AppBundle\Service\FileMover::move not found.

                  50%                                     50%                    2
1 specs
2 examples (1 passed, 1 broken)
10ms


  Do you want me to create `AppBundle\Service\FileMover::move()` for you?
                                                                         [Y/n]

There's no 2 examples (or tests) inside one spec file.

These are the two functions I have in the FileMoverSpec.

We saw how the first function was (and still is) passing.

The new example / function however, is failing.

This is to be expected. We have just added this as a brand new concept, so without any underlying implementation it rightly fails.

PhpSpec wants to help us.

If we want to be able to called move, then we must have a public function of move. It has asked if it should create one for us, to which I am going to accept (y):

y

  Method AppBundle\Service\FileMover::move() has been created.

AppBundle/Service/FileMover
  28  - it can move a file from temporary to controlled storage
      expected true, but got null.

                  50%                                     50%                    2
1 specs
2 examples (1 passed, 1 failed)
11ms

The test still fails, but if we look inside the implementation, we have a generated method stub we can start using:

<?php

// /AppBundle/Service/FileMover.php

namespace AppBundle\Service;

class FileMover
{
    public function move($argument1, $argument2)
    {
        // TODO: write logic here
    }
}

We need to tidy this up just a touch:

    public function move($existingFilePath, $newFilePath)
    {

To make our test pass, we could add in a simple return true; statement into our move method. This would satisfy the test, but it's not really adding any business value.

Instead, we need to start thinking about how files could be moved from one location to another. This is what we will get on to in the very next video.


Code For This Course

Get the code for this course.

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