Symfony2 - extending the console component
-
Upload
hugo-hamon -
Category
Technology
-
view
8.679 -
download
2
description
Transcript of Symfony2 - extending the console component
Extending and Leveraging the Power of the CLI.
Hugo Hamon
Who’s talking?
@hhamon
Follow me on Twitter…
Introduction to the Console Component
CRON jobs and batch processing.
Redondant and tedious tasks.
Interactive setup tools.
Code generation.
Cache clearing / warming. …
Improve your productivity and effiency.
Be proud to be lazy J
Creating new command line tools in bundles
The Command folder
src/Sensio/Bundle/HangmanBundle/Command/
GameHangmanCommand.php
Bootstrapping a new command namespace Sensio\Bundle\HangmanBundle\Command; use Symfony\Component\Console\Command\Command; class GameHangmanCommand extends Command { protected function configure() { $this ->setName('game:hangman') ->setDescription('Play the famous hangman game from the CLI') ; } }
Adding usage manual
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); }
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), ));
$ php app/console help game:hangman
Executing a command
protected function execute( InputInterface $input, OutputInterface $output) { // the business logic goes here... }
InputInterface
namespace Symfony\Component\Console\Input; 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); }
OutputInterface
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(); }
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)); }
Validating the input arguments and options.
// 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)); }
Validating input parameters
$ php app/console game:hangman foo
$ php app/console game:hangman 8 --max-attempts=bar
Formatting the output.
The formatter helper
class FormatterHelper extends Helper { public function formatSection($section, $message, $style); public function formatBlock($messages, $style, $large); }
$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');
// 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), '', ));
Make the command interact with the end user.
Dialog Helper
class DialogHelper extends Helper { public function ask(...); public function askConfirmation(...); public function askAndValidate(...); }
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?'); } }
$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);
do { $answer = $dialog->askAndValidate( $output, 'Type a letter or a word... ', array($this, 'validateLetter') ); $currentAttempt++; } while ($currentAttempt <= $attempts);
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; }
Asking and validating the answer
Asking and validating the answer
Refactoring your code is good for your command.
Think your commands as controllers.
Request <-> Response Input <-> Output
The Dictionary class namespace Sensio\Bundle\HangmanBundle\Game; class Dictionary implements \Countable { private $words; public function addWord($word); public function count(); public function getRandomWord($length); }
The Game class namespace Sensio\Bundle\HangmanBundle\Game; 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(); }
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())); }
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()); }
Command class refactoring
Unit testing console commands
Unit testing is about testing your model classes.
namespace Sensio\Bundle\HangmanBundle\Tests\Game; use Sensio\Bundle\HangmanBundle\Game\Game; 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()); } }
Unit testing the Game class
Functional testing console commands
Run the command and check the output.
namespace Sensio\Bundle\DemoBundle\Command; class HelloWorldCommand extends Command { // ... protected function execute($input, $output) { $name = $input->getOption('name'); $output->writeln('Your name is <info>'. $name .'</info>'); } }
The SayHello command
StreamOutput
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() ); } }
CommandTester
namespace Symfony\Component\Console\Tester; class CommandTester { public function __construct(Command $command); public function execute($input, $options); public function getDisplay(); public function getInput(); public function getOutput(); }
class SayHelloCommandTest extends CommandTester { public function testSayHello() { $tester = new CommandTester(new SayHelloCommand()); $tester->execute(array('name' => 'Hugo'), array( 'interactive' => false )); $this->assertEquals( 'Your name is <info>Hugo</info>', $tester->getDisplay() ); } }
Being the God of the command line J
Container
ContainerAwareInterface
namespace Symfony\Component\DependencyInjection; interface ContainerAwareInterface { /** * Sets the Container. * * @param ContainerInterface $container * * @api */ function setContainer(ContainerInterface $container = null); }
namespace Sensio\Bundle\HangmanBundle\Command; //... class GameHangmanCommand extends Command implements ContainerAwareInterface { // ... private $container; public function setContainer(ContainerInterface $container = null) { $this->container = $container; } protected function execute(InputInterface $input, OutputInterface $output) { $service = $this->container->get('my_service'); } }
ContainerAwareCommand
namespace Symfony\Bundle\FrameworkBundle\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; abstract class ContainerAwareCommand extends Command implements ContainerAwareInterface { private $container; protected function getContainer() { if (null === $this->container) { $this->container = $this->getApplication()->getKernel()->getContainer(); } return $this->container; } public function setContainer(ContainerInterface $container = null) { $this->container = $container; } }
$container = $this->getContainer(); $max = $container->getParameter('hangman.max_attempts');
Reading the con"guration
$container = $this->getContainer(); $doctrine = $container->get('doctrine'); $em = $doctrine->getEntityManager('default'); $score = new Score(); $score->setScore(10); $score->setPlayer('hhamon'); $em->persist($score); $em->flush();
Accessing the Doctrine registry
$container = $this->getContainer(); $templating = $container->get('templating'): $content = $templating->render( 'SensioHangmanBundle:Game:finish.txt.twig', array('game' => $this->game) );
Rendering Twig templates
$container = $this->getContainer(); $router = $container->get('router'): $url = $router->generate( 'game_finish', array('user' => 'hhamon'), true );
Generating urls
$container = $this->getContainer(); $translator = $container->get('translator'): $content = $translator->trans( 'Hello %user%!', array('user' => 'hhamon'), null, 'fr' );
Translating messages
$container = $this->getContainer(); $logger = $container->get('logger'); $logger->info('Game finished!');
Writing logs
$container = $this->getContainer(); $fs = $container->get('filesystem'); $fs->touch('/path/to/toto.txt');
Dealing with the "lesystem
Conclusion
Ask a (little) ninja J
Questions & Answers
• Calling a command from a command • Calling a command in a command • Sending an email