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:
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:
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.
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...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
There's no need to
require
symfony/class-loader. Composer can generate the autoloader without it.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!
you might also want to check out http://cilex.github.com/
Thanks, an excellent suggestion! This will indeed help starting up a larger console oriented application.