Symfony2: Setting up a Console-centered Application (with Composer)

Posted on by Matthias Noback

I recently took a quick look at Composer and I have come to like it very much. I want to demonstrate the incredible ease of use. My example is also not trivial, it means to elaborate on the concept of a Console Application. The Symfony2 documentation does not tell us how to use the Console Component stand-alone, but only shows us how to add new commands to an already existing application. Therefore, let's use Composer to set up a very light-weight Console Application, a good starting point from where you can add your own commands.

Define the dependencies in composer.json

In a clean project directory, add a composer.json file, containing one requirement and also an extra autoloading suggestion for your own PHP files:

{
    "require" : {
        "symfony/console": "2.0.*"
    },
    "autoload" : {
        "psr-0" : {
            "Matthias" : "src"
        }
    }
}

Now, install Composer if you haven't already done this, and (in your project directory) run

php composer.phar install

Composer will tell you that it's downloading the Symfony Console component. It will be copied to the /vendor directory. In /vendor/.composer a set of files are generated: a class loader (ClassLoader.php), and several generated files, which contain information about where to look for files containing PHP classes in certain namespaces (autoload_classmap.php, autoload_namespaces.php). Furthermore, there is a file called installed.json. This file contains information about the currently installed versions. You may want to put these files under version control, so your teammates will run the same versions of the project's dependencies. Finally, there is a autoload.php. We need this file to initialize the autoloading for our console application.

Add the application's entry point: console.php

We need to have an entry point for our application. This will be console.php in the root of the project. Composer has already taken care of the autoloading and has generated a autoload.php file containing all we need, so we just require it here:

require __DIR__ . '/vendor/.composer/autoload.php';

Next, we define an Application and a sample command. Think of the application as the container, which holds all commands and also provides the console with some default options, concerning output coloring, verbosity, interactivity, etc. Let's extend the Symfony\Component\Console\Application class and create our own Application in /src/Matthias/Console/Application.php:

namespace Matthias\Console;

use Symfony\Component\Console\Application as BaseApplication;

class Application extends BaseApplication
{
    const NAME = 'Matthias\' Console Application';
    const VERSION = '1.0';

    public function __construct()
    {
        parent::__construct(static::NAME, static::VERSION);
    }
}

Later on, we will add commands to the application, but right now we already have a functional console application!

Try running

php console.php

You will see a list of available commands (list and help) and options.

Adding commands

The application does not do much yet, so let's add a first command, say: "batch:process".

A command is (ideally) a class extending the Symfony\Component\Console\Command\Command class. The "batch:process" command might look something like this:

namespace Matthias\Console\Command;

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

class BatchProcessCommand extends Command
{
    protected function configure()
    {
        $this
            ->setName('batch:process')
            ->addArgument('type', InputArgument::REQUIRED, 'The type of items to process')
            ->addOption('no-cleanup', null, InputOption::VALUE_NONE)
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $output->writeln('<info>I am going to do something very useful</info>');
    }
}

Inside the Application's constructor, we may now announce the existence of the BatchProcessCommand:

use Matthias\Console\Command\BatchProcessCommand;

class Application extends BaseApplication
{
    /* ... */

    public function __construct()
    {
        /* ... */

        $batchProcessCommand = new BatchProcessCommand();
        $this->add($batchProcessCommand);
    }
}

And now, we have a new console command available:

php console.php help batch:process

Organizing commands

It should be common practice to put as little execution logic as possible inside the execute() method of the command. At least, try to split the class up in logical units (which also allows for better unit testing):

class SomeCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $type = $input->getArgument('type');
        $this->validateType($type);

        $items = $this->findAllItemsOfType($type);

        foreach ($items as $item) {
            if ($this->processItem($item)) {
                $this->writeln(sprintf('Process item %s', $item));
            }
        }

        if (!$input->hasOption('no-cleanup)) {
            $this->cleanUp();
        }
    }

    private function findAllItemsOfType($type)
    {
        /* ... */
    }

    private function processItem($item)
    {
        /* ... */
    }

    private function cleanUp()
    {
        /* ... */
    }
}

Even better: extract all code related to the real execution of the task at hand, and move it over to it's own class. The command should only take care of collecting and parsing all arguments and options, and then leave the real work to some other class. This is reminiscent of the way an action in a controller usually needs almost no knowledge of the domain, since it delegates related actions to the domain objects themselves.

Creating interactive commands

To make commands interactive, you only have to implement the interact() method:

class BatchProcessCommand extends Command
{
    protected function interact(InputInterface $input, OutputInterface $output)
    {
        /* ... */
    }
}

Interacting means: asking the user a number of questions and possibly also ask for some confirmations, then storing the results as arguments and options of the given $input object. The interact() method will always be executed before the final validation is done, to assert that all necessary arguments and options are in place. It thus provides the user with a last chance to fill in (missing) arguments and options (unless he uses the option --no-interaction):

    protected function interact(InputInterface $input, OutputInterface $output)
    {
        $defaultType = 1;
        $question = array(
            "<comment>1</comment>: Messages\n",
            "<comment>2</comment>: Jobs\n",
            "<question>Choose a type:</question> [<comment>$defaultType</comment>] ",
        );

        $type = $this->getHelper('dialog')->askAndValidate($output, $question, function($typeInput) {
            if (!in_array($typeInput, array(1, 2))) {
                throw new \InvalidArgumentException('Invalid type');
            }

            return $typeInput;
        }, 10, $defaultType);

        $input->setArgument('type', $type);
    }

This example shows you the use of coloring output (using XML-like tags "info", "error", "comment" and "question"), but it also contains a call to the DialogHelper's askAndValidate() method. This method renders the given question and asks the user for input. It then calls the provided "callable" (in this case a closure) to find out if the user's input is valid. If an exception gets thrown, the user will again be asked to give some valid input (in our case, it does this 10 times, but when you provide false as the fourth argument, the user may provide incorrect input forever).

All this will result in the following dialog:

Dialog

Only after providing a "correct" answer, the execution flow continues.

Bonus: defining custom styles

One little thing I noticed in the documentation, and which is really fun to use, is adding custom styling to the output of your command. This can be accomplished in the following way:

$style = new OutputFormatterStyle('white', 'green', array('bold'));
$output->getFormatter()->setStyle('goodluck', $style);
$output->writeln('<goodluck>I am going to do something very useful</goodluck>');

The output of this will look like:

Custom format

Available foreground and background colors: black, red, green, yellow, blue, magenta, cyan, white Extra options (third argument) are: bold, underscore, blink, reverse, conceal

It should not be underestimated how important coloring your console output is. At some point it may actually comfort you, when everything around you is just black and white.

PHP Symfony2 console
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
Matthias Noback

Thanks, I will update the post - by the way it's really funny that you are the first one in months to notice this big omission: you should indeed create an instance of the application (in my case the application subclass) and call run() on it...

mauricio lopez

Hi great post

I got confused at the beginning. It was the first time creating a console command with symfony console.

Please update your post. The first time you require the autoload.php in console.php, you also have to add:

use Symfony\Component\Console\Application;

$console = new Application();

$console->run();

And now, composer is not using .composer hidden folder anymore and autoload.php is outside that folder.

Thanks

Igor Wiedler

There's no need to require symfony/class-loader. Composer can generate the autoloader without it.

Matthias Noback

Hi Igor, while traveling from home to work I thought of this too. Requiring the Symfony ClassLoader component was a left-over (I had not thought of the fact that all the autoloading could be done by Composer). I just edited the post. Thanks!

Lukas

you might also want to check out http://cilex.github.com/

Matthias Noback

Thanks, an excellent suggestion! This will indeed help starting up a larger console oriented application.