Symfony2 - extending the console component

76
Extending and Leveraging the Power of the CLI.

description

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.

Transcript of Symfony2 - extending the console component

Page 1: Symfony2 - extending the console component

Extending and Leveraging the Power of the CLI.

Page 2: Symfony2 - extending the console component

Hugo Hamon

Who’s talking?

Page 3: Symfony2 - extending the console component

@hhamon

Follow me on Twitter…

Page 4: Symfony2 - extending the console component

Introduction to the Console Component

Page 5: Symfony2 - extending the console component

CRON jobs and batch processing.

Redondant and tedious tasks.

Page 6: Symfony2 - extending the console component

Interactive setup tools.

Code generation.

Cache clearing / warming. …

Page 7: Symfony2 - extending the console component

Improve your productivity and effiency.

Page 8: Symfony2 - extending the console component

Be proud to be lazy J

Page 9: Symfony2 - extending the console component

Creating new command line tools in bundles

Page 10: Symfony2 - extending the console component

The Command folder

Page 11: Symfony2 - extending the console component

src/Sensio/Bundle/HangmanBundle/Command/

GameHangmanCommand.php

Page 12: Symfony2 - extending the console component

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

Page 13: Symfony2 - extending the console component

Adding usage manual

Page 14: Symfony2 - extending the console component

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

Page 15: Symfony2 - extending the console component

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

Page 16: Symfony2 - extending the console component

$ php app/console help game:hangman

Page 17: Symfony2 - extending the console component

Executing a command

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

Page 18: Symfony2 - extending the console component

InputInterface

Page 19: Symfony2 - extending the console component

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

Page 20: Symfony2 - extending the console component

OutputInterface

Page 21: Symfony2 - extending the console component

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

Page 22: Symfony2 - extending the console component

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

Page 23: Symfony2 - extending the console component

Validating the input arguments and options.

Page 24: Symfony2 - extending the console component

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

Page 25: Symfony2 - extending the console component

$ php app/console game:hangman foo

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

Page 26: Symfony2 - extending the console component

Formatting the output.

Page 27: Symfony2 - extending the console component

The formatter helper

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

Page 28: Symfony2 - extending the console component

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

Page 29: Symfony2 - extending the console component

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

Page 30: Symfony2 - extending the console component
Page 31: Symfony2 - extending the console component

Make the command interact with the end user.

Page 32: Symfony2 - extending the console component

Dialog Helper

Page 33: Symfony2 - extending the console component

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

Page 34: Symfony2 - extending the console component

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

Page 35: Symfony2 - extending the console component

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

Page 36: Symfony2 - extending the console component

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

Asking and validating the answer

Page 37: Symfony2 - extending the console component

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

Page 38: Symfony2 - extending the console component

Asking and validating the answer

Page 39: Symfony2 - extending the console component

Refactoring your code is good for your command.

Page 40: Symfony2 - extending the console component

Think your commands as controllers.

Page 41: Symfony2 - extending the console component

Request <-> Response Input <-> Output

Page 42: Symfony2 - extending the console component

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

Page 43: Symfony2 - extending the console component

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

Page 44: Symfony2 - extending the console component

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

Page 45: Symfony2 - extending the console component

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

Page 46: Symfony2 - extending the console component
Page 47: Symfony2 - extending the console component

Unit testing console commands

Page 48: Symfony2 - extending the console component

Unit testing is about testing your model classes.

Page 49: Symfony2 - extending the console component

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

Page 50: Symfony2 - extending the console component

Functional testing console commands

Page 51: Symfony2 - extending the console component

Run the command and check the output.

Page 52: Symfony2 - extending the console component

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

The SayHello command

Page 53: Symfony2 - extending the console component

StreamOutput

Page 54: Symfony2 - extending the console component

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

Page 55: Symfony2 - extending the console component
Page 56: Symfony2 - extending the console component

CommandTester

Page 57: Symfony2 - extending the console component

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

Page 58: Symfony2 - extending the console component

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

Page 59: Symfony2 - extending the console component
Page 60: Symfony2 - extending the console component

Being the God of the command line J

Page 61: Symfony2 - extending the console component

Container

Page 62: Symfony2 - extending the console component

ContainerAwareInterface

Page 63: Symfony2 - extending the console component

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

Page 64: Symfony2 - extending the console component

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

Page 65: Symfony2 - extending the console component

ContainerAwareCommand

Page 66: Symfony2 - extending the console component

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

Page 67: Symfony2 - extending the console component

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

Reading the con"guration

Page 68: Symfony2 - extending the console component

$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

Page 69: Symfony2 - extending the console component

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

Rendering Twig templates

Page 70: Symfony2 - extending the console component

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

Generating urls

Page 71: Symfony2 - extending the console component

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

Translating messages

Page 72: Symfony2 - extending the console component

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

Writing logs

Page 73: Symfony2 - extending the console component

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

Dealing with the "lesystem

Page 74: Symfony2 - extending the console component

Conclusion

Page 75: Symfony2 - extending the console component

Ask a (little) ninja J

Questions & Answers

Page 76: Symfony2 - extending the console component

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