Extending and Leveraging the Power of the CLI.
Who’s talking? Hugo Hamon
Follow me on Twitter… @hhamon
Introduction to the Console Component
Redondant and tedious tasks. CRON jobs and batch processing.
Code generation. Interactive setup tools. 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 SensioBundleHangmanBundleCommand; use SymfonyComponentConsoleCommandCommand; 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 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); }
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.
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)); }
$ 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);
Asking and validating the answer 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
Refactoring your code is good for your command.
Think your commands as controllers.
Request <-> Response Input <-> Output
The Dictionary class namespace SensioBundleHangmanBundleGame; class Dictionary implements Countable { private $words; public function addWord($word); public function count(); public function getRandomWord($length); }
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(); }
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())); }
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()); }
Unit testing console commands
Unit testing is about testing your model classes.
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()); } }
Functional testing console commands
Run the command and check the output.
The SayHello command namespace SensioBundleDemoBundleCommand; class HelloWorldCommand extends Command { // ... protected function execute($input, $output) { $name = $input->getOption('name'); $output->writeln('Your name is <info>'. $name .'</info>'); } }
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 SymfonyComponentConsoleTester; 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 SymfonyComponentDependencyInjection; interface ContainerAwareInterface { /** * Sets the Container. * * @param ContainerInterface $container * * @api */ function setContainer(ContainerInterface $container = null); }
namespace SensioBundleHangmanBundleCommand; //... 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 SymfonyBundleFrameworkBundleCommand; use SymfonyComponentConsoleCommandCommand; use SymfonyComponentDependencyInjectionContainerInterface; use SymfonyComponentDependencyInjectionContainerAwareInterface; 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; } }
Reading the con guration $container = $this->getContainer(); $max = $container->getParameter('hangman.max_attempts');
Accessing the Doctrine registry $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();
Rendering Twig templates $container = $this->getContainer(); $templating = $container->get('templating'): $content = $templating->render( 'SensioHangmanBundle:Game:finish.txt.twig', array('game' => $this->game) );
Generating urls $container = $this->getContainer(); $router = $container->get('router'): $url = $router->generate( 'game_finish', array('user' => 'hhamon'), true );
Translating messages $container = $this->getContainer(); $translator = $container->get('translator'): $content = $translator->trans( 'Hello %user%!', array('user' => 'hhamon'), null, 'fr' );
Writing logs $container = $this->getContainer(); $logger = $container->get('logger'); $logger->info('Game finished!');
Dealing with the lesystem $container = $this->getContainer(); $fs = $container->get('filesystem'); $fs->touch('/path/to/toto.txt');
Conclusion
Questions & Answers Ask a (little) ninja J
•  Calling  a  command  from  a  command   •  Calling  a  command  in  a  command   •  Sending  an  email  

Symfony2 - extending the console component

  • 1.
  • 2.
  • 3.
    Follow me onTwitter… @hhamon
  • 4.
    Introduction to theConsole Component
  • 5.
    Redondant and tedious tasks. CRONjobs and batch processing.
  • 6.
    Code generation. Interactive setuptools. Cache clearing / warming. …
  • 7.
  • 8.
    Be proud to belazy J
  • 9.
    Creating new command linetools in bundles
  • 10.
  • 11.
  • 12.
    Bootstrapping a newcommand namespace SensioBundleHangmanBundleCommand; use SymfonyComponentConsoleCommandCommand; class GameHangmanCommand extends Command { protected function configure() { $this ->setName('game:hangman') ->setDescription('Play the famous hangman game from the CLI') ; } }
  • 13.
  • 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), ));
  • 16.
    $ php app/consolehelp game:hangman
  • 17.
    Executing a command protectedfunction execute( InputInterface $input, OutputInterface $output) { // the business logic goes here... }
  • 18.
  • 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); }
  • 20.
  • 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)); }
  • 23.
  • 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)); }
  • 25.
    $ php app/consolegame:hangman foo $ php app/console game:hangman 8 --max-attempts=bar
  • 26.
  • 27.
    The formatter helper classFormatterHelper 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 theformatter 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), '', ));
  • 31.
    Make the command interactwith the end user.
  • 32.
  • 33.
    class DialogHelper extendsHelper { 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 validatingthe answer do { $answer = $dialog->askAndValidate( $output, 'Type a letter or a word... ', array($this, 'validateLetter') ); $currentAttempt++; } while ($currentAttempt <= $attempts);
  • 37.
    Asking and validatingthe 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; }
  • 38.
  • 39.
    Refactoring your codeis good for your command.
  • 40.
    Think your commandsas controllers.
  • 41.
    Request <-> Response Input <-> Output
  • 42.
    The Dictionary class namespaceSensioBundleHangmanBundleGame; class Dictionary implements Countable { private $words; public function addWord($word); public function count(); public function getRandomWord($length); }
  • 43.
    The Game class namespaceSensioBundleHangmanBundleGame; 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 protectedfunction 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 protectedfunction 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()); }
  • 47.
  • 48.
    Unit testing is abouttesting your model classes.
  • 49.
    Unit testing theGame 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()); } }
  • 50.
  • 51.
    Run the commandand check the output.
  • 52.
    The SayHello command namespaceSensioBundleDemoBundleCommand; class HelloWorldCommand extends Command { // ... protected function execute($input, $output) { $name = $input->getOption('name'); $output->writeln('Your name is <info>'. $name .'</info>'); } }
  • 53.
  • 54.
    class SayHelloCommandTest extendsCommandTester { 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() ); } }
  • 56.
  • 57.
    namespace SymfonyComponentConsoleTester; class CommandTester { public function __construct(Command $command); public function execute($input, $options); public function getDisplay(); public function getInput(); public function getOutput(); }
  • 58.
    class SayHelloCommandTest extendsCommandTester { 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() ); } }
  • 60.
    Being the Godof the command line J
  • 61.
  • 62.
  • 63.
    namespace SymfonyComponentDependencyInjection; interface ContainerAwareInterface { /** * Sets the Container. * * @param ContainerInterface $container * * @api */ function setContainer(ContainerInterface $container = null); }
  • 64.
    namespace SensioBundleHangmanBundleCommand; //... class GameHangmanCommandextends 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'); } }
  • 65.
  • 66.
    namespace SymfonyBundleFrameworkBundleCommand; use SymfonyComponentConsoleCommandCommand; useSymfonyComponentDependencyInjectionContainerInterface; use SymfonyComponentDependencyInjectionContainerAwareInterface; 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; } }
  • 67.
    Reading the conguration $container = $this->getContainer(); $max = $container->getParameter('hangman.max_attempts');
  • 68.
    Accessing the Doctrineregistry $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();
  • 69.
    Rendering Twig templates $container= $this->getContainer(); $templating = $container->get('templating'): $content = $templating->render( 'SensioHangmanBundle:Game:finish.txt.twig', array('game' => $this->game) );
  • 70.
    Generating urls $container =$this->getContainer(); $router = $container->get('router'): $url = $router->generate( 'game_finish', array('user' => 'hhamon'), true );
  • 71.
    Translating messages $container =$this->getContainer(); $translator = $container->get('translator'): $content = $translator->trans( 'Hello %user%!', array('user' => 'hhamon'), null, 'fr' );
  • 72.
    Writing logs $container =$this->getContainer(); $logger = $container->get('logger'); $logger->info('Game finished!');
  • 73.
    Dealing with thelesystem $container = $this->getContainer(); $fs = $container->get('filesystem'); $fs->touch('/path/to/toto.txt');
  • 74.
  • 75.
    Questions & Answers Ask a (little) ninja J
  • 76.
    •  Calling  a  command  from  a  command   •  Calling  a  command  in  a  command   •  Sending  an  email