Symfony2 - extending the console component

Post on 07-Nov-2014

8.679 views 2 download

Tags:

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

Extending and Leveraging the Power of the CLI.

Hugo Hamon

Who’s talking?

@hhamon

Follow me on Twitter…

Introduction to the Console Component

CRON jobs and batch processing.

Redondant and tedious tasks.

Interactive setup tools.

Code generation.

Cache clearing / warming. …

Improve your productivity and effiency.

Be proud to be lazy J

Creating new command line tools in bundles

The Command folder

src/Sensio/Bundle/HangmanBundle/Command/

GameHangmanCommand.php

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

Adding usage manual

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

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

$ php app/console help game:hangman

Executing a command

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

InputInterface

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

OutputInterface

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

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

Validating the input arguments and options.

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

$ php app/console game:hangman foo

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

Formatting the output.

The formatter helper

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

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

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

Make the command interact with the end user.

Dialog Helper

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

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

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

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

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

Asking and validating the answer

Asking and validating the answer

Refactoring your code is good for your command.

Think your commands as controllers.

Request <-> Response Input <-> Output

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

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

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

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

Unit testing console commands

Unit testing is about testing your model classes.

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

Functional testing console commands

Run the command and check the output.

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

StreamOutput

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

CommandTester

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

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

Being the God of the command line J

Container

ContainerAwareInterface

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

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

ContainerAwareCommand

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

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

Reading the con"guration

$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

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

Rendering Twig templates

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

Generating urls

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

Translating messages

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

Writing logs

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

Dealing with the "lesystem

Conclusion

Ask a (little) ninja J

Questions & Answers

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