Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

me on the web @danielcsgomes rate & feedback https://joind.in/10376

Slide 4

Slide 4 text

is easy to create console applications?

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

run(); Daniel Gomes @danielcsgomes

Slide 7

Slide 7 text

$ php app.php Daniel Gomes @danielcsgomes

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

What I needed •Run background jobs •Interactive setup tools •Cache clean up / warming •Basically to do a specific task Daniel Gomes @danielcsgomes

Slide 10

Slide 10 text

What I got • Several script files • Written in Bash, PHP, Python, etc • No documentation • Not very friendly for other users Daniel Gomes @danielcsgomes

Slide 11

Slide 11 text

I had an idea © Cayusa http://farm2.staticflickr.com/1288/981372736_74e2d99d8f_b_d.jpg Daniel Gomes @danielcsgomes

Slide 12

Slide 12 text

Why not … • Centralize everything • Good documentation • Tests • User friendly Daniel Gomes @danielcsgomes

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Zero Dependencies { "require": { "symfony/console": "~2.4" } } Install via composer

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Let’s start

Slide 17

Slide 17 text

1. Create the application

Slide 18

Slide 18 text

run(); Daniel Gomes @danielcsgomes

Slide 19

Slide 19 text

$ php app.php Daniel Gomes @danielcsgomes

Slide 20

Slide 20 text

! How it works

Slide 21

Slide 21 text

Standalone Console Component APPLICATION COMMAND A COMMAND B COMMAND C Plug-in & run Daniel Gomes @danielcsgomes

Slide 22

Slide 22 text

with Symfony Framework ! ├── app ├── bin ├── src └── Acme └── DemoBundle ├── Command │ ├── HelloWorldCommand.php │ └── ├── vendor └── web Autoload all commands inside the Bundles Command folder Daniel Gomes @danielcsgomes

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

$app = new Application(); $app->add(new MyCommand()); $app->add(…); … $app->run(); Daniel Gomes @danielcsgomes

Slide 25

Slide 25 text

! 2. Create the command

Slide 26

Slide 26 text

class MyCommand extends Command { protected function configure(){…} ! protected function execute($input, $output){…} ! protected function interact($input, $output){…} } Daniel Gomes @danielcsgomes

Slide 27

Slide 27 text

Workflow Command Configure Interact Execute is Interactive? yes no Daniel Gomes @danielcsgomes

Slide 28

Slide 28 text

Configure the command

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

$ php app.php Daniel Gomes @danielcsgomes

Slide 33

Slide 33 text

Arguments and options

Slide 34

Slide 34 text

Arguments • ordered • input type: • optional • required • array Daniel Gomes @danielcsgomes

Slide 35

Slide 35 text

Options • unordered • input type: • optional • required • array • none (no input) Daniel Gomes @danielcsgomes

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

! usage manual

Slide 38

Slide 38 text

protected function configure() { // ... $this->setHelp(<<Hello world 'Your Name’.” to the console. EOF ); } Daniel Gomes @danielcsgomes

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Add logic

Slide 45

Slide 45 text

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 $name."); } } Daniel Gomes @danielcsgomes

Slide 46

Slide 46 text

$ php app.php hello:world 'Daniel Gomes' Daniel Gomes @danielcsgomes

Slide 47

Slide 47 text

Call a command from other command

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

$ php app.php examples:calling:command Daniel Gomes @danielcsgomes

Slide 53

Slide 53 text

Always validate the input

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

$ php app.php hello:world 1 Daniel Gomes @danielcsgomes

Slide 56

Slide 56 text

User Interaction •Dialog Helper •Formatter Helper •Progress Helper •Table Helper Daniel Gomes @danielcsgomes

Slide 57

Slide 57 text

Dialog Helper

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

$ php app.php examples:select Daniel Gomes @danielcsgomes

Slide 61

Slide 61 text

$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}"); Daniel Gomes @danielcsgomes

Slide 62

Slide 62 text

$ php app.php examples:dialog Daniel Gomes @danielcsgomes

Slide 63

Slide 63 text

Formatter Helper

Slide 64

Slide 64 text

! $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

Slide 65

Slide 65 text

! $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

Slide 66

Slide 66 text

! $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

Slide 67

Slide 67 text

! $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

Slide 68

Slide 68 text

! $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

Slide 69

Slide 69 text

$ php app.php examples:formatter Daniel Gomes @danielcsgomes

Slide 70

Slide 70 text

Progress Helper

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

1/10 [==>-------------------------] 10% ! 10/10 [============================] 100% $ php app.php examples:progress Daniel Gomes @danielcsgomes

Slide 73

Slide 73 text

Table Helper

Slide 74

Slide 74 text

$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

Slide 75

Slide 75 text

$ php app.php examples:table Daniel Gomes @danielcsgomes

Slide 76

Slide 76 text

Use case Dump Databases

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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('Databases dumped with success.'); }

Slide 82

Slide 82 text

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('Databases dumped with success.'); }

Slide 83

Slide 83 text

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('Databases dumped with success.'); }

Slide 84

Slide 84 text

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('Databases dumped with success.'); }

Slide 85

Slide 85 text

$ php app.php database:dump Daniel Gomes @danielcsgomes

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

! 3. Testing

Slide 88

Slide 88 text

think in your command tests as functional tests

Slide 89

Slide 89 text

CommandTester

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

Test the name output

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

$ ./vendor/bin/phpunit Daniel Gomes @danielcsgomes

Slide 94

Slide 94 text

4. Dependency Injection in your CLI app

Slide 95

Slide 95 text

Container

Slide 96

Slide 96 text

Services CLI Application My Commands Container DBAL Finder setContainer(...) getContainer() Monolog … Daniel Gomes @danielcsgomes

Slide 97

Slide 97 text

ContainerAwareInterface

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

ContainerAwareCommand

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

Example Logging with monolog

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

5. Catching Signals

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

$ ./bin/app examples:catch:signal Daniel Gomes @danielcsgomes

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

http://conference.phplx.net @phplxConf

Slide 111

Slide 111 text

Questions? Daniel Gomes @danielcsgomes

Slide 112

Slide 112 text

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