Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 7

Slide 7 text

$ php app.php

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

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

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.3.*@dev" } } 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();

Slide 19

Slide 19 text

$ php app.php

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

! 2. Create commands

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Bootstrap the command

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

$ php app.php

Slide 29

Slide 29 text

Arguments and options

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

! usage manual

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

$ php app.php hello:world —help

Slide 36

Slide 36 text

Add business logic

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

Always validate the input

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

$ php app.php hello:world 1

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Dialog Helper

Slide 44

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

Slide 45

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

Slide 46

Slide 46 text

$ php app.php examples:select

Slide 47

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

Slide 48

Slide 48 text

$ php app.php examples:dialog

Slide 49

Slide 49 text

Formatter Helper

Slide 50

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

Slide 51

Slide 51 text

$ php app.php examples:formatter

Slide 52

Slide 52 text

Progress Helper

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

Table Helper

Slide 56

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

Slide 57

Slide 57 text

$ php app.php examples:table

Slide 58

Slide 58 text

Use case Dump Databases

Slide 59

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

$ php app.php database:dump

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

! 3. Testing

Slide 64

Slide 64 text

think in your command tests as functional tests

Slide 65

Slide 65 text

CommandTester

Slide 66

Slide 66 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 67

Slide 67 text

Test the name output

Slide 68

Slide 68 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 69

Slide 69 text

$ ./vendor/bin/phpunit

Slide 70

Slide 70 text

4. Dependency Injection in your CLI app

Slide 71

Slide 71 text

Container

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

ContainerAwareInterface

Slide 74

Slide 74 text

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

Slide 75

Slide 75 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 76

Slide 76 text

Example Logging

Slide 77

Slide 77 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 78

Slide 78 text

5. Catching Signals

Slide 79

Slide 79 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

Slide 80

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

Slide 81

Slide 81 text

$ ./bin/app examples:catch:signal

Slide 82

Slide 82 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/ZendCon-console-applications-made-easy https://github.com/symfony/Console Catching Signals http://signalingphp.com https://github.com/Cilex/Cilex Cilex

Slide 83

Slide 83 text

http://conference.phplx.net @phplxConf

Slide 84

Slide 84 text

@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?