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

How to build Console Applications | SymfonyCon 2013

How to build Console Applications | SymfonyCon 2013

Source code: https://github.com/danielcsgomes/symfonycon-how-to-build-console-apps

In some cases you feel the need to have some specific command-line commands for deployment, testing some code or do any other specific task and you usually use shell script to do it.

Why not use Symfony2 Console component? You can easily build your own commands and interact with other bundles/components.

The Symfony2 Console component is a tool that gives you to build in a simple and easy way console applications.

This talk will focus on how you can simple build an Console applications and how you can interact with other bundles/components.

0fe2d959c89cf2d9de497a237c4ea99d?s=128

Daniel Gomes

December 13, 2013
Tweet

More Decks by Daniel Gomes

Other Decks in Programming

Transcript

  1. How to build Console Applications Daniel Gomes @danielcsgomes December 13,

    2013
  2. Who am I • Senior Software Engineer @ GuestCentric Systems

    • Co-founder & organizer @ phplx • Co-founder of a beautiful boy • ZCE PHP 5.3, CSM, OCP MySQL 5 Developer
  3. me on the web @danielcsgomes rate & feedback https://joind.in/10376

  4. is easy to create console applications?

  5. visualgrover © http://farm4.staticflickr.com/3081/2286086833_91f262868a_o_d.jpg are you nuts?

  6. <?php ! // /path/to/app.php require_once __DIR__ . '/vendor/autoload.php'; ! use

    Symfony\Component\Console\Application; ! $app = new Application('My Console App', '0.0.1'); $app->run(); Daniel Gomes @danielcsgomes
  7. $ php app.php Daniel Gomes @danielcsgomes

  8. Thamara Maura © http://farm9.staticflickr.com/8491/8301588586_1ef18f3d9c_o_d.jpg

  9. What I needed •Run background jobs •Interactive setup tools •Cache

    clean up / warming •Basically to do a specific task Daniel Gomes @danielcsgomes
  10. What I got • Several script files • Written in

    Bash, PHP, Python, etc • No documentation • Not very friendly for other users Daniel Gomes @danielcsgomes
  11. I had an idea © Cayusa http://farm2.staticflickr.com/1288/981372736_74e2d99d8f_b_d.jpg Daniel Gomes @danielcsgomes

  12. Why not … • Centralize everything • Good documentation •

    Tests • User friendly Daniel Gomes @danielcsgomes
  13. Hello Symfony Console Component http://symfony.com/doc/current/components/console/introduction.html

  14. Zero Dependencies { "require": { "symfony/console": "~2.4" } } Install

    via composer
  15. What it brings •Application •Commands •Inputs •Outputs •Helpers •Events •Formatters

  16. Let’s start

  17. 1. Create the application

  18. <?php ! // /path/to/app.php require_once __DIR__ . '/vendor/autoload.php'; ! use

    Symfony\Component\Console\Application; ! $app = new Application('My Console App', '0.0.1'); $app->run(); Daniel Gomes @danielcsgomes
  19. $ php app.php Daniel Gomes @danielcsgomes

  20. ! How it works

  21. Standalone Console Component APPLICATION COMMAND A COMMAND B COMMAND C

    Plug-in & run Daniel Gomes @danielcsgomes
  22. with Symfony Framework ! ├── app ├── bin ├── src

    └── Acme └── DemoBundle ├── Command │ ├── HelloWorldCommand.php │ └── <your commands> ├── vendor └── web Autoload all commands inside the Bundles Command folder Daniel Gomes @danielcsgomes
  23. command <-> controller input <-> request output <-> response CLI

    <-> Web
  24. $app = new Application(); $app->add(new MyCommand()); $app->add(…); … $app->run(); Daniel

    Gomes @danielcsgomes
  25. ! 2. Create the command

  26. class MyCommand extends Command { protected function configure(){…} ! protected

    function execute($input, $output){…} ! protected function interact($input, $output){…} } Daniel Gomes @danielcsgomes
  27. Workflow Command Configure Interact Execute is Interactive? yes no Daniel

    Gomes @danielcsgomes
  28. Configure the command

  29. namespace DCSG\Command; ! use Symfony\Component\Console\Command\Command; ! class HelloWorldCommand extends Command

    { protected function configure() { $this->setName('hello:world') ->setDescription('Hello World <name>'); } } Daniel Gomes @danielcsgomes
  30. namespace DCSG\Command; ! use Symfony\Component\Console\Command\Command; ! class HelloWorldCommand extends Command

    { protected function configure() { $this->setName('hello:world') ->setDescription('Hello World <name>'); } } Daniel Gomes @danielcsgomes namespace
  31. namespace DCSG\Command; ! use Symfony\Component\Console\Command\Command; ! class HelloWorldCommand extends Command

    { protected function configure() { $this->setName('hello:world') ->setDescription('Hello World <name>'); } } Daniel Gomes @danielcsgomes namespace name
  32. $ php app.php Daniel Gomes @danielcsgomes

  33. Arguments and options

  34. Arguments • ordered • input type: • optional • required

    • array Daniel Gomes @danielcsgomes
  35. Options • unordered • input type: • optional • required

    • array • none (no input) Daniel Gomes @danielcsgomes
  36. protected function configure() { // ... $this->addArgument('name', InputArgument::REQUIRED); $this->addOption('uppercase', 'u');

    } Daniel Gomes @danielcsgomes
  37. ! usage manual

  38. protected function configure() { // ... $this->setHelp(<<<EOF This is a

    simple command that outputs “<info>Hello world</info> 'Your Name’.” to the console. EOF ); } Daniel Gomes @danielcsgomes
  39. $ php app.php hello:world —help Daniel Gomes @danielcsgomes

  40. $ php app.php hello:world —help Daniel Gomes @danielcsgomes

  41. $ php app.php hello:world —help Daniel Gomes @danielcsgomes

  42. $ php app.php hello:world —help Daniel Gomes @danielcsgomes

  43. $ php app.php hello:world —help Daniel Gomes @danielcsgomes

  44. Add logic

  45. class HelloWorldCommand extends Command { // ... ! protected function

    execute( InputInterface $input, OutputInterface $output) { $name = $input->getArgument(‘name'); ! if ($input->getOption('uppercase')) { $name = strtoupper($name); } ! $output->writeln("Hello World <info>$name</info>."); } } Daniel Gomes @danielcsgomes
  46. $ php app.php hello:world 'Daniel Gomes' Daniel Gomes @danielcsgomes

  47. Call a command from other command

  48. protected function execute($input, $output) { $command = $this->getApplication()->find('hello:world'); ! $arguments

    = array( 'command' => 'hello:world', 'name' => 'Daniel Gomes' ); ! $returnCode = $command->run( new ArrayInput($arguments), $output ); $output->writeln("Exit code $returnCode"); } } Daniel Gomes @danielcsgomes
  49. protected function execute($input, $output) { $command = $this->getApplication()->find('hello:world'); ! $arguments

    = array( 'command' => 'hello:world', 'name' => 'Daniel Gomes' ); ! $returnCode = $command->run( new ArrayInput($arguments), $output ); $output->writeln("Exit code $returnCode"); } } Daniel Gomes @danielcsgomes
  50. protected function execute($input, $output) { $command = $this->getApplication()->find('hello:world'); ! $arguments

    = array( 'command' => 'hello:world', 'name' => 'Daniel Gomes' ); ! $returnCode = $command->run( new ArrayInput($arguments), $output ); $output->writeln("Exit code $returnCode"); } } Daniel Gomes @danielcsgomes
  51. protected function execute($input, $output) { $command = $this->getApplication()->find('hello:world'); ! $arguments

    = array( 'command' => 'hello:world', 'name' => 'Daniel Gomes' ); ! $returnCode = $command->run( new ArrayInput($arguments), $output ); $output->writeln("Exit code $returnCode"); } } Daniel Gomes @danielcsgomes
  52. $ php app.php examples:calling:command Daniel Gomes @danielcsgomes

  53. Always validate the input

  54. protected function execute( InputInterface $input, OutputInterface $output) { $name =

    $input->getArgument(‘name'); ! if (preg_match("/[0-9]+/", $name)) { throw new \InvalidArgumentException('Invalid name.'); } // … } Daniel Gomes @danielcsgomes
  55. $ php app.php hello:world 1 Daniel Gomes @danielcsgomes

  56. User Interaction •Dialog Helper •Formatter Helper •Progress Helper •Table Helper

    Daniel Gomes @danielcsgomes
  57. Dialog Helper

  58. // Symfony/Component/Console/Helper/DialogHelper.php ! public function select(...){...} ! public function ask(...){...}

    ! public function askConfirmation(...){...} ! public function askHiddenResponse(...){...} ! public function askAndValidate(...){...} ! public function askHiddenResponseAndValidate (...){...} Daniel Gomes @danielcsgomes
  59. protected function execute($input, $output) { $colors = array('Red', 'Yellow', 'Green',

    'Blue', 'Black'); $dialog = $this->getHelperSet()->get('dialog'); $index = $dialog->select( $output, 'Please select your favorite color:', $colors ); $output->writeln("Your favorite color is {$colors[$index]}"); } Daniel Gomes @danielcsgomes
  60. $ php app.php examples:select Daniel Gomes @danielcsgomes

  61. $name = $this->getHelper('dialog')->askAndValidate( $output, 'Insert your name: ', function ($name)

    { if (empty($name)) { throw new \InvalidArgumentException( 'The name cannot be empty.’ ); } ! return $name; } ); $output->writeln("Your name is <info>{$name}</info>"); Daniel Gomes @danielcsgomes
  62. $ php app.php examples:dialog Daniel Gomes @danielcsgomes

  63. Formatter Helper

  64. ! $fmt = $this->getHelperSet()->get('formatter'); ! $formattedLine = $fmt->formatSection( ‘My Section',

    'Here is some message related to that section' ); $output->writeln($formattedLine); ! $msg = array('Something went wrong'); $fmtBlock = $fmt->formatBlock($msg, 'error'); $output->writeln($fmtBlock); ! $msg = array('Custom Colors'); $fmtBlock = $fmt->formatBlock($msg, ‘bg=blue;fg=white'); $output->writeln($fmtBlock); Daniel Gomes @danielcsgomes
  65. ! $fmt = $this->getHelperSet()->get('formatter'); ! $formattedLine = $fmt->formatSection( ‘My Section',

    'Here is some message related to that section' ); $output->writeln($formattedLine); ! $msg = array('Something went wrong'); $fmtBlock = $fmt->formatBlock($msg, 'error'); $output->writeln($fmtBlock); ! $msg = array('Custom Colors'); $fmtBlock = $fmt->formatBlock($msg, ‘bg=blue;fg=white'); $output->writeln($fmtBlock); Daniel Gomes @danielcsgomes
  66. ! $fmt = $this->getHelperSet()->get('formatter'); ! $formattedLine = $fmt->formatSection( ‘My Section',

    'Here is some message related to that section' ); $output->writeln($formattedLine); ! $msg = array('Something went wrong'); $fmtBlock = $fmt->formatBlock($msg, 'error'); $output->writeln($fmtBlock); ! $msg = array('Custom Colors'); $fmtBlock = $fmt->formatBlock($msg, ‘bg=blue;fg=white'); $output->writeln($fmtBlock); Daniel Gomes @danielcsgomes
  67. ! $fmt = $this->getHelperSet()->get('formatter'); ! $formattedLine = $fmt->formatSection( ‘My Section',

    'Here is some message related to that section' ); $output->writeln($formattedLine); ! $msg = array('Something went wrong'); $fmtBlock = $fmt->formatBlock($msg, 'error'); $output->writeln($fmtBlock); ! $msg = array('Custom Colors'); $fmtBlock = $fmt->formatBlock($msg, ‘bg=blue;fg=white'); $output->writeln($fmtBlock); Daniel Gomes @danielcsgomes
  68. ! $fmt = $this->getHelperSet()->get('formatter'); ! $formattedLine = $fmt->formatSection( ‘My Section',

    'Here is some message related to that section' ); $output->writeln($formattedLine); ! $msg = array('Something went wrong'); $fmtBlock = $fmt->formatBlock($msg, 'error'); $output->writeln($fmtBlock); ! $msg = array('Custom Colors'); $fmtBlock = $fmt->formatBlock($msg, ‘bg=blue;fg=white'); $output->writeln($fmtBlock); Daniel Gomes @danielcsgomes
  69. $ php app.php examples:formatter Daniel Gomes @danielcsgomes

  70. Progress Helper

  71. $progress = $this->getHelperSet()->get('progress'); ! $progress->start($output, 50); $i = 0; while

    ($i++ < 50) { sleep(1); $progress->advance(); } ! $progress->finish(); Daniel Gomes @danielcsgomes
  72. 1/10 [==>-------------------------] 10% ! 10/10 [============================] 100% $ php app.php

    examples:progress Daniel Gomes @danielcsgomes
  73. Table Helper

  74. $table = $this->getHelperSet()->get('table'); $table ->setHeaders(array('Color', 'HEX')) ->setRows( array( array('Red', '#ff0000'),

    array('Blue', '#0000ff'), array('Green', '#008000'), array('Yellow', '#ffff00') ) ); $table->render($output); Daniel Gomes @danielcsgomes
  75. $ php app.php examples:table Daniel Gomes @danielcsgomes

  76. Use case Dump Databases

  77. protected function interact($input, $output) { $isEmpty = function($value) { if

    (empty($value)) { throw new \InvalidArgumentException('Value cannot be empty.'); } return $value; }; $dialog = $this->getHelper('dialog'); $this->host = $dialog->askAndValidate($output, 'host: ', $isEmpty); $this->user = $dialog->askAndValidate($output, 'username: ', $isEmpty); $this->password = $dialog->askHiddenResponseAndValidate( $output, 'password: ', $isEmpty ); $this->dbnames = $dialog->askAndValidate( $output, 'databases separate by space: ', $isEmpty ); } Daniel Gomes @danielcsgomes
  78. protected function interact($input, $output) { $isEmpty = function($value) { if

    (empty($value)) { throw new \InvalidArgumentException('Value cannot be empty.'); } return $value; }; $dialog = $this->getHelper('dialog'); $this->host = $dialog->askAndValidate($output, 'host: ', $isEmpty); $this->user = $dialog->askAndValidate($output, 'username: ', $isEmpty); $this->password = $dialog->askHiddenResponseAndValidate( $output, 'password: ', $isEmpty ); $this->dbnames = $dialog->askAndValidate( $output, 'databases separate by space: ', $isEmpty ); } Daniel Gomes @danielcsgomes
  79. protected function interact($input, $output) { $isEmpty = function($value) { if

    (empty($value)) { throw new \InvalidArgumentException('Value cannot be empty.'); } return $value; }; $dialog = $this->getHelper('dialog'); $this->host = $dialog->askAndValidate($output, 'host: ', $isEmpty); $this->user = $dialog->askAndValidate($output, 'username: ', $isEmpty); $this->password = $dialog->askHiddenResponseAndValidate( $output, 'password: ', $isEmpty ); $this->dbnames = $dialog->askAndValidate( $output, 'databases separate by space: ', $isEmpty ); } Daniel Gomes @danielcsgomes
  80. protected function interact($input, $output) { $isEmpty = function($value) { if

    (empty($value)) { throw new \InvalidArgumentException('Value cannot be empty.'); } return $value; }; $dialog = $this->getHelper('dialog'); $this->host = $dialog->askAndValidate($output, 'host: ', $isEmpty); $this->user = $dialog->askAndValidate($output, 'username: ', $isEmpty); $this->password = $dialog->askHiddenResponseAndValidate( $output, 'password: ', $isEmpty ); $this->dbnames = $dialog->askAndValidate( $output, 'databases separate by space: ', $isEmpty ); } Daniel Gomes @danielcsgomes
  81. Daniel Gomes @danielcsgomes protected function execute(… $input, … $output) {

    $mysqldump = $this->composeExecCommand($this->getOptions($input)); ! $process = new Process($mysqldump); $process->run(); ! // executes after the command finishes if (!$process->isSuccessful()) { throw new \RuntimeException($process->getErrorOutput()); } ! $output->writeln('<info>Databases dumped with success.</info>'); }
  82. Daniel Gomes @danielcsgomes protected function execute(… $input, … $output) {

    $mysqldump = $this->composeExecCommand($this->getOptions($input)); ! $process = new Process($mysqldump); $process->run(); ! // executes after the command finishes if (!$process->isSuccessful()) { throw new \RuntimeException($process->getErrorOutput()); } ! $output->writeln('<info>Databases dumped with success.</info>'); }
  83. Daniel Gomes @danielcsgomes protected function execute(… $input, … $output) {

    $mysqldump = $this->composeExecCommand($this->getOptions($input)); ! $process = new Process($mysqldump); $process->run(); ! // executes after the command finishes if (!$process->isSuccessful()) { throw new \RuntimeException($process->getErrorOutput()); } ! $output->writeln('<info>Databases dumped with success.</info>'); }
  84. Daniel Gomes @danielcsgomes protected function execute(… $input, … $output) {

    $mysqldump = $this->composeExecCommand($this->getOptions($input)); ! $process = new Process($mysqldump); $process->run(); ! // executes after the command finishes if (!$process->isSuccessful()) { throw new \RuntimeException($process->getErrorOutput()); } ! $output->writeln('<info>Databases dumped with success.</info>'); }
  85. $ php app.php database:dump Daniel Gomes @danielcsgomes

  86. Events •command - before run •exception - on exceptions •terminate

    - before return exit code •extend to add custom event note: requires Symfony EventDispatcher Component Daniel Gomes @danielcsgomes
  87. ! 3. Testing

  88. think in your command tests as functional tests

  89. CommandTester

  90. namespace Symfony\Component\Console\Tester; ! class CommandTester { private $command; private $input;

    private $output; ! public function __construct(Command $command){…} ! public function execute(array $input, array $options=array()){…} ! public function getDisplay($normalize = false){…} ! public function getInput(){…} ! public function getOutput(){…} }
  91. Test the name output

  92. public function testOutputNameInUppercase() { $command = new HelloWorldCommand(); $commandTester =

    new CommandTester($command); $commandTester->execute( array( 'command' => $command->getName(), 'name' => 'Daniel', '--uppercase' => true, ) ); ! $this->assertRegExp( '/DANIEL/', $commandTester->getDisplay() ); }
  93. $ ./vendor/bin/phpunit Daniel Gomes @danielcsgomes

  94. 4. Dependency Injection in your CLI app

  95. Container

  96. Services CLI Application My Commands Container DBAL Finder setContainer(...) getContainer()

    Monolog … Daniel Gomes @danielcsgomes
  97. ContainerAwareInterface

  98. namespace Symfony\Component\DependencyInjection; ! interface ContainerAwareInterface { /** * Sets the

    Container. * * @param ContainerInterface|null $container */ public function setContainer(ContainerInterface $container = null); }
  99. use Symfony\Component\Console\Command\Command; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; ! class HelloWorldCommand extends

    Command implements ContainerAwareInterface { private $container; ! public function setContainer(ContainerInterface $container = null) { $this->container = $container; } ! protected function execute($input, $output) { $this->container->get(‘my_service'); // ... } ! // ... }
  100. ContainerAwareCommand

  101. abstract class ContainerAwareCommand extends Command implements ContainerAwareInterface { /** @var

    ContainerInterface|null */ private $container; ! /** @return ContainerInterface */ 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; } }
  102. Example Logging with monolog

  103. protected function execute($input, $output) { $logger = $this->container->get('logger'); $mysqldump =

    $this->composeExecCommand($this->getOptions($input)); $exitCode = 0; $execOutput = array(); exec($mysqldump, $execOutput, $exitCode); if (0 === $exitCode) { $message = "<info>Success</info>"; $logger->addInfo('Databases dumped with success.’); } else { $message = "<error>Error with exit code: {$exitCode}"; $logger->addCritical('Error dumping databases.'); touch($filename); } return $filename; }
  104. 5. Catching Signals

  105. Catching Signals • SIGSTOP & SIGKILL cannot be catch •

    Only SIGINT can be triggered by shortcut (ctrl+c) • Find the best Tick value that fits your needs • Define inside your Commands • Create a base command • Read “Signaling PHP” book by Cal Evans http://www.signalingphp.com Daniel Gomes @danielcsgomes
  106. protected function execute($input, $output) { declare(ticks = 10); pcntl_signal(SIGINT, [$this,

    'signalHandler']); ! do { // do something interesting here. $this->write('.'); } while ($this->continueFlag); } ! public function signalHandler($signal) { ! echo "Caught a signal" . $signal . PHP_EOL; $this->continueFlag = false; } Daniel Gomes @danielcsgomes
  107. protected function execute($input, $output) { declare(ticks = 10); pcntl_signal(SIGINT, [$this,

    'signalHandler']); ! do { // do something interesting here. $this->write('.'); } while ($this->continueFlag); } ! public function signalHandler($signal) { ! echo "Caught a signal" . $signal . PHP_EOL; $this->continueFlag = false; } Daniel Gomes @danielcsgomes
  108. $ ./bin/app examples:catch:signal Daniel Gomes @danielcsgomes

  109. Resources http://goo.gl/h0Xcfe http://symfony.com/doc/current/components/process.html Long running processes http://symfony.com/doc/current/components/console/index.html http://symfony.com/doc/current/cookbook/console/index.html http://symfony.com/doc/current/cookbook/service_container/index.html http://symfony.com/doc/current/components/dependency_injection/index.html

    Symfony2 Docs Cron jobs http://www.cyberciti.biz/faq/how-do-i-add-jobs-to-cron-under-linux-or-unix-oses/ Source Code https://github.com/danielcsgomes/symfonycon-how-to-build-console-apps https://github.com/symfony/Console Catching Signals http://signalingphp.com https://github.com/Cilex/Cilex Cilex
  110. http://conference.phplx.net @phplxConf

  111. Questions? Daniel Gomes @danielcsgomes

  112. @danielcsgomes | me@danielcsgomes.com | http://danielcsgomes.com Photo by Jian Awe ©

    http://www.flickr.com/photos/qqjawe/6511141237 Please give feedback: https://joind.in/10376