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 events4edf29d
added helperSet to console event objectsf224102
Added events for CLI commands
This commit is contained in:
commit
c1bd3b5b25
@ -27,6 +27,10 @@ use Symfony\Component\Console\Helper\HelperSet;
|
||||
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 @@ class Application
|
||||
}
|
||||
}
|
||||
|
||||
public function setDispatcher(EventDispatcher $dispatcher)
|
||||
{
|
||||
$this->dispatcher = $dispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the current application.
|
||||
*
|
||||
@ -103,7 +113,7 @@ class Application
|
||||
}
|
||||
|
||||
try {
|
||||
$statusCode = $this->doRun($input, $output);
|
||||
$exitCode = $this->doRun($input, $output);
|
||||
} catch (\Exception $e) {
|
||||
if (!$this->catchExceptions) {
|
||||
throw $e;
|
||||
@ -114,21 +124,21 @@ class Application
|
||||
} 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 @@ class Application
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -911,6 +921,45 @@ class Application
|
||||
return array(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
@ -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
|
||||
|
||||
|
55
src/Symfony/Component/Console/ConsoleEvents.php
Normal file
55
src/Symfony/Component/Console/ConsoleEvents.php
Normal file
@ -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';
|
||||
}
|
25
src/Symfony/Component/Console/Event/ConsoleCommandEvent.php
Normal file
25
src/Symfony/Component/Console/Event/ConsoleCommandEvent.php
Normal file
@ -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
|
||||
{
|
||||
}
|
67
src/Symfony/Component/Console/Event/ConsoleEvent.php
Normal file
67
src/Symfony/Component/Console/Event/ConsoleEvent.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -15,12 +15,18 @@ use Symfony\Component\Console\Application;
|
||||
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 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
$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
|
||||
|
@ -18,6 +18,9 @@
|
||||
"require": {
|
||||
"php": ">=5.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/event-dispatcher": "~2.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-0": { "Symfony\\Component\\Console\\": "" }
|
||||
},
|
||||
|
Reference in New Issue
Block a user