Publicité
Publicité

Contenu connexe

Publicité
Publicité

Symfony2 - extending the console component

  1. Extending and Leveraging the Power of the CLI.
  2. Who’s talking? Hugo Hamon
  3. Follow me on Twitter… @hhamon
  4. Introduction to the Console Component
  5. Redondant and tedious tasks. CRON jobs and batch processing.
  6. Code generation. Interactive setup tools. Cache clearing / warming. …
  7. Improve your productivity and effiency.
  8. Be proud to be lazy J
  9. Creating new command line tools in bundles
  10. The Command folder
  11. src/Sensio/Bundle/ HangmanBundle/Command/ GameHangmanCommand.php
  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') ; } }
  13. Adding usage manual
  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/console help game:hangman
  17. Executing a command protected function execute( InputInterface $input, OutputInterface $output) { // the business logic goes here... }
  18. InputInterface
  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. OutputInterface
  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. Validating the input arguments and options.
  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/console game:hangman foo $ php app/console game:hangman 8 --max-attempts=bar
  26. Formatting the output.
  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), '', ));
  30. Make the command interact with the end user.
  31. Dialog Helper
  32. class DialogHelper extends Helper { public function ask(...); public function askConfirmation(...); public function askAndValidate(...); }
  33. 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?'); } }
  34. $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);
  35. Asking and validating the answer do { $answer = $dialog->askAndValidate( $output, 'Type a letter or a word... ', array($this, 'validateLetter') ); $currentAttempt++; } while ($currentAttempt <= $attempts);
  36. 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; }
  37. Asking and validating the answer
  38. Refactoring your code is good for your command.
  39. Think your commands as controllers.
  40. Request <-> Response Input <-> Output
  41. The Dictionary class namespace SensioBundleHangmanBundleGame; class Dictionary implements Countable { private $words; public function addWord($word); public function count(); public function getRandomWord($length); }
  42. 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(); }
  43. 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())); }
  44. 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()); }
  45. Unit testing console commands
  46. Unit testing is about testing your model classes.
  47. 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()); } }
  48. Functional testing console commands
  49. Run the command and check the output.
  50. 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>'); } }
  51. StreamOutput
  52. 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() ); } }
  53. CommandTester
  54. namespace SymfonyComponentConsoleTester; class CommandTester { public function __construct(Command $command); public function execute($input, $options); public function getDisplay(); public function getInput(); public function getOutput(); }
  55. 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() ); } }
  56. Being the God of the command line J
  57. Container
  58. ContainerAwareInterface
  59. namespace SymfonyComponentDependencyInjection; interface ContainerAwareInterface { /** * Sets the Container. * * @param ContainerInterface $container * * @api */ function setContainer(ContainerInterface $container = null); }
  60. 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'); } }
  61. ContainerAwareCommand
  62. 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; } }
  63. Reading the con guration $container = $this->getContainer(); $max = $container->getParameter('hangman.max_attempts');
  64. 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();
  65. Rendering Twig templates $container = $this->getContainer(); $templating = $container->get('templating'): $content = $templating->render( 'SensioHangmanBundle:Game:finish.txt.twig', array('game' => $this->game) );
  66. Generating urls $container = $this->getContainer(); $router = $container->get('router'): $url = $router->generate( 'game_finish', array('user' => 'hhamon'), true );
  67. Translating messages $container = $this->getContainer(); $translator = $container->get('translator'): $content = $translator->trans( 'Hello %user%!', array('user' => 'hhamon'), null, 'fr' );
  68. Writing logs $container = $this->getContainer(); $logger = $container->get('logger'); $logger->info('Game finished!');
  69. Dealing with the lesystem $container = $this->getContainer(); $fs = $container->get('filesystem'); $fs->touch('/path/to/toto.txt');
  70. Conclusion
  71. Questions & Answers Ask a (little) ninja J
  72. •  Calling  a  command  from  a  command   •  Calling  a  command  in  a  command   •  Sending  an  email  
Publicité