refactored the implementation of how a console application can handle events

This commit is contained in:
Fabien Potencier 2013-03-24 10:08:12 +01:00
parent 4edf29d04a
commit 4f9a55a03a
14 changed files with 317 additions and 183 deletions

View File

@ -4,7 +4,6 @@ CHANGELOG
2.3.0
-----
* added an init and terminate event dispatched by CLI commands
* added `--clean` option the the `translation:update` command
* added `http_method_override` option

View File

@ -11,7 +11,6 @@
namespace Symfony\Bundle\FrameworkBundle\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@ -24,10 +23,8 @@ use Symfony\Component\Finder\Finder;
* @author Francis Besset <francis.besset@gmail.com>
* @author Fabien Potencier <fabien@symfony.com>
*/
class CacheClearCommand extends Command
class CacheClearCommand extends ContainerAwareCommand
{
private $container;
/**
* {@inheritdoc}
*/
@ -200,16 +197,4 @@ EOF;
return new $class($parent->getEnvironment(), $parent->isDebug());
}
/**
* @return ContainerInterface
*/
protected function getContainer()
{
if (null === $this->container) {
$this->container = $this->getApplication()->getKernel()->getContainer();
}
return $this->container;
}
}

View File

@ -11,12 +11,7 @@
namespace Symfony\Bundle\FrameworkBundle\Command;
use Symfony\Bundle\FrameworkBundle\Console\ConsoleEvents;
use Symfony\Bundle\FrameworkBundle\Event\ConsoleEvent;
use Symfony\Bundle\FrameworkBundle\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
@ -32,27 +27,6 @@ abstract class ContainerAwareCommand extends Command implements ContainerAwareIn
*/
private $container;
/**
* {@inheritdoc}
*/
public function run(InputInterface $input, OutputInterface $output)
{
$dispatcher = $this->getContainer()->get('event_dispatcher');
$helperSet = $this->getHelperSet();
$initEvent = new ConsoleEvent($input, $output);
$initEvent->setHelperSet($helperSet);
$dispatcher->dispatch(ConsoleEvents::INIT, $initEvent);
$exitCode = parent::run($input, $output);
$terminateEvent = new ConsoleTerminateEvent($input, $output, $exitCode);
$terminateEvent->setHelperSet($helperSet);
$dispatcher->dispatch(ConsoleEvents::TERMINATE, $terminateEvent);
return $exitCode;
}
/**
* @return ContainerInterface
*/

View File

@ -1,43 +0,0 @@
<?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\Bundle\FrameworkBundle\Console;
/**
* Contains all events thrown during Console commands execution
*
* @author Francesco Levorato <git@flevour.net>
*/
final class ConsoleEvents
{
/**
* The INIT event allows you to attach listeners before any command is
* executed by the console. It also allows you to modify the input and output
* before they are handled to the command.
*
* The event listener method receives a \Symfony\Bundle\FrameworkBundle\Event\ConsoleEvent
* instance.
*
* @var string
*/
const INIT = 'console.init';
/**
* The TERMINATE event allows you to attach listeners after a command is
* executed by the console.
*
* The event listener method receives a \Symfony\Bundle\FrameworkBundle\Event\ConsoleTerminateEvent
* instance.
*
* @var string
*/
const TERMINATE = 'console.terminate';
}

View File

@ -12,9 +12,7 @@
namespace Symfony\Bundle\FrameworkBundle\Tests\Console;
use Symfony\Bundle\FrameworkBundle\Tests\TestCase;
use Symfony\Bundle\FrameworkBundle\Tests\Console\Fixtures\FooCommand;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Console\ConsoleEvents;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
@ -24,7 +22,7 @@ class ApplicationTest extends TestCase
{
$bundle = $this->getMock("Symfony\Component\HttpKernel\Bundle\BundleInterface");
$kernel = $this->getKernel(array($bundle), $this->never());
$kernel = $this->getKernel(array($bundle));
$application = new Application($kernel);
$application->doRun(new ArrayInput(array('list')), new NullOutput());
@ -35,23 +33,13 @@ class ApplicationTest extends TestCase
$bundle = $this->getMock("Symfony\Component\HttpKernel\Bundle\Bundle");
$bundle->expects($this->once())->method('registerCommands');
$kernel = $this->getKernel(array($bundle), $this->never());
$kernel = $this->getKernel(array($bundle));
$application = new Application($kernel);
$application->doRun(new ArrayInput(array('list')), new NullOutput());
}
public function testCommandDispatchEvents()
{
$kernel = $this->getKernel(array(), $this->once());
$application = new Application($kernel);
$application->add(new FooCommand('foo'));
$application->doRun(new ArrayInput(array('foo')), new NullOutput());
}
private function getKernel(array $bundles, $dispatcherExpected = null)
private function getKernel(array $bundles)
{
$kernel = $this->getMock("Symfony\Component\HttpKernel\KernelInterface");
$kernel
@ -59,42 +47,6 @@ class ApplicationTest extends TestCase
->method('getBundles')
->will($this->returnValue($bundles))
;
$container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface');
$dispatcherExpected = $dispatcherExpected ?: $this->any();
if ($this->never() == $dispatcherExpected) {
$container
->expects($dispatcherExpected)
->method('get');
} else {
$eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$eventDispatcher
->expects($this->at(0))
->method('dispatch')
->with(
$this->equalTo(ConsoleEvents::INIT),
$this->isInstanceOf('Symfony\Bundle\FrameworkBundle\Event\ConsoleEvent')
);
$eventDispatcher
->expects($this->at(1))
->method('dispatch')
->with(
$this->equalTo(ConsoleEvents::TERMINATE),
$this->isInstanceOf('Symfony\Bundle\FrameworkBundle\Event\ConsoleTerminateEvent')
);
$container
->expects($dispatcherExpected)
->method('get')
->with($this->equalTo('event_dispatcher'))
->will($this->returnValue($eventDispatcher));
}
$kernel
->expects($this->any())
->method('getContainer')
->will($this->returnValue($container))
;
return $kernel;
}

View File

@ -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.
*

View File

@ -4,6 +4,7 @@ CHANGELOG
2.3.0
-----
* added support for events in `Application`
* added a way to set the progress bar progress via the `setCurrent` method
2.2.0

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

View File

@ -9,16 +9,17 @@
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\Tests\Console\Fixtures;
namespace Symfony\Component\Console\Event;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class FooCommand extends ContainerAwareCommand
/**
* Allows to do things before the command is executed.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ConsoleCommandEvent extends ConsoleEvent
{
protected function execute(InputInterface $input, OutputInterface $output)
{
return 0;
}
}

View File

@ -9,9 +9,9 @@
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\Event;
namespace Symfony\Component\Console\Event;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\EventDispatcher\Event;
@ -23,42 +23,32 @@ use Symfony\Component\EventDispatcher\Event;
*/
class ConsoleEvent extends Event
{
private $input;
protected $command;
private $input;
private $output;
private $helperSet;
public function __construct(InputInterface $input, OutputInterface $output)
public function __construct(Command $command, InputInterface $input, OutputInterface $output)
{
$this->command = $command;
$this->input = $input;
$this->output = $output;
}
/**
* Sets the helper set.
* Gets the command that is executed.
*
* @param HelperSet $helperSet A HelperSet instance
* @return Command A Command instance
*/
public function setHelperSet(HelperSet $helperSet)
public function getCommand()
{
$this->helperSet = $helperSet;
return $this->command;
}
/**
* Gets the helper set.
* Gets the input instance.
*
* @return HelperSet A HelperSet instance
*/
public function getHelperSet()
{
return $this->helperSet;
}
/**
* Returns the input object
*
* @return InputInterface
* @return InputInterface An InputInterface instance
*/
public function getInput()
{
@ -66,9 +56,9 @@ class ConsoleEvent extends Event
}
/**
* Returns the output object
* Gets the output instance.
*
* @return OutputInterface
* @return OutputInterface An OutputInterface instance
*/
public function getOutput()
{

View 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;
/**
* 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 File

@ -9,13 +9,14 @@
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\Event;
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 receive the exit code of a command after its execution.
* Allows to manipulate the exit code of a command after its execution.
*
* @author Francesco Levorato <git@flevour.net>
*/
@ -28,16 +29,27 @@ class ConsoleTerminateEvent extends ConsoleEvent
*/
private $exitCode;
public function __construct(InputInterface $input, OutputInterface $output, $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)
{
parent::__construct($input, $output);
$this->exitCode = $exitCode;
}
/**
* Returns the exit code.
* Gets the exit code.
*
* @return integer
* @return integer The command exit code
*/
public function getExitCode()
{

View File

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

View File

@ -18,6 +18,9 @@
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"symfony/event-dispatcher": "~2.1"
},
"autoload": {
"psr-0": { "Symfony\\Component\\Console\\": "" }
},