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

ZCEU - Console applications made easy

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

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

    View Slide

  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

    View Slide

  3. Follow me
    @danielcsgomes
    Rate & Feedback
    https://joind.in/9269

    View Slide

  4. easy console
    applications?

    View Slide

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

    View Slide

  6. !
    // /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();

    View Slide

  7. $ php app.php

    View Slide

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

    View Slide

  9. What I needed
    •Run background jobs
    •Interactive setup tools
    •Cache clean up / warming
    •Basically to do a specific task

    View Slide

  10. What I got
    • Several script files
    • Written in Bash, PHP, Python, etc
    • No documentation
    • Not very friendly for other users

    View Slide

  11. I had an
    idea
    Cayusa © http://farm2.staticflickr.com/1288/981372736_74e2d99d8f_b_d.jpg

    View Slide

  12. Why not …
    • Centralize everything
    • Good documentation
    • Tests
    • User friendly

    View Slide

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

    View Slide

  14. Zero Dependencies
    {
    "require": {
    "symfony/console": "2.3.*@dev"
    }
    }
    Install via composer

    View Slide

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

    View Slide

  16. Let’s start

    View Slide

  17. 1.
    Create
    the application

    View Slide

  18. !
    // /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();

    View Slide

  19. $ php app.php

    View Slide

  20. How it works
    APPLICATION
    COMMAND A
    COMMAND B
    COMMAND C
    Plug-in & run

    View Slide

  21. $app = new Application();
    $app->add(new MyCommand());
    $app->add(…);

    $app->run();

    View Slide

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

    View Slide

  23. !
    2.
    Create commands

    View Slide

  24. class MyCommand extends Command
    {
    protected function configure(){…}
    !
    protected function execute($input, $output){…}
    !
    protected function interact($input, $output){…}
    }

    View Slide

  25. How it works
    Command Configure
    Interact
    Execute
    is
    Interactive?
    yes
    no

    View Slide

  26. Bootstrap
    the command

    View Slide

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

    View Slide

  28. $ php app.php

    View Slide

  29. Arguments
    and options

    View Slide

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

    View Slide

  31. Options
    • unordered
    • input type:
    • optional
    • required
    • array
    • none (no input)

    View Slide

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

    View Slide

  33. !
    usage manual

    View Slide

  34. protected function configure()
    {
    // ...
    $this->setHelp(<<This is a simple command that outputs
    “Hello world 'Your Name’.”
    to the console.
    EOF
    );
    }

    View Slide

  35. $ php app.php hello:world —help

    View Slide

  36. Add
    business logic

    View Slide

  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 $name.");
    }
    }

    View Slide

  38. $ php app.php hello:world 'Daniel Gomes'

    View Slide

  39. Always validate
    the input

    View Slide

  40. protected function execute(
    InputInterface $input,
    OutputInterface $output)
    {
    $name = $input->getArgument(‘name');
    !
    if (preg_match("/[0-9]+/", $name)) {
    throw new \InvalidArgumentException('Invalid name.');
    }
    // …
    }

    View Slide

  41. $ php app.php hello:world 1

    View Slide

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

    View Slide

  43. Dialog Helper

    View Slide

  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 (...){...}

    View Slide

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

    View Slide

  46. $ php app.php examples:select

    View Slide

  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 {$name}");

    View Slide

  48. $ php app.php examples:dialog

    View Slide

  49. Formatter Helper

    View Slide

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

    View Slide

  51. $ php app.php examples:formatter

    View Slide

  52. Progress Helper

    View Slide

  53. $progress = $this->getHelperSet()->get('progress');
    !
    $progress->start($output, 50);
    $i = 0;
    while ($i++ < 50) {
    sleep(1);
    $progress->advance();
    }
    !
    $progress->finish();

    View Slide

  54. 1/10 [==>-------------------------] 10%
    !
    10/10 [============================] 100%
    $ php app.php examples:progress

    View Slide

  55. Table Helper

    View Slide

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

    View Slide

  57. $ php app.php examples:table

    View Slide

  58. Use case
    Dump Databases

    View Slide

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

    View Slide

  60. protected function execute($input, $output)
    {
    $mysqldump = $this->composeExecCommand(
    $this->getOptions($input)
    );
    $exitCode = 0;
    $execOutput = array();
    exec($mysqldump, $execOutput, $exitCode);
    if (0 === $exitCode) {
    $message = "Databases dumped with success.";
    } else {
    $message = "Error with exit code: {$exitCode}";
    }
    $output->writeln($message);
    }

    View Slide

  61. $ php app.php database:dump

    View Slide

  62. Events
    •command - before run
    •exception - on exceptions
    •terminate - before return exit code
    •extend to add custom event
    note: requires Symfony EventDispatcher Component

    View Slide

  63. !
    3.
    Testing

    View Slide

  64. think in your
    command tests
    as functional tests

    View Slide

  65. CommandTester

    View Slide

  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(){…}
    }

    View Slide

  67. Test the name output

    View Slide

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

    View Slide

  69. $ ./vendor/bin/phpunit

    View Slide

  70. 4.
    Dependency Injection
    in your CLI app

    View Slide

  71. Container

    View Slide

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

    View Slide

  73. ContainerAwareInterface

    View Slide

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

    View Slide

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

    View Slide

  76. Example
    Logging

    View Slide

  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 = "Success";
    $logger->addInfo('Databases dumped with success.’);
    } else {
    $message = "Error with exit code: {$exitCode}";
    $logger->addCritical('Error dumping databases.');
    touch($filename);
    }
    return $filename;
    }

    View Slide

  78. 5.
    Catching Signals

    View Slide

  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

    View Slide

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

    View Slide

  81. $ ./bin/app examples:catch:signal

    View Slide

  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

    View Slide

  83. http://conference.phplx.net
    @phplxConf

    View Slide

  84. @danielcsgomes | [email protected] | http://danielcsgomes.com
    Photo by Jian Awe © http://www.flickr.com/photos/qqjawe/6511141237
    Please give feedback:
    https://joind.in/9269
    Questions?

    View Slide