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.

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