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

How to build Console Applications | SymfonyCon 2013

Daniel Gomes
December 13, 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.

Daniel Gomes

December 13, 2013
Tweet

More Decks by Daniel Gomes

Other Decks in Programming

Transcript

  1. 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
  2. <?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
  3. What I needed •Run background jobs •Interactive setup tools •Cache

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

    Bash, PHP, Python, etc • No documentation • Not very friendly for other users Daniel Gomes @danielcsgomes
  5. Why not … • Centralize everything • Good documentation •

    Tests • User friendly Daniel Gomes @danielcsgomes
  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. 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
  8. class MyCommand extends Command { protected function configure(){…} ! protected

    function execute($input, $output){…} ! protected function interact($input, $output){…} } Daniel Gomes @danielcsgomes
  9. 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
  10. 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
  11. 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
  12. Options • unordered • input type: • optional • required

    • array • none (no input) Daniel Gomes @danielcsgomes
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. // 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
  21. 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
  22. $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
  23. ! $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
  24. ! $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
  25. ! $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
  26. ! $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
  27. ! $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
  28. $progress = $this->getHelperSet()->get('progress'); ! $progress->start($output, 50); $i = 0; while

    ($i++ < 50) { sleep(1); $progress->advance(); } ! $progress->finish(); Daniel Gomes @danielcsgomes
  29. $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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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>'); }
  35. 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>'); }
  36. 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>'); }
  37. 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>'); }
  38. 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
  39. 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(){…} }
  40. 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() ); }
  41. namespace Symfony\Component\DependencyInjection; ! interface ContainerAwareInterface { /** * Sets the

    Container. * * @param ContainerInterface|null $container */ public function setContainer(ContainerInterface $container = null); }
  42. 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'); // ... } ! // ... }
  43. 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; } }
  44. 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; }
  45. 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
  46. 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
  47. 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
  48. 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
  49. @danielcsgomes | [email protected] | http://danielcsgomes.com Photo by Jian Awe ©

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