Commands and Templates - Extending Symfony2 Web Application Framework (2014)

Extending Symfony2 Web Application Framework (2014)

Chapter 2. Commands and Templates

In this chapter, we will review two of the most common kinds of extensions that you will encounter while working on a Symfony project:

· Commands: They are similar to the ones that Symfony brings you, such as the ones already in the framework (cache:clear, doctrine:database:create, and so on)

· Twig: It's relatively easy to extend the templating language of Symfony as well

Commands

Symfony ships with a powerful console component. Just like many components in Symfony, it can also be used as a standalone component to create command-line programs. In fact, Composer (http://getcomposer.org), the dependency manager that you use every day with Symfony, has its command line-based on the Symfony Console component.

Let's find out how to create commands and what they are good for.

The initial situation

Our site users have a profile on the website. On their profile, they can upload their own picture (in any avatar). They can upload any kind of picture with different sizes and ratios, and the system will crop it and/or resize it to a square picture of 150 x 150 pixels. We always keep the higher resolution uploaded picture but pregenerate the 150-pixel one to improve the load speed of our site. Now that so many people are browsing our site from very high resolution tablets, we need to make that profile picture also available in 300 pixels size.

This is a relatively heavy task as it must apply to all of our users in one pass and involves image processing. This is also not something that should be available to our users, but only to the tech people; therefore, a controller doesn't seem like the right place for this functionality. Furthermore, this is probably a one-time thing, unless the process crashes in the middle or we need to have images of 600 pixels in a couple of months when even higher resolution displays appear! In this case, a Console command seems the appropriate place.

Resizing user pictures

We'll write our first command that just works on a single image and resizes it. To simplify the process of manipulating images, we will rely on the Imagine library (https://imagine.readthedocs.org/en/latest/). A command should extend the Symfony base command class. Within the framework, if you want to be able to use other services, it is easier to directly extend from Symfony. The two important functions in this class that you must define are configure() and execute(). Enter the following lines of code in the configure()function:

class ResizePictureCommand extends ContainerAwareCommand

{

protected function configure()

{

$this

->setName('picture:resize')

->setDescription('Resize a single picture')

->addArgument('path', InputArgument::REQUIRED,'Path tothe picture you want to resize')

->addOption('size', null, InputOption::VALUE_OPTIONAL,'Size of the output picture (default 300 pixels)')

->addOption('out', 'o', InputOption::VALUE_OPTIONAL,'Folder which to output the picture (default same asoriginal picture)')

;

}

In the preceding configure() function, we choose a command name, define the arguments (picture path), and some optional parameters. Now, our command can be invoked using the following command statement:

$./app/console picture:resize <path> (--size=) (--out|-o=)

Now, enter the following lines of code in the execute() function:

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

{

// Command line info

$path = $input->getArgument('path');

$size = $input->getOption('size') ?: 300;

$out = $input->getOption('out');

// Prepare image and resize tool

$imagine = new \Imagine\Gd\Imagine();

$image = $imagine->open($path);

$box = new \Imagine\Image\Box($size, $size);

$filename = basename($path);

// Resize image

$image->resize($box)->save($out.'/'.$filename);

$output->writeln(sprintf('%s --> %s', $path, $out));

}

}

In the execute() method, we receive an $input and $output argument representing the following:

· The command-line arguments we passed in as the input

· The console to which we can write information for the user

We get this information or replace it with the default ones using the Imagine image manipulation library and resize our picture. Finally, we output some information that tells us all went well.

Nothing extraordinary here, but this shows how we can create a simple command. Let's now try to apply that to all our users. We will create a command that browses through the list of our users and executes this command for each of them. To make things nice and simple, we won't ask the user to remember the order of arguments but display a series of questions on the console. We'll also add a progress bar, shown as follows, so the person using it knows how much is done or left to do:

class UpdateProfilePicsCommand extends ContainerAwareCommand

{

protected function configure()

{

$this

->setName('picture:profile:update')

->setDescription('Resizes all user\'s pictures to a newsize');

}

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

{

$dialog = $this

->getHelperSet()

->get('dialog');

$size = $dialog->ask($output, 'Size of the final pictures(300): ', '300');

$out = $dialog->ask($output, 'Output folder: ');

We use the dialog helper to display questions to the command-line user and get the necessary information.

$command = $this->getApplication()->find('picture:resize');

$arguments = array(

'command' => 'picture:resize',

'--size' => $size,

'--out' => $out

);

We get the command that we previously defined for resizing a single picture and prepare the arguments for calling the command. This is shown in the following code snippet:

// Get list of all users

$users = $this->getContainer()->get('fos_user.user_manager')

->findUsers();

$progress = $this->getHelperSet()->get('progress');

$progress->start($output, count($users));

Here, we use the progress helper that we saw earlier and set its maximum value as the total number of users in our database. You don't need to calculate percentages by yourself; just provide the total number of unit steps that will be processed and the helper will do the rest.

foreach($users as $user) {

// Run the picutre:resize command

$arguments['path'] = $user->getPicture();

$input = new ArrayInput($arguments);

$command->run($input, $output);

// Advance progress

$progress->advance();

}

// Show that the whole process was successful

$output->writeln('');

$output->writeln('<info>Success!</info>');

}

}

The output in our terminal should look like the following:

Resizing user pictures

The command runs successfully, and if you have enabled colored output in your console, the line saying Success! should appear in green. We should now test our command to ensure it behaves correctly.

Testing a command

As with everything in our application, we would feel more confident knowing that there are tests that ensure it runs smoothly. We can see that our picture:resize command will be, in a way, hard to test. We cannot really mock anything it's going to use as it doesn't take any PHP objects as a parameter; it only takes input strings. It looks like we'll need to actually resize a picture to test it in its current stage. Let's try that using the following lines of code:

use Symfony\Bundle\FrameworkBundle\Console\Application as App;

use Symfony\Component\Console\Tester\CommandTester;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

use Khepin\BookBundle\Command\ResizePictureCommand;

class ResizePictureCommandTest extends WebTestCase

{

public function testCommand()

{

$kernel = $this->createKernel();

$kernel->boot();

$application = new App($kernel);

$application->add(new ResizePictureCommand());

$command = $application->find('picture:resize');

$commandTester = new CommandTester($command);

$commandTester->execute([

'command' => $command->getName(),

'path' => __DIR__.'/fixtures/pic.png',

'-o' => __DIR__.'/fixtures/resized/'

]);

$this

->assertTrue(file_exists(__DIR__.'/fixtures/resized/pic.png'));

}

}

Note

It might seem weird to some of you that we extend this test class from WebTestCase and not from a standard PHPUnit_Framework_TestCase. This is mostly for convenience as the WebTestCase class gives us methods to directly access an initialized kernel. Otherwise, you would have to mock or create the kernel yourself.

To avoid messing with our whole application, we only test on a picture file that is inside our test folder in a fixtures subfolder.

Commands as an interface to services

We saw that our test is a bit special as it needs to actually resize a picture so that we can say it performed correctly. We can't pass it a mocked version of Imagine and check if the right calls to the library are made as we could have done if it were a service.

However, we saw that it is possible to call services from within a command, like when we used the fos_user.user_manager service to retrieve our list of users. We could, therefore, actually move all the core tasks performed by our command to a service and then have that command act only as an interface to input some arguments.

There are tremendous advantages in doing this, and we can only hope that more developers in the Symfony community start adopting this practice. It doesn't stop with testing. Opening a terminal is already a technical operation for many people. If this operation of resizing pictures becomes more frequent, why not have a web interface for starting the process that the site admins could use?

Loading fixture data in your database is something most developers would think of using, but again, you could benefit from having this defined as a service. It will be available from a controller when you want to prepopulate a new user's demo account.

I strongly encourage everyone to follow this practice of having very thin commands that are actually only an interface to something else. Let's do it right now and refactor our commands a bit.

We start with the picture:resize command and extract its logic to a service class.

namespace Khepin\BookBundle\Command;

class Shrinker

{

protected $imagine;

public function __construct($imagine)

{

$this->imagine = $imagine;

}

public function shrinkImage($path, $out, $size)

{

$image = $this->imagine->open($path);

$box = new \Imagine\Image\Box($size, $size);

$filename = basename($path);

$image->resize($box)->save($out.'/'.$filename);

}

}

The configuration for that new service is the following:

imagine:

class: Imagine\Gd\Imagine

khepin_book.shrinker:

class: Khepin\BookBundle\Command\Shrinker

arguments: [@imagine]

Our command then becomes as follows:

$path = $input->getArgument('path');

$size = $input->getOption('size') ?: 300;

$out = $input->getOption('out');

$this->getContainer()->get('khepin_book.shrinker')->shrinkImage($path, $out, $size);

$output->writeln(sprintf('%s --> %s', $path, $out));

As you can see, it is now only a very thin wrapper around our service. So thin indeed, that it starts feeling weird having all this complexity in our command that resizes all the user's pictures. It doesn't matter if we keep our command to resize only one picture; thepicture:profile:update command directly calls the shrinker service. This is shown in the following code snippet:

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

{

$dialog = $this->getHelperSet()->get('dialog');

$size = $dialog->ask($output, 'Size of the final pictures(300): ', '300');

$out = $dialog->ask($output, 'Output folder: ');

// start shrinking

$users = $this

->getContainer()

->get('fos_user.user_manager')->findUsers();

$progress = $this->getHelperSet()->get('progress');

$progress->start($output, count($users));

foreach($users as $user) {

$path = $user->getPicture();

$this

->getContainer()

->get('khepin_book.shrinker')

->shrinkImage($path, $out, $size);

// Advance progress

$progress->advance();

}

// finish shrinking

// Show that the whole process was successful

$output->writeln('');

$output->writeln('<info>Success!</info>');

}

As an added benefit, services are only created once and then reused. We no longer create an instance of Imagine for each picture resize or for one instance of the simple command. We always have access to the same one. In fact, we could again reduce the size of our command and move more logic to a service that would then be reusable. All the code between the // start shrinking and // finish shrinking comments should be as follows:

$this

->getContainer()

->get('khepin_book.user_manager')

->resizeAllPictures($size, $out);

If this service was sending events, as we saw in the previous chapter, you could still get the progress information, and it could now be used directly outside of a command.

Twig

By default, Symfony ships with the Twig templating system. Twig is incredibly powerful and out of the box. The possibilities offered by blocks, extending templates, including templates, and macros are huge and will be enough for most cases. There are cases where you still need something more though, and an extension for Twig is the only elegant way of doing so.

Twig offers five different ways to create extensions:

· Globals: This lets you define some global variables that are available in all templates. You could access them like any other variable.

· Functions: This will let you write {{my_function(var)}}.

· Tests: These are specific functions that return Boolean values and can be used after the is keyword in templates.

· Filters: They modify the output of an output tag.

· Tags: This will let you define custom Twig tags.

Some of the pages on our website will require some JavaScript in them to make them a bit more dynamic or simple to use. The form to create a meetup for organizers will definitely use a datepicker. The events page might display a map from Google or Bing's APIs. We are not creating a complete JavaScript application, just adding the bits we need here and there.

Managing our scripts

To improve the perceived page-load speed, it's usually good to load all our scripts at the end of the page. However, if we output the tag for the datepicker files to be loaded in the same template where we have the datepicker, things become more manageable. This is because when we decide to remove or change it, we don't need to remember it.

So, while rendering the templates, we'd prefer if there was a way to write a tag for the JavaScript to be loaded, but actually have the output of that tag be somewhere at the bottom of our generated HTML page. As Twig cannot deal with this, we'll create an extension for it.

class KhepinExtension extends \Twig_Extension

{

protected $javascripts = [];

public function getFunctions()

{

return [

new \Twig_SimpleFunction('jslater', [$this, 'jslater'])

];

}

public function jslater($src)

{

$this->javascripts[] = $src;

}

public function getName()

{

return 'khepin_extension';

}

}

We start with this simple extension. It declares a Twig function that will remember the source path for any JavaScript tag that is passed to it. In our templates, we use it as follows:

{{jslater('web/scripts/datepicker.js')}}

We now need Twig to be aware of the existence of this extension. How is this done? You guessed it, by making our extension a service and giving it the proper tag. To do so, use the following lines of code:

khepin.twig.khepin_extension:

class: Khepin\BookBundle\Twig\KhepinExtension

tags:

- { name: twig.extension }

The first part of our extension is working, so now we need to be able to output a <script> tag for each of the scripts that we collected using the following lines of code:

public function getFunctions()

{

return [

new \Twig_SimpleFunction('jslater', [$this, 'jslater']),

new \Twig_SimpleFunction('jsnow', [$this, 'jsnow'])

];

}

public function jsnow()

{

//...

}

In here, we would like to use the power of Twig to render a template that outputs all the <script> tags. Whenever Twig initializes an extension, if it is declared with the right methods, Twig will inject itself in the extension.

{% for script in scripts %}

<script type="text/javascript" src="{{script}}" />

{% endfor %}

class KhepinExtension extends \Twig_Extension

{

protected $javascripts = [];

public function initRuntime(\Twig_Environment $environment)

{

$this->environment = $environment;

}

public function getFunctions()

{

return [

new \Twig_SimpleFunction('jslater', [$this, 'jslater']),

new \Twig_SimpleFunction('jsnow', [$this, 'jsnow'])

];

}

public function jslater($src)

{

$this->javascripts[] = $src;

}

public function jsnow()

{

$template = 'KhepinBookBundle:Twig:javascripts.html.twig';

return $this->environment->render($template, ['scripts' => $this->javascripts]);

}

public function getName()

{

return 'khepin_extension';

}

}

The second part of our extension is now used as follows:

{{ jsnow() | raw }}

Testing a Twig extension

The format for testing a Twig extension is quite specific; you declare a test case that loads all your extensions and then define fixture files under a specific format.

use Khepin\BookBundle\Twig\KhepinExtension;

use Twig_Test_IntegrationTestCase;

class KhepinExtensionTest extends Twig_Test_IntegrationTestCase

{

public function getExtensions()

{

return array(

new KhepinExtension()

);

}

public function getFixturesDir()

{

return __DIR__.'/Fixtures/';

}

}

The fixtures then look as follows:

--TEST--

"jslater / jsnow" filter

--TEMPLATE--

{{jslater(script)}}

{{jslater(script)}}{{jsnow()|raw}}

--DATA--

return ['script' => 'jquery.js'];

--EXPECT--

<script type="text/javascript" src="jquery.js" />

This file defines the following:

· The test title

· A series of templates to be rendered

· The data to be passed to each template

· The expected results

However, running it will give us an error. Symfony, by default, loads templates from the filesystem based on a given convention—the Bundle:Controller:template format. This is fine, but during the tests, Twig doesn't know how to load this format. We'll refactor our class so that it can load the template directly as a string.

public function __construct()

{

$this->environment = new \Twig_Environment(new \Twig_Loader_String());

}

public function jsnow()

{

$template = '{% for script in scripts %}<script type="text/javascript" src="{{script}}" />{% endfor %}';

$scripts = array_unique($this->javascripts);

return $this->environment->render($template, compact('scripts'));

}

As we now create our own Twig environment to load templates as strings, we no longer need to call initRuntime and can use our own constructor.

The time difference filter

As an exercise, try to define a Twig extension for the following case:

On the home page, we want to display the activity of the website by showing who recently joined a meetup. Instead of showing "Molly joined Yoga Teachers Training on Nov 29 at 16:15", we'd like to show "Molly joined Yoga Teachers Training 5 minutes ago".

What we are trying to do is take an existing date, compare it to the current date, and format the output accordingly. Therefore, a filter seems to be the perfect extension type we need. Therefore, this time, we will be using the \Twig_SimpleFilter class.

Summary

With commands, we can now easily create tools for the developers who work on our application. We know that commands have access to the whole service container. We also know how to make them rely as much as possible on services, making the code for the command available to the whole application, if we need it later.

We only saw one form of extensions for templates but know that all other extension types (with the exception of custom tags) are just as easy and straightforward to implement. Custom tags are quite complex, and they are also very rarely needed. You can learn the basics of creating a new tag at http://twig.sensiolabs.org/doc/advanced.html#tags.