Building CLI Apps with Symfony Console Component

We use MVC frameworks to implement kick ass web apps in short amount of time, but what is with CLI applications, how do we architecture them?

When working with PHP, I usually use Zend Framework for my projects. It has nice code generation tool (Zend_Tool), MVC, forms, helpers…and it’s very flexible. But one aspect where Zend wasn’t the best tool for the job is building CLI applications. Usually, application have web interface, but there are some parts, like cron scripts, where you should implement command line interface. I used Zend_Console_Getopt for this, and it’s nice, but it just helps you to deal with arguments and options, not really helps you organize the application. Looks like the component is not finished, and it’s not, since there are a number of to-dos in the source code.

Meet Symfony Components

At the moment of writing Symfony2 released vPR4 and will probably be production ready around March 2011. Symfony1 is a very cool framework, and I really liked it’s components, especially Dependency Injection Container. In new version there are many new components and today we will talk about the Console Component.

Symfony Console Component is still under development, and there is no good documentation yet. I managed to google out Fabien Potencier’s slides from ConFoo 2010 conference where he mentioned the component and showed one simple example. But still, most of the time working with the component, I find myself reading its source code.

Demo CLI Application

Let’s implement simple demo CLI application using Symfony Console Component. Our application will be simple command line calculator, to keep focus on concepts, not business logic.

Our app will consist of command classes which will extend Command and application class which will connect all comands, configure and run them depending on user input. Lets implement add command which will take two arguments, compute the sum and print it to standard output.

namespace Application\Cli\Command;

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

/**
 * Add command.
 *
 * @author Saša Stamenković <umpirsky@gmail.com>
 */
class Add extends Console\Command\Command {
    /**
     * Configure command, set parameters definition and help.
     */
    protected function configure() {
        $this
        ->setName('calc:add')
        ->setDescription('Calculates the sum of two numbers.')
        ->setDefinition(array(
            new InputArgument('x', InputArgument::REQUIRED, 'First addend'),
            new InputArgument('y', InputArgument::REQUIRED, 'Second addend'),
            new InputOption('round', null, null, 'Round result')
        ))
        ->setHelp(sprintf(
				'%sCalculates the sum of two numbers.%s',
				PHP_EOL,
				PHP_EOL
		));
    }

    /**
     * Calculates the sum of two numbers.
     */
    protected function execute(InputInterface $input, OutputInterface $output) {
        $result = $input->getArgument('x') + $input->getArgument('y');
		if ($input->getOption('round')) {
			$result = round($result);
		}

        $output->write($result);
    }
}

As you can see, our command overrides configure and execute methods.

Configure method first sets command name, commands can be namespaced with colon (:), we will use calc namespace for our commands. Then it sets command description. Command definition is set by passing an array of InputArgument and InputOption objects. InputArgument constructor accepts argument name and optional mode, description and default value.  We finish configure with setting help message.

Execute method is where all the action happens. We should implement our command there or delegate the job to other parts of our application. Since we have a simple task for our example, we will implement it right there.  We get input and output parameters. Input is used to retrieve user input, and output to write the result, pretty simple, isn’t it? The example speaks for itself, we retrieve addends, calculate the sum, and write it to the output.

Same as above I have implemented few other commands sub, mul, div.

Now, we put it all together in calculator application class.

namespace Application\Cli;

use Symfony\Component\Console\Application,
	Application\Cli\Command;

/**
 * Calculator application.
 *
 * @author Saša Stamenković <umpirsky@gmail.com>
 */
class Calculator extends Application {
	/**
     * Calculator constructor.
     */
    public function __construct() {
    	parent::__construct('Welcome to Umpirsky CLI Calculator', '1.0');

    	$this->addCommands(array(
			new Command\Add(),
			new Command\Sub(),
			new Command\Mul(),
			new Command\Div()
		));
    }
}

We just set the title and version, and add our commands, and that’s all. Now all we have to do in order to make our app working is to wrap it into simple php script where we bootstrap, instantiate our calculator app class and run it.

require 'bootstrap.php';

$calculator = new \Application\Cli\Calculator();
$calculator->run();

Running CLI application

Lets check our demo app. If you check application folder you will see that I have wrapped php script into bash, so we can run it like any other command.

We go to command line and type:

calculator

we will get output like this:

Welcome to Umpirsky CLI Calculator version 1.0

Usage:
  [options] command [arguments]

Options:
  --help           -h Display this help message.
  --quiet          -q Do not output any message.
  --verbose        -v Increase verbosity of messages.
  --version        -V Display this program version.
  --ansi           -a Force ANSI output.
  --no-interaction -n Do not ask any interactive question.

Available commands:
  help   Displays help for a command (?)
  list   Lists commands
calc
  :add   Calculates the sum of two numbers.
  :div   Calculates the quotient of two numbers.
  :mul   Calculates the product of two numbers.
  :sub   Calculates the difference between two numbers.

It’s the short help for our app, to get more details about desired command run:

calculator --help calc:add

help for command is printed:

Usage:
 calc:add [--round] x y

Arguments:
 x        First addend
 y        Second addend

Options:
 --round Round result

Help:

 Calculates the sum of two numbers.

Now run:

calculator calc:add 3 5

as expected, the result will be:

8

It works! There are list and help commands added automatically by Symfony, and if you miss some of required parameters you’ll get user friendly error message. You can continue playing with the app, try other commands, add your own, try --round option. Also you can check some of advanced features like interacting with user using the DialogHelper.

Testing CLI application

One of the big benefits when using this component is that our console application is unit testable. It can be tested on the application level, or test each command separatelly.

Lets write test for our add command.

namespace Test\Application\Cli\Command;

use Symfony\Component\Console\Tester\CommandTester;
use Application\Cli\Command\Add;

class AddTest extends \PHPUnit_Framework_TestCase {
	/**
     * @dataProvider provider
     */
    public function testExecute($x, $y, $r) {
        $commandTester = new CommandTester(new Add());
        $commandTester->execute(array('x' => $x, 'y' => $y));
        $this->assertEquals($commandTester->getDisplay(), $r);
	}

	public function provider() {
		return array(
			array(1, 1, 2),
			array(25, 36, 61),
			array(12.5, 1.2, 13.7),
			array(7.123, 6.4, 13.523)
		);
    }
}

Similar, using ApplicationTester we can test our calculator.

namespace Test\Application\Cli;

use Symfony\Component\Console\Tester\ApplicationTester;

class CalculatorTest extends \PHPUnit_Framework_TestCase {
	/**
     * @dataProvider provider
     */
    public function testRun($command, $x, $y, $result) {
    	$calculator = new \Application\Cli\Calculator();
    	$calculator->setAutoExit(false); // Set autoExit to false when testing
        $calculatorTester = new ApplicationTester($calculator);
        $calculatorTester->run(array('command' => $command, 'x' => $x, 'y' => $y));
        $stream = $calculatorTester->getOutput()->getStream();
        rewind($stream);
        $this->assertEquals(stream_get_contents($stream), $result);
	}

	public function provider() {
		return array(
			array('calc:add', 1, 1, 2),
			array('calc:sub', 5, 3, 2),
			array('calc:mul', 2, 2, 4),
			array('calc:div', 6, 2, 3),
		);
    }
}

What is important here, and took some time for me to figure it out, is that you should set autoExit to false when testing the app, othervise Symfony console application will just exit and your tests will never get executed.

Conclusion

The fact that this component is not released yet doesn’t mean you shouldn’t use it in your projects since it’s pretty stable. For example, Doctrine2 is already using it for their console code generation tool.

This component:

  • Saves you a lot of time dealing with input parameters and formatting output.
  • Validates user input.
  • Have user interaction and output coloring support.
  • Makes your application testable.
  • Provides standard CLI interface and makes app more user friendly.

I strongly recommand using this if you are on PHP 5.3+. It will save you a lot of time and make code maintainance much easyer, as you have seen, adding new commands is dead simple.

You can find demo application source code on github. I encourage you to get the code and play with it. You can see how to bootstrap the app, deal with include paths, set Symfony autoloader, etc.

  • Marija

    Great job, clean and nice :)

  • http://citytalk.hk Weiyan

    Great job.
    Some minor problem, can’t find
    ‘Symfony/Component/HttpFoundation/UniversalClassLoader.php’
    The sandbox seems not have this component.

  • http://umpirsky.com umpirsky

    @Weiyan Thanks for reading the article and trying out the app.

    I removed some parts of Symfony in order to keep codebase smaller, and left only the Console component. So, I removed UniversalClassLoader by mistake. Now I have added it back so you can pull my changes from https://github.com/umpirsky/symfony-cli-calculator.

    If you have any problems, don’t hesitate to contact me.

  • http://bigwhoop.ch Phil

    Thanks for the arcticle + code! :)

  • http://itunes.apple.com/be/app/around-you-pad/id418330877?mt=8# Nick

    Thanks for the post…..

    The Surajkund Fair : SurajKund Fair, the Internationally famous Crafts Fair (Mela) arrives on iPad. More than 300+ HD Quality Photos and quick information on the fair.

  • Pingback: Łatwe rozszerzanie funkcjonalności komponentu Symfony2 Console dzięki Dependency Injection | technologie improwizowane

  • Matthew

    What about “calculator calc:add -3 5″ – does that work? Doesn’t seem to be any tests with negative numbers here, and I think they may cause an issue with the console parser. How would you work around that?

    • Anonymous

      Well, the goal of this article and demo CLI app is not to make a calculator, but to show how to implement CLI app using Symfony Console Component. Calculator is given as an example.

  • Tal Ater

    I’ve created a tutorial to help people who are having trouble setting up the Symfony console component as a standalone component, apart from the main Symfony framework – http://talater.com/symfony_console_component/

    It covers the initial steps which aren’t covered in the documentation, such as bootstraping, and setting up the auto loading.