Wallpaper Setup Command - Part 2 - Injection Is Easy


To begin with we need to find all the images inside our web/images directory. We could do this manually, but ... let's not.

Instead, we will use the glob function to find all the files for us. Assuming we don't get this wrong, we should expect to receive an array containing all the found files.

However, there is a little gotcha here.

To glob, we must provide a valid path to the location of our files. We know that part of this path is web/images, and that web is a directory inside the root of our Symfony project.

The naive way to do this is to hardcode the path. If I did this, if you were to copy / paste the code from my implementation into your own, it very likely would not work. My project might be at:

/home/chris/development/wallpaper/web/images

And you might be on Windows, or OSX, and the section of the path up until web/images is just not going to be the same.

Let's pretend that on your computer this path is:

/Users/chris/Projects/wallpaper-demo/web/images

Even so, you may be thinking - well, I can hardcode it on mine anyway as it will only ever run on my computer. But hold on. What if you deploy this code, as we will do later. The path is highly likely not the same on your server either.

Anyway, enough of this. Symfony has a solution to this very problem.

For the purposes of this video we are using Symfony 3.2. At the time of recording, very shortly Symfony 3.3 will be released which has a nicer solution to this problem. However, if you are on Symfony 3.2 or lower we must use kernel.root_dir.

kernel.root_dir is a special variable that will resolve to the location of the AppKernel.php file. By default AppKernel.php lives inside the projects /app directory. This will have an impact on how we use this variable shortly.

If you are following along at a future date, then new in Symfony 3.3 is the inclusion of the kernel.project_dir variable.

This is a special variable provided by Symfony which points to the root directory of our project. That is, unless you have moved your composer.json file, then you will need to do some tweaking.

In our example, on my computer the kernel.root_dir would resolve to:

/home/chris/development/wallpaper/app

We might pretend that you're using OSX, and in your case kernel.root_dir would resolve to:

/Users/chris/Projects/wallpaper-demo/app

By comparison, as of Symfony 3.3 onwards, on my computer the kernel.project_dir would resolve to:

/home/chris/development/wallpaper

and our pretend example from your computer, this same variable would resolve to:

/Users/chris/Projects/wallpaper-demo

Very handy.

However, we can't just start using this in our console command. Symfony is not magic.

To use this parameter we must inject it.

This might sound complicated, but hopefully it won't be complicated once you've seen it in action. There are two steps. You can do these in any order, but both must be done.

First, I'm going to update the service definition to specify that we want to use the %kernel.project_dir% as an inject constructor argument:

# /app/config/services.yml

services:

    # Symfony 3.2 or below
    app.command.wallpaper_setup:
        class: AppBundle\Command\WallpaperSetupCommand
        arguments:
            - "%kernel.root_dir%"
        tags:
            - { name: console.command }

    # Symfony 3.3 onwards
    app.command.wallpaper_setup:
        class: AppBundle\Command\WallpaperSetupCommand
        arguments:
            - "%kernel.project_dir%"
        tags:
            - { name: console.command }

The new section here is arguments. It's plural even though we have only one argument at this time.

Notice that the "%kernel.root_dir%" and "%kernel.project_dir%" entries are wrapped in percentage symbols? This indicates to Symfony that these are parameters, not services. This distinction is important.

As a side note, you could inject any of your existing parameter from parameters.yml in this same way.

This arguments key expects a sequence. A sequence in a YAML file will become an array in a PHP file.

This is a YAML file so of course, there's more than one way to represent a sequence.

I prefer to list my entries out one per line, but you can use brackets notation also:

# /app/config/services.yml

services:

    # Symfony 3.2 or below
    app.command.wallpaper_setup:
        class: AppBundle\Command\WallpaperSetupCommand
        arguments: ["%kernel.root_dir%"]
        tags:
            - { name: console.command }

    # Symfony 3.3 onwards
    app.command.wallpaper_setup:
        class: AppBundle\Command\WallpaperSetupCommand
        arguments: ["%kernel.project_dir%"]
        tags:
            - { name: console.command }

Same thing.

More info here, and here. Look for 'sequence' - I can't do a direct link to the appropriate examples unfortunately.

That's our service definition updated.

We have specified that we want to pass in an argument to our WallpaperSetupCommand. When using arguments this implies we will pass in these arguments via the constructor.

Our service implementation does not yet have a constructor. We need to add one. Again, this varies slightly depending on Symfony 3.2 or below, or 3.3 or above:

<?php

namespace AppBundle\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class WallpaperSetupCommand extends Command
{
    /**
     * @var string
     */
    private $rootDir;

    public function __construct(string $rootDir)
    {
        parent::__construct();

        $this->rootDir = $rootDir;
    }

    // Symfony 3.3 approach
    // /**
    //  * @var string
    //  */
    // private $projectDir;
    //
    // public function __construct(string $projectDir)
    // {
    //     parent::__construct();
    //
    //     $this->projectDir = $projectDir;
    // }

    protected function configure()
    {
        $this
            // the name of the command (the part after "bin/console")
            ->setFilename('app:setup-wallpapers')

            // the short description shown while running "php bin/console list"
            ->setDescription('Grabs all local wallpapers and creates an entity for each one')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {

    }
}

If you're not using PHP7 (what?!) then remove the string scalar type hint from the construct method arguments.

And that's it. We now have access to this rootDir (or projectDir) inside our console command.

It is a bit weird the first (few) time(s) you do this admittedly, but it's one of those things that becomes quite natural with a bit of repetition. And of course, you always have this document as a reference.

Globing for Fun and Profit (and Images)

All that was a lot of work to enable us to glob properly.

But now, glob properly we can!

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // Symfony 3.2 or below
        $wallpapers = glob($this->root_dir . '/../web/images/*.*');

        // Symfony 3.3 or above
        $wallpapers = glob($this->projectDir . '/web/images/*.*');
    }

If we were to run this command at this stage, not a great deal will happen. Let's hook up a little manual check to ensure it is behaving as expecting:

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // Symfony 3.2 or below
        $wallpapers = glob($this->root_dir . '/../web/images/*.*');

        // Symfony 3.3 or above
        // $wallpapers = glob($this->projectDir . '/web/images/*.*');

        $wallpaperCount = count($wallpapers);

        $output->writeLn(sprintf('Found %d wallpapers', $wallpaperCount));
    }

Now if we run this, in our case we should expect to see: "Found 22 wallpapers", or similar.

Each of the entries in the $wallpapers array will be a string - the filename (including the file extension) of the found file.

Let's loop over this to new up some Wallpaper entities:

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $wallpapers = glob($this->root_dir . '/../web/images/*.*');

        foreach ($wallpapers as $wallpaper) {

            $wp = (new Wallpaper())
                ->setFilename($wallpaper)
                ->setSlug($wallpaper)
                ->setWidth(1920)
                ->setHeight(1080)
            ;

        }

    }

Not hugely exciting.

We already know our wallpaper dimensions as we grabbed them by size from Google in an earlier video. Therefore, for the moment at least, we will hardcode these two values.

The filename and slug properties can at this stage be the name of the given file, though this won't be accurate as the outcome will be the full path to the image, akin to:

/home/chris/development/wallpaper/app/../web/images/summer-picture-of-tree.jpg

Before we fix up these issues, we best tackle the next major hurdle: saving this new $wp (wallpaper entity) to the database.

To do this we need access to the entity manager.

How do we get access to the entity manager inside a Symfony console command?

We inject it, of course!

We've already seen how to do this with our kernel.root_dir (or kernel.project_dir) variable. Injecting a service is almost identical.

Remember when injecting the %kernel.{whatever}% parameter that we needed to wrap the parameter in percentage symbols?

Well, for a service we need to prefix with an @ symbol instead. Note - prefix, not wrap!

As a tip here, if using the PhpStorm Symfony plugin, setting up services comes with some nice autocompletion. If not, a quick way to figure out the right service to use is to debug:container:

php bin/console debug:container entity

 Select one of the following services to display its information:
  [0] form.type.entity
  [1] doctrine.orm.default_entity_listener_resolver
  [2] doctrine.orm.default_listeners.attach_entity_listeners
  [3] doctrine.orm.default_entity_manager
  [4] doctrine.orm.default_entity_manager.property_info_extractor
  [5] doctrine.orm.entity_manager
 > 5

 // This service is an alias for the service doctrine.orm.default_entity_manager

We need to use this service id as our second argument:

# /app/config/services.yml

services:

    # Symfony 3.2 or below
    app.command.wallpaper_setup:
        class: AppBundle\Command\WallpaperSetupCommand
        arguments:
            - "%kernel.root_dir%"
            - "@doctrine.orm.default_entity_manager"
        tags:
            - { name: console.command }

    # Symfony 3.3 onwards
    app.command.wallpaper_setup:
        class: AppBundle\Command\WallpaperSetupCommand
        arguments:
            - "%kernel.project_dir%"
            - "@doctrine.orm.default_entity_manager"
        tags:
            - { name: console.command }

And with that, we can update our WallpaperSetupCommand::__construct method:

<?php

namespace AppBundle\Command;

use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class WallpaperSetupCommand extends Command
{
    /**
     * @var string
     */
    private $rootDir;

    /**
     * @var EntityManager
     */
    private $em;

    public function __construct(string $rootDir, EntityManagerInterface $em)
    {
        parent::__construct();

        $this->rootDir = $rootDir;
        $this->em = $em;
    }

And now we can start using the entity manager :)

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $wallpapers = glob($this->root_dir . '/../web/images/*.*');

        foreach ($wallpapers as $wallpaper) {

            $wp = (new Wallpaper())
                ->setFilename($wallpaper)
                ->setSlug($wallpaper)
                ->setWidth(1920)
                ->setHeight(1080)
            ;

            $this->em->persist($wp);
        }

        $this->em->flush();
    }

You should be able to run this command now, and aside from polluting your wallpaper table with mess, it should work.

Note that we call persist inside the foreach loop, but only flush the change once at the end of the loop. This is intentional.

Persisting the entity does not actually save the entity to the database. It alerts Doctrine that this entity should be saved the next time changes are flushed. This has some interesting implications in more complex applications, though is currently not something we need to concern ourselves with.

Lastly, we call flush to save off (or insert) all 22 entities to the wallpaper table in one go. More info here.

The data added to your database will be messy and not very useful at this stage. However, we have proved that this command will work. What we now need to do is figure out how to get back some useful data, without relying on us providing it file by file.

That is going to be our very next task.

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