Slide 1

Slide 1 text

Extending and Leveraging the Power of the CLI.

Slide 2

Slide 2 text

Hugo Hamon Who’s talking?

Slide 3

Slide 3 text

@hhamon Follow me on Twitter…

Slide 4

Slide 4 text

Introduction to the Console Component

Slide 5

Slide 5 text

CRON jobs and batch processing. Redondant and tedious tasks.

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Improve your productivity and effiency.

Slide 8

Slide 8 text

Be proud to be lazy J

Slide 9

Slide 9 text

Creating new command line tools in bundles

Slide 10

Slide 10 text

The Command folder

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Adding usage manual

Slide 14

Slide 14 text

protected function configure() { $this->setHelp(<<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); }

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

$ php app/console help game:hangman

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

InputInterface

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

OutputInterface

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Validating the input arguments and options.

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Formatting the output.

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

Make the command interact with the end user.

Slide 32

Slide 32 text

Dialog Helper

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

Asking and validating the answer

Slide 39

Slide 39 text

Refactoring your code is good for your command.

Slide 40

Slide 40 text

Think your commands as controllers.

Slide 41

Slide 41 text

Request <-> Response Input <-> Output

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

Unit testing console commands

Slide 48

Slide 48 text

Unit testing is about testing your model classes.

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Functional testing console commands

Slide 51

Slide 51 text

Run the command and check the output.

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

StreamOutput

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

CommandTester

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

No content

Slide 60

Slide 60 text

Being the God of the command line J

Slide 61

Slide 61 text

Container

Slide 62

Slide 62 text

ContainerAwareInterface

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

ContainerAwareCommand

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

$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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

Conclusion

Slide 75

Slide 75 text

Ask a (little) ninja J Questions & Answers

Slide 76

Slide 76 text

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