Browse files

merged branch fabpot/console-dispatcher (PR #7466)

This PR was merged into the master branch.

Discussion
----------

Console dispatcher

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #3889, #6124
| License       | MIT
| Doc PR        | symfony/symfony-docs#2352

refs #1884, #1929

This is an alternative implementation for adding events to console applications.

This implementation has the following features:

* Available for anyone using the Console component and it is not tied to
  FrameworkBundle (this is important as one thing we are trying to solve is
  email sending from a command, and frameworks like Silex using the Console
  component needs a solution too);

* Non-intrusive as the current code has not been changed (except for renaming
  an internal variable that was wrongly named -- so that's not strictly needed
  for this PR)

* The new DispatchableApplication class also works without a dispatcher,
  falling back to the regular behavior. That makes easy to create applications
  that can benefit from a dispatcher when available, but can still work
  otherwise.

* Besides the *before* and *after* events, there is also an *exception* event
  that is dispatched whenever an exception is thrown.

* Each event is quite powerful and can manipulate the input, the output, but
  also the command to be executed.

Commits
-------

4f9a55a refactored the implementation of how a console application can handle events
4edf29d added helperSet to console event objects
f224102 Added events for CLI commands
  • Loading branch information...
2 parents e94346e + 4f9a55a commit c1bd3b5b25a80ebfece8a89d8278035625bec3ea @fabpot fabpot committed Mar 25, 2013
View
67 src/Symfony/Component/Console/Application.php
@@ -27,6 +27,10 @@
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\DialogHelper;
use Symfony\Component\Console\Helper\ProgressHelper;
+use Symfony\Component\Console\Event\ConsoleCommandEvent;
+use Symfony\Component\Console\Event\ConsoleForExceptionEvent;
+use Symfony\Component\Console\Event\ConsoleTerminateEvent;
+use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* An Application is the container for a collection of commands.
@@ -56,6 +60,7 @@ class Application
private $autoExit;
private $definition;
private $helperSet;
+ private $dispatcher;
/**
* Constructor.
@@ -80,6 +85,11 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN')
}
}
+ public function setDispatcher(EventDispatcher $dispatcher)
+ {
+ $this->dispatcher = $dispatcher;
+ }
+
/**
* Runs the current application.
*
@@ -103,7 +113,7 @@ public function run(InputInterface $input = null, OutputInterface $output = null
}
try {
- $statusCode = $this->doRun($input, $output);
+ $exitCode = $this->doRun($input, $output);
} catch (\Exception $e) {
if (!$this->catchExceptions) {
throw $e;
@@ -114,21 +124,21 @@ public function run(InputInterface $input = null, OutputInterface $output = null
} else {
$this->renderException($e, $output);
}
- $statusCode = $e->getCode();
+ $exitCode = $e->getCode();
- $statusCode = is_numeric($statusCode) && $statusCode ? $statusCode : 1;
+ $exitCode = is_numeric($exitCode) && $exitCode ? $exitCode : 1;
}
if ($this->autoExit) {
- if ($statusCode > 255) {
- $statusCode = 255;
+ if ($exitCode > 255) {
+ $exitCode = 255;
}
// @codeCoverageIgnoreStart
- exit($statusCode);
+ exit($exitCode);
// @codeCoverageIgnoreEnd
}
- return $statusCode;
+ return $exitCode;
}
/**
@@ -190,10 +200,10 @@ public function doRun(InputInterface $input, OutputInterface $output)
$command = $this->find($name);
$this->runningCommand = $command;
- $statusCode = $command->run($input, $output);
+ $exitCode = $this->doRunCommand($command, $input, $output);
$this->runningCommand = null;
- return is_numeric($statusCode) ? $statusCode : 0;
+ return is_numeric($exitCode) ? $exitCode : 0;
}
/**
@@ -912,6 +922,45 @@ public function getTerminalDimensions()
}
/**
+ * Runs the current command.
+ *
+ * If an event dispatcher has been attached to the application,
+ * events are also dispatched during the life-cycle of the command.
+ *
+ * @param Command $command A Command instance
+ * @param InputInterface $input An Input instance
+ * @param OutputInterface $output An Output instance
+ *
+ * @return integer 0 if everything went fine, or an error code
+ */
+ protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
+ {
+ if (null === $this->dispatcher) {
+ return $command->run($input, $output);
+ }
+
+ $event = new ConsoleCommandEvent($command, $input, $output);
+ $this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event);
+
+ try {
+ $exitCode = $command->run($input, $output);
+ } catch (\Exception $e) {
+ $event = new ConsoleTerminateEvent($command, $input, $output, $e->getCode());
+ $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
+
+ $event = new ConsoleForExceptionEvent($command, $input, $output, $e, $event->getExitCode());
+ $this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event);
+
+ throw $event->getException();
+ }
+
+ $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
+ $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
+
+ return $event->getExitCode();
+ }
+
+ /**
* Gets the name of the command based on input.
*
* @param InputInterface $input The input interface
View
1 src/Symfony/Component/Console/CHANGELOG.md
@@ -4,6 +4,7 @@ CHANGELOG
2.3.0
-----
+ * added support for events in `Application`
* added a way to normalize EOLs in `ApplicationTester::getDisplay()` and `CommandTester::getDisplay()`
* added a way to set the progress bar progress via the `setCurrent` method
View
55 src/Symfony/Component/Console/ConsoleEvents.php
@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console;
+
+/**
+ * Contains all events dispatched by an Application.
+ *
+ * @author Francesco Levorato <git@flevour.net>
+ */
+final class ConsoleEvents
+{
+ /**
+ * The COMMAND event allows you to attach listeners before any command is
+ * executed by the console. It also allows you to modify the command, input and output
+ * before they are handled to the command.
+ *
+ * The event listener method receives a Symfony\Component\Console\Event\ConsoleCommandEvent
+ * instance.
+ *
+ * @var string
+ */
+ const COMMAND = 'console.command';
+
+ /**
+ * The TERMINATE event allows you to attach listeners after a command is
+ * executed by the console.
+ *
+ * The event listener method receives a Symfony\Component\Console\Event\ConsoleTerminateEvent
+ * instance.
+ *
+ * @var string
+ */
+ const TERMINATE = 'console.terminate';
+
+ /**
+ * The EXCEPTION event occurs when an uncaught exception appears.
+ *
+ * This event allows you to deal with the exception or
+ * to modify the thrown exception. The event listener method receives
+ * a Symfony\Component\Console\Event\ConsoleForExceptionEvent
+ * instance.
+ *
+ * @var string
+ */
+ const EXCEPTION = 'console.exception';
+}
View
25 src/Symfony/Component/Console/Event/ConsoleCommandEvent.php
@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Allows to do things before the command is executed.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ConsoleCommandEvent extends ConsoleEvent
+{
+}
View
67 src/Symfony/Component/Console/Event/ConsoleEvent.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Allows to inspect input and output of a command.
+ *
+ * @author Francesco Levorato <git@flevour.net>
+ */
+class ConsoleEvent extends Event
+{
+ protected $command;
+
+ private $input;
+ private $output;
+
+ public function __construct(Command $command, InputInterface $input, OutputInterface $output)
+ {
+ $this->command = $command;
+ $this->input = $input;
+ $this->output = $output;
+ }
+
+ /**
+ * Gets the command that is executed.
+ *
+ * @return Command A Command instance
+ */
+ public function getCommand()
+ {
+ return $this->command;
+ }
+
+ /**
+ * Gets the input instance.
+ *
+ * @return InputInterface An InputInterface instance
+ */
+ public function getInput()
+ {
+ return $this->input;
+ }
+
+ /**
+ * Gets the output instance.
+ *
+ * @return OutputInterface An OutputInterface instance
+ */
+ public function getOutput()
+ {
+ return $this->output;
+ }
+}
View
67 src/Symfony/Component/Console/Event/ConsoleForExceptionEvent.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Allows to handle exception thrown in a command.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ConsoleForExceptionEvent extends ConsoleEvent
+{
+ private $exception;
+ private $exitCode;
+
+ public function __construct(Command $command, InputInterface $input, OutputInterface $output, \Exception $exception, $exitCode)
+ {
+ parent::__construct($command, $input, $output);
+
+ $this->setException($exception);
+ $this->exitCode = $exitCode;
+ }
+
+ /**
+ * Returns the thrown exception.
+ *
+ * @return \Exception The thrown exception
+ */
+ public function getException()
+ {
+ return $this->exception;
+ }
+
+ /**
+ * Replaces the thrown exception.
+ *
+ * This exception will be thrown if no response is set in the event.
+ *
+ * @param \Exception $exception The thrown exception
+ */
+ public function setException(\Exception $exception)
+ {
+ $this->exception = $exception;
+ }
+
+ /**
+ * Gets the exit code.
+ *
+ * @return integer The command exit code
+ */
+ public function getExitCode()
+ {
+ return $this->exitCode;
+ }
+}
View
58 src/Symfony/Component/Console/Event/ConsoleTerminateEvent.php
@@ -0,0 +1,58 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Allows to manipulate the exit code of a command after its execution.
+ *
+ * @author Francesco Levorato <git@flevour.net>
+ */
+class ConsoleTerminateEvent extends ConsoleEvent
+{
+ /**
+ * The exit code of the command.
+ *
+ * @var integer
+ */
+ private $exitCode;
+
+ public function __construct(Command $command, InputInterface $input, OutputInterface $output, $exitCode)
+ {
+ parent::__construct($command, $input, $output);
+
+ $this->setExitCode($exitCode);
+ }
+
+ /**
+ * Sets the exit code.
+ *
+ * @param integer $exitCode The command exit code
+ */
+ public function setExitCode($exitCode)
+ {
+ $this->exitCode = $exitCode;
+ }
+
+ /**
+ * Gets the exit code.
+ *
+ * @return integer The command exit code
+ */
+ public function getExitCode()
+ {
+ return $this->exitCode;
+ }
+}
View
89 src/Symfony/Component/Console/Tests/ApplicationTest.php
@@ -15,12 +15,18 @@
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\Output;
+use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\ApplicationTester;
+use Symfony\Component\Console\Event\ConsoleCommandEvent;
+use Symfony\Component\Console\Event\ConsoleForExceptionEvent;
+use Symfony\Component\Console\Event\ConsoleTerminateEvent;
+use Symfony\Component\EventDispatcher\EventDispatcher;
class ApplicationTest extends \PHPUnit_Framework_TestCase
{
@@ -634,6 +640,89 @@ public function testSettingCustomInputDefinitionOverwritesDefaultValues()
$this->assertTrue($inputDefinition->hasOption('custom'));
}
+
+ public function testRunWithDispatcher()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->setDispatcher($this->getDispatcher());
+
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $output->write('foo.');
+ });
+
+ $tester = new ApplicationTester($application);
+ $tester->run(array('command' => 'foo'));
+ $this->assertEquals('before.foo.after.', $tester->getDisplay());
+ }
+
+ /**
+ * @expectedException \LogicException
+ * @expectedExceptionMessage caught
+ */
+ public function testRunWithExceptionAndDispatcher()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $application = new Application();
+ $application->setDispatcher($this->getDispatcher());
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ throw new \RuntimeException('foo');
+ });
+
+ $tester = new ApplicationTester($application);
+ $tester->run(array('command' => 'foo'));
+ }
+
+ public function testRunDispatchesAllEventsWithException()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $application = new Application();
+ $application->setDispatcher($this->getDispatcher());
+ $application->setAutoExit(false);
+
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $output->write('foo.');
+
+ throw new \RuntimeException('foo');
+ });
+
+ $tester = new ApplicationTester($application);
+ $tester->run(array('command' => 'foo'));
+ $this->assertContains('before.foo.after.caught.', $tester->getDisplay());
+ }
+
+ protected function getDispatcher()
+ {
+ $dispatcher = new EventDispatcher;
+ $dispatcher->addListener('console.command', function (ConsoleCommandEvent $event) {
+ $event->getOutput()->write('before.');
+ });
+ $dispatcher->addListener('console.terminate', function (ConsoleTerminateEvent $event) {
+ $event->getOutput()->write('after.');
+
+ $event->setExitCode(128);
+ });
+ $dispatcher->addListener('console.exception', function (ConsoleForExceptionEvent $event) {
+ $event->getOutput()->writeln('caught.');
+
+ $event->setException(new \LogicException('caught.', $event->getExitCode(), $event->getException()));
+ });
+
+ return $dispatcher;
+ }
}
class CustomApplication extends Application
View
3 src/Symfony/Component/Console/composer.json
@@ -18,6 +18,9 @@
"require": {
"php": ">=5.3.3"
},
+ "require-dev": {
+ "symfony/event-dispatcher": "~2.1"
+ },
"autoload": {
"psr-0": { "Symfony\\Component\\Console\\": "" }
},

0 comments on commit c1bd3b5

Please sign in to comment.