The goal of this session is to explain how to take benefit from the Symfony2 command line interface tool. First, I have a closer look at the most interesting commands to generate code and help you reduce your development time. Then, I will show you how to create your own commands to extend the Symfony CLI tool and automate your tedious and redundant tasks. This part of the talk will also explain how to create interactive tasks, interact with the database, generating links or send emails from the command line. Of course, there will be a focus on how to design your commands the best way to make them as much testable as possible.
12. Bootstrapping a new command
namespace SensioBundleHangmanBundleCommand;
use SymfonyComponentConsoleCommandCommand;
class GameHangmanCommand extends Command
{
protected function configure()
{
$this
->setName('game:hangman')
->setDescription('Play the famous hangman game from the CLI')
;
}
}
14. protected function configure()
{
$this->setHelp(<<<EOF
The <info>game:hangman</info> command starts a new game of the
famous hangman game:
<info>game:hangman 8</info>
Try to guess the hidden <comment>word</comment> whose length is
<comment>8</comment> before you reach the maximum number of
<comment>attempts</comment>.
You can also configure the maximum number of attempts
with the <info>--max-attempts</info> option:
<info>game:hangman 8 --max-attempts=5</info>
EOF);
}
15. Adding arguments & options
$this->setDefinition(array(
new InputArgument('length',
InputArgument::REQUIRED, 'The length of the word to
guess'),
new InputOption('max-attempts', null,
InputOption::VALUE_OPTIONAL, 'Max number of
attempts', 10),
));
19. namespace SymfonyComponentConsoleInput;
interface InputInterface
{
function getFirstArgument();
function hasParameterOption($values);
function getParameterOption($values, $default = false);
function bind(InputDefinition $definition);
function validate();
function isInteractive();
function getArguments();
function getArgument($name);
function getOptions();
function getOption($name);
}
21. interface OutputInterface
{
function write($messages, $newline, $type);
function writeln($messages, $type = 0);
function setVerbosity($level);
function getVerbosity();
function setDecorated($decorated);
function isDecorated();
function setFormatter($formatter);
function getFormatter();
}
22. protected function execute(InputInterface $input, OutputInterface $output)
{
$dictionary = array(
7 => array('program', 'speaker', 'symfony'),
8 => array('business', 'software', 'hardware'),
9 => array('algorithm', 'framework', 'developer')
);
// Read the input
$length = $input->getArgument('length');
$attempts = $input->getOption('max-attempts');
// Find a word to guess
$words = $dictionary[$length];
$word = $words[array_rand($words)];
// Write the output
$output->writeln(sprintf('The word to guess is %s.', $word));
$output->writeln(sprintf('Max number of attempts is %u.', $attempts));
}
24. Validating input parameters
// Read the input
$length = $input->getArgument('length');
$attempts = $input->getOption('max-attempts');
$lengths = array_keys($dictionary);
if (!in_array($length, $lengths)) {
throw new InvalidArgumentException(sprintf('The length "%s" must be
an integer between %u and %u.', $length, min($lengths), max($lengths)));
}
if ($attempts < 1) {
throw new InvalidArgumentException(sprintf('The attempts "%s" must
be a valid integer greater than or equal than 1.', $attempts));
}
27. The formatter helper
class FormatterHelper extends Helper
{
public function formatSection($section, $message, $style);
public function formatBlock($messages, $style, $large);
}
28. $formatter->formatBlock('A green information', 'info');
$formatter->formatBlock('A yellow comment', 'comment');
$formatter->formatBlock('A red error', 'error');
$formatter->formatBlock('A custom style', 'bg=blue;fg=white');
29. // Get the formatter helper
$formatter = $this->getHelperSet()->get('formatter');
// Write the output
$output->writeln(array(
'',
$formatter->formatBlock('Welcome in the Hangman Game',
'bg=blue;fg=white', true),
'',
));
$output->writeln(array(
$formatter->formatSection('Info', sprintf('You have %u
attempts to guess the hidden word.', $attempts), 'info', true),
'',
));
33. class DialogHelper extends Helper
{
public function ask(...);
public function askConfirmation(...);
public function askAndValidate(...);
}
34. class Command
{
// ...
protected function interact(
InputInterface $input,
OutputInterface $output
)
{
$dialog = $this->getHelperSet()->get('dialog');
$answer = $dialog->ask($output, 'Do you enjoy
your Symfony Day 2011?');
}
}
35. $dialog = $this->getHelperSet()->get('dialog');
$won = false;
$currentAttempt = 1;
do {
$letter = $dialog->ask(
$output, 'Type a letter or a word... '
);
$currentAttempt++;
} while (!$won && $currentAttempt <= $attempts);
36. Asking and validating the answer
do {
$answer = $dialog->askAndValidate(
$output,
'Type a letter or a word... ',
array($this, 'validateLetter')
);
$currentAttempt++;
} while ($currentAttempt <= $attempts);
37. Asking and validating the answer
public function validateLetter($letter)
{
$ascii = ord(mb_strtolower($letter));
if ($ascii < 97 || $ascii > 122) {
throw new InvalidArgumentException('The expected
letter must be a single character between A and Z.');
}
return $letter;
}
42. The Dictionary class
namespace SensioBundleHangmanBundleGame;
class Dictionary implements Countable
{
private $words;
public function addWord($word);
public function count();
public function getRandomWord($length);
}
43. The Game class
namespace SensioBundleHangmanBundleGame;
class Game
{
public function __construct($word, $maxAttempts);
public function getWord();
public function getHiddenWord();
public function getAttempts();
public function tryWord($word);
public function tryLetter($letter);
public function isOver();
public function isWon();
}
44. Command class refactoring
protected function interact(InputInterface $input, OutputInterface $output)
{
$length = $input->getArgument('length');
$attempts = $input->getOption('max-attempts');
$this->dictionary = new Dictionary();
$this->dictionary
->addWord('program')
...
;
$word = $dictionary->getRandomWord($length);
$this->game = new Game($word, $attempts);
$this->writeIntro($output, 'Welcome in the Hangman Game');
$this->writeInfo($output, sprintf('%u attempts to guess the word.', $attempts));
$this->writeInfo($output, implode(' ', $this->game->getHiddenWord()));
}
45. Command class refactoring
protected function interact(InputInterface $input, OutputInterface $output)
{
// ...
$dialog = $this->getHelperSet()->get('dialog');
do {
if ($letter = $dialog->ask($output, 'Type a letter... ')) {
$this->game->tryLetter($letter);
$this->writeInfo($output, implode(' ', $this->game->getHiddenWord()));
}
if (!$letter && $word = $dialog->ask($output, 'Try a word... ')) {
$this->game->tryWord($word);
}
} while (!$this->game->isOver());
}
49. Unit testing the Game class
namespace SensioBundleHangmanBundleTestsGame;
use SensioBundleHangmanBundleGameGame;
class GameTest extends PHPUnit_Framework_TestCase
{
public function testGameIsWon()
{
$game = new Game('foo', 10);
$game->tryLetter('o');
$game->tryLetter('f');
$this->assertEquals(array('f', 'o', 'o'), $game->getHiddenWord());
$this->assertTrue($game->isWon());
}
}
54. class SayHelloCommandTest extends CommandTester
{
public function testSayHello()
{
$input = new ArrayInput(array('name' => 'Hugo'));
$input->setInteractive(false);
$output = new StreamOutput();
$command = new SayHelloCommand();
$command->run($input, $output);
$this->assertEquals(
'Your name is <info>Hugo</info>',
$output->getStream()
);
}
}