$30 off During Our Annual Pro Sale. View Details »

Symfony - Extending the Console component

Hugo Hamon
October 21, 2011

Symfony - Extending the Console component

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.

Hugo Hamon

October 21, 2011
Tweet

More Decks by Hugo Hamon

Other Decks in Technology

Transcript

  1. Extending and Leveraging
    the Power of the CLI.

    View Slide

  2. Hugo
    Hamon
    Who’s talking?

    View Slide

  3. @hhamon
    Follow me on Twitter…

    View Slide

  4. Introduction to the Console
    Component

    View Slide

  5. CRON jobs and
    batch processing.
    Redondant and
    tedious tasks.

    View Slide

  6. Interactive setup tools.
    Code generation.
    Cache clearing / warming.

    View Slide

  7. Improve your
    productivity and effiency.

    View Slide

  8. Be proud to
    be lazy J

    View Slide

  9. Creating new command
    line tools in bundles

    View Slide

  10. The Command folder

    View Slide

  11. src/Sensio/Bundle/
    HangmanBundle/Command/
    GameHangmanCommand.php

    View Slide

  12. 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')
    ;
    }
    }

    View Slide

  13. Adding usage manual

    View Slide

  14. protected function configure()
    {
    $this->setHelp(<<The game:hangman command starts a new game of the
    famous hangman game:
    game:hangman 8
    Try to guess the hidden word whose length is
    8 before you reach the maximum number of
    attempts.
    You can also configure the maximum number of attempts
    with the --max-attempts option:
    game:hangman 8 --max-attempts=5
    EOF);
    }

    View Slide

  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),
    ));

    View Slide

  16. $ php app/console help game:hangman

    View Slide

  17. Executing a command
    protected function execute(
    InputInterface $input,
    OutputInterface $output)
    {
    // the business logic goes here...
    }

    View Slide

  18. InputInterface

    View Slide

  19. 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);
    }

    View Slide

  20. OutputInterface

    View Slide

  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();
    }

    View Slide

  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));
    }

    View Slide

  23. Validating the input
    arguments and options.

    View Slide

  24. // 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

    View Slide

  25. $ php app/console game:hangman foo
    $ php app/console game:hangman 8 --max-attempts=bar

    View Slide

  26. Formatting the output.

    View Slide

  27. The formatter helper
    class FormatterHelper extends Helper
    {
    public function formatSection($section, $message, $style);
    public function formatBlock($messages, $style, $large);
    }

    View Slide

  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');

    View Slide

  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),
    '',
    ));

    View Slide

  30. View Slide

  31. Make the command
    interact with the end user.

    View Slide

  32. Dialog Helper

    View Slide

  33. class DialogHelper extends Helper
    {
    public function ask(...);
    public function askConfirmation(...);
    public function askAndValidate(...);
    }

    View Slide

  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?');
    }
    }

    View Slide

  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);

    View Slide

  36. do {
    $answer = $dialog->askAndValidate(
    $output,
    'Type a letter or a word... ',
    array($this, 'validateLetter')
    );
    $currentAttempt++;
    } while ($currentAttempt <= $attempts);
    Asking and validating the answer

    View Slide

  37. 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

    View Slide

  38. Asking and validating the answer

    View Slide

  39. Refactoring your code is
    good for your command.

    View Slide

  40. Think your commands as
    controllers.

    View Slide

  41. Request <-> Response
    Input <-> Output

    View Slide

  42. 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);
    }

    View Slide

  43. 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();
    }

    View Slide

  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()));
    }

    View Slide

  45. 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

    View Slide

  46. View Slide

  47. Unit testing console
    commands

    View Slide

  48. Unit testing is
    about testing
    your model
    classes.

    View Slide

  49. 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

    View Slide

  50. Functional testing console
    commands

    View Slide

  51. Run the command and
    check the output.

    View Slide

  52. namespace Sensio\Bundle\DemoBundle\Command;
    class HelloWorldCommand extends Command
    {
    // ...
    protected function execute($input, $output)
    {
    $name = $input->getOption('name');
    $output->writeln('Your name is '. $name .'');
    }
    }
    The SayHello command

    View Slide

  53. StreamOutput

    View Slide

  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 Hugo',
    $output->getStream()
    );
    }
    }

    View Slide

  55. View Slide

  56. CommandTester

    View Slide

  57. 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();
    }

    View Slide

  58. 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 Hugo',
    $tester->getDisplay()
    );
    }
    }

    View Slide

  59. View Slide

  60. Being the God of the
    command line J

    View Slide

  61. Container

    View Slide

  62. ContainerAwareInterface

    View Slide

  63. namespace Symfony\Component\DependencyInjection;
    interface ContainerAwareInterface
    {
    /**
    * Sets the Container.
    *
    * @param ContainerInterface $container
    *
    * @api
    */
    function setContainer(ContainerInterface $container = null);
    }

    View Slide

  64. 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');
    }
    }

    View Slide

  65. ContainerAwareCommand

    View Slide

  66. 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;
    }
    }

    View Slide

  67. $container = $this->getContainer();
    $max = $container->getParameter('hangman.max_attempts');
    Reading the con guration

    View Slide

  68. $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

    View Slide

  69. $container = $this->getContainer();
    $templating = $container->get('templating'):
    $content = $templating->render(
    'SensioHangmanBundle:Game:finish.txt.twig',
    array('game' => $this->game)
    );
    Rendering Twig templates

    View Slide

  70. $container = $this->getContainer();
    $router = $container->get('router'):
    $url = $router->generate(
    'game_finish',
    array('user' => 'hhamon'),
    true
    );
    Generating urls

    View Slide

  71. $container = $this->getContainer();
    $translator = $container->get('translator'):
    $content = $translator->trans(
    'Hello %user%!',
    array('user' => 'hhamon'),
    null,
    'fr'
    );
    Translating messages

    View Slide

  72. $container = $this->getContainer();
    $logger = $container->get('logger');
    $logger->info('Game finished!');
    Writing logs

    View Slide

  73. $container = $this->getContainer();
    $fs = $container->get('filesystem');
    $fs->touch('/path/to/toto.txt');
    Dealing with the lesystem

    View Slide

  74. Conclusion

    View Slide

  75. Ask a (little) ninja J
    Questions & Answers

    View Slide

  76. •  Calling  a  command  from  a  command  
    •  Calling  a  command  in  a  command  
    •  Sending  an  email  

    View Slide