Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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 full-size slide

  2. Hugo
    Hamon
    Who’s talking?

    View full-size slide

  3. @hhamon
    Follow me on Twitter…

    View full-size slide

  4. Introduction to the Console
    Component

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  7. Improve your
    productivity and effiency.

    View full-size slide

  8. Be proud to
    be lazy J

    View full-size slide

  9. Creating new command
    line tools in bundles

    View full-size slide

  10. The Command folder

    View full-size slide

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

    View full-size 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 full-size slide

  13. Adding usage manual

    View full-size 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 full-size 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 full-size slide

  16. $ php app/console help game:hangman

    View full-size slide

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

    View full-size slide

  18. InputInterface

    View full-size 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 full-size slide

  20. OutputInterface

    View full-size 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 full-size 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 full-size slide

  23. Validating the input
    arguments and options.

    View full-size 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 full-size slide

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

    View full-size slide

  26. Formatting the output.

    View full-size slide

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

    View full-size 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 full-size 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 full-size slide

  30. Make the command
    interact with the end user.

    View full-size slide

  31. Dialog Helper

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  36. 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 full-size slide

  37. Asking and validating the answer

    View full-size slide

  38. Refactoring your code is
    good for your command.

    View full-size slide

  39. Think your commands as
    controllers.

    View full-size slide

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

    View full-size slide

  41. 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 full-size slide

  42. 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 full-size slide

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

    View full-size slide

  44. 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 full-size slide

  45. Unit testing console
    commands

    View full-size slide

  46. Unit testing is
    about testing
    your model
    classes.

    View full-size slide

  47. 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 full-size slide

  48. Functional testing console
    commands

    View full-size slide

  49. Run the command and
    check the output.

    View full-size slide

  50. 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 full-size slide

  51. StreamOutput

    View full-size slide

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

    View full-size slide

  53. CommandTester

    View full-size slide

  54. 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 full-size slide

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

    View full-size slide

  56. Being the God of the
    command line J

    View full-size slide

  57. ContainerAwareInterface

    View full-size slide

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

    View full-size slide

  59. 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 full-size slide

  60. ContainerAwareCommand

    View full-size slide

  61. 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 full-size slide

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

    View full-size slide

  63. $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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide