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 flush
ed. 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.