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

ZCEU - Console applications made easy

0fe2d959c89cf2d9de497a237c4ea99d?s=47 Daniel Gomes
November 20, 2013

ZCEU - Console applications made easy

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. The Symfony2 Console component is a tool that gives you a simple and easy way to build a console application with your own command-line commands. This talk is an introduction to the Console component and a walkthrough on how to use it in an effective way.

Source code of the slides: https://github.com/danielcsgomes/ZendCon-console-applications-made-easy

0fe2d959c89cf2d9de497a237c4ea99d?s=128

Daniel Gomes

November 20, 2013
Tweet

More Decks by Daniel Gomes

Other Decks in Programming

Transcript

  1. Console applications made easy Daniel Gomes @danielcsgomes November 20, 2013

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

    Co-founder & organizer @ phplx • Father of a beautiful boy • ZCE PHP 5.3, CSM, OCP MySQL 5 Developer
  3. Follow me @danielcsgomes Rate & Feedback https://joind.in/9269

  4. easy 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();
  7. $ php app.php

  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
  10. What I got • Several script files • Written in

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

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

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

  14. Zero Dependencies { "require": { "symfony/console": "2.3.*@dev" } } 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();
  19. $ php app.php

  20. How it works APPLICATION COMMAND A COMMAND B COMMAND C

    Plug-in & run
  21. $app = new Application(); $app->add(new MyCommand()); $app->add(…); … $app->run();

  22. command <-> controller input <-> request output <-> response CLI

    <-> Web
  23. ! 2. Create commands

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

    function execute($input, $output){…} ! protected function interact($input, $output){…} }
  25. How it works Command Configure Interact Execute is Interactive? yes

    no
  26. Bootstrap the command

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

    { protected function configure() { $this->setName('hello:world') ->setDescription('Hello World <name>'); } }
  28. $ php app.php

  29. Arguments and options

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

    • array
  31. Options • unordered • input type: • optional • required

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

    }
  33. ! usage manual

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

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

  36. Add business logic

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

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

  39. Always validate the input

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

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

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

  43. Dialog Helper

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

    ! public function askConfirmation(...){...} ! public function askHiddenResponse(...){...} ! public function askAndValidate(...){...} ! public function askHiddenResponseAndValidate (...){...}
  45. 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]}"); }
  46. $ php app.php examples:select

  47. $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>");
  48. $ php app.php examples:dialog

  49. Formatter Helper

  50. ! $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);
  51. $ php app.php examples:formatter

  52. Progress Helper

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

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

    examples:progress
  55. Table Helper

  56. $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);
  57. $ php app.php examples:table

  58. Use case Dump Databases

  59. 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 ); }
  60. protected function execute($input, $output) { $mysqldump = $this->composeExecCommand( $this->getOptions($input) );

    $exitCode = 0; $execOutput = array(); exec($mysqldump, $execOutput, $exitCode); if (0 === $exitCode) { $message = "<info>Databases dumped with success.</info>"; } else { $message = "<error>Error with exit code: {$exitCode}"; } $output->writeln($message); }
  61. $ php app.php database:dump

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

    - before return exit code •extend to add custom event note: requires Symfony EventDispatcher Component
  63. ! 3. Testing

  64. think in your command tests as functional tests

  65. CommandTester

  66. 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(){…} }
  67. Test the name output

  68. 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() ); }
  69. $ ./vendor/bin/phpunit

  70. 4. Dependency Injection in your CLI app

  71. Container

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

    Monolog …
  73. ContainerAwareInterface

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

    Container. * * @param ContainerInterface|null $container */ public function setContainer(ContainerInterface $container = null); }
  75. 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'); // ... } ! // ... }
  76. Example Logging

  77. 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; }
  78. 5. Catching Signals

  79. 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
  80. 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; }
  81. $ ./bin/app examples:catch:signal

  82. 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/ZendCon-console-applications-made-easy https://github.com/symfony/Console Catching Signals http://signalingphp.com https://github.com/Cilex/Cilex Cilex
  83. http://conference.phplx.net @phplxConf

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

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