From f224102c72f8153822bc54cc92685d30faade0da Mon Sep 17 00:00:00 2001 From: Francesco Levorato Date: Wed, 11 Apr 2012 23:35:46 +0200 Subject: [PATCH 1/3] Added events for CLI commands This adds an init and terminate event for commands. They are dispatched from ContainerAwareCommand. The cache:clear command can't implement this (cf. #3889 on Github). --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../Command/CacheClearCommand.php | 17 +++++- .../Command/ContainerAwareCommand.php | 23 ++++++++ .../FrameworkBundle/Console/ConsoleEvents.php | 43 +++++++++++++++ .../FrameworkBundle/Event/ConsoleEvent.php | 54 +++++++++++++++++++ .../Event/ConsoleTerminateEvent.php | 46 ++++++++++++++++ .../Tests/Console/ApplicationTest.php | 53 ++++++++++++++++-- .../Tests/Console/Fixtures/FooCommand.php | 24 +++++++++ .../Tests/Console/Fixtures/SilentCommand.php | 25 +++++++++ 9 files changed, 282 insertions(+), 4 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/Console/ConsoleEvents.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Event/ConsoleTerminateEvent.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/FooCommand.php create mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/SilentCommand.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index df2e1ce1529aa..08abac098bb6b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,6 +4,7 @@ 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 diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index e2bade38a7dcd..d2c08175a13a2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -11,6 +11,7 @@ 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; @@ -23,8 +24,10 @@ * @author Francis Besset * @author Fabien Potencier */ -class CacheClearCommand extends ContainerAwareCommand +class CacheClearCommand extends Command { + private $container; + /** * {@inheritdoc} */ @@ -197,4 +200,16 @@ public function getRootDir() 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; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php index 035f5536ee8dc..13a14f381007a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php @@ -11,7 +11,12 @@ 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; @@ -27,6 +32,24 @@ abstract class ContainerAwareCommand extends Command implements ContainerAwareIn */ private $container; + /** + * {@inheritdoc} + */ + public function run(InputInterface $input, OutputInterface $output) + { + $dispatcher = $this->getContainer()->get('event_dispatcher'); + + $initEvent = new ConsoleEvent($input, $output); + $dispatcher->dispatch(ConsoleEvents::INIT, $initEvent); + + $exitCode = parent::run($input, $output); + + $terminateEvent = new ConsoleTerminateEvent($input, $output, $exitCode); + $dispatcher->dispatch(ConsoleEvents::TERMINATE, $terminateEvent); + + return $exitCode; + } + /** * @return ContainerInterface */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/ConsoleEvents.php b/src/Symfony/Bundle/FrameworkBundle/Console/ConsoleEvents.php new file mode 100644 index 0000000000000..b8fee5fcee9f0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Console/ConsoleEvents.php @@ -0,0 +1,43 @@ + + * + * 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 + */ +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'; +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php b/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php new file mode 100644 index 0000000000000..23b6b42f882c3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Event; + +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 + */ +class ConsoleEvent extends Event +{ + private $input; + + private $output; + + public function __construct(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + } + + /** + * Returns the input object + * + * @return InputInterface + */ + public function getInput() + { + return $this->input; + } + + /** + * Returns the output object + * + * @return OutputInterface + */ + public function getOutput() + { + return $this->output; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleTerminateEvent.php b/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleTerminateEvent.php new file mode 100644 index 0000000000000..8ade530a85dc5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleTerminateEvent.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Event; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Allows to receive the exit code of a command after its execution. + * + * @author Francesco Levorato + */ +class ConsoleTerminateEvent extends ConsoleEvent +{ + /** + * The exit code of the command. + * + * @var integer + */ + private $exitCode; + + public function __construct(InputInterface $input, OutputInterface $output, $exitCode) + { + parent::__construct($input, $output); + $this->exitCode = $exitCode; + } + + /** + * Returns the exit code. + * + * @return integer + */ + public function getExitCode() + { + return $this->exitCode; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index 1427453b10650..b870e1d728584 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -12,6 +12,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Console; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Bundle\FrameworkBundle\Tests\Console\Fixtures\FooCommand; +use Symfony\Bundle\FrameworkBundle\Tests\Console\Fixtures\SilentCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; @@ -22,7 +24,7 @@ public function testBundleInterfaceImplementation() { $bundle = $this->getMock("Symfony\Component\HttpKernel\Bundle\BundleInterface"); - $kernel = $this->getKernel(array($bundle)); + $kernel = $this->getKernel(array($bundle), $this->never()); $application = new Application($kernel); $application->doRun(new ArrayInput(array('list')), new NullOutput()); @@ -33,13 +35,33 @@ public function testBundleCommandsAreRegistered() $bundle = $this->getMock("Symfony\Component\HttpKernel\Bundle\Bundle"); $bundle->expects($this->once())->method('registerCommands'); - $kernel = $this->getKernel(array($bundle)); + $kernel = $this->getKernel(array($bundle), $this->never()); $application = new Application($kernel); $application->doRun(new ArrayInput(array('list')), new NullOutput()); } - private function getKernel(array $bundles) + 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()); + } + + public function testSilentCommand() + { + $kernel = $this->getKernel(array(), $this->never()); + + $application = new Application($kernel); + $application->add(new SilentCommand('chut')); + + $application->doRun(new ArrayInput(array('chut')), new NullOutput()); + } + + private function getKernel(array $bundles, $dispatcherExpected = null) { $kernel = $this->getMock("Symfony\Component\HttpKernel\KernelInterface"); $kernel @@ -47,6 +69,31 @@ private function getKernel(array $bundles) ->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->atLeastOnce()) + ->method('dispatch'); + $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; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/FooCommand.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/FooCommand.php new file mode 100644 index 0000000000000..513f60198f78b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/FooCommand.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Console\Fixtures; + +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class FooCommand extends ContainerAwareCommand +{ + protected function execute(InputInterface $input, OutputInterface $output) + { + return 0; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/SilentCommand.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/SilentCommand.php new file mode 100644 index 0000000000000..1e3621b437c73 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/SilentCommand.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Console\Fixtures; + + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class SilentCommand extends Command +{ + protected function execute(InputInterface $input, OutputInterface $output) + { + return 0; + } +} From 4edf29d04a06e08dcc174c7c1ebaf36c847cea4e Mon Sep 17 00:00:00 2001 From: Bilal Amarni Date: Wed, 6 Feb 2013 11:19:01 +0100 Subject: [PATCH 2/3] added helperSet to console event objects --- .../Command/ContainerAwareCommand.php | 3 +++ .../FrameworkBundle/Event/ConsoleEvent.php | 23 ++++++++++++++++ .../Tests/Console/ApplicationTest.php | 27 ++++++++++--------- .../Tests/Console/Fixtures/SilentCommand.php | 25 ----------------- 4 files changed, 40 insertions(+), 38 deletions(-) delete mode 100644 src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/SilentCommand.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php index 13a14f381007a..b909e7e1a2273 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php @@ -38,13 +38,16 @@ abstract class ContainerAwareCommand extends Command implements ContainerAwareIn 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; diff --git a/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php b/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php index 23b6b42f882c3..76a72d1e05d7e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php +++ b/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Event; +use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\Event; @@ -26,12 +27,34 @@ class ConsoleEvent extends Event private $output; + private $helperSet; + public function __construct(InputInterface $input, OutputInterface $output) { $this->input = $input; $this->output = $output; } + /** + * Sets the helper set. + * + * @param HelperSet $helperSet A HelperSet instance + */ + public function setHelperSet(HelperSet $helperSet) + { + $this->helperSet = $helperSet; + } + + /** + * Gets the helper set. + * + * @return HelperSet A HelperSet instance + */ + public function getHelperSet() + { + return $this->helperSet; + } + /** * Returns the input object * diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index b870e1d728584..8c83891ec0496 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -13,8 +13,8 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FrameworkBundle\Tests\Console\Fixtures\FooCommand; -use Symfony\Bundle\FrameworkBundle\Tests\Console\Fixtures\SilentCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\FrameworkBundle\Console\ConsoleEvents; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; @@ -51,16 +51,6 @@ public function testCommandDispatchEvents() $application->doRun(new ArrayInput(array('foo')), new NullOutput()); } - public function testSilentCommand() - { - $kernel = $this->getKernel(array(), $this->never()); - - $application = new Application($kernel); - $application->add(new SilentCommand('chut')); - - $application->doRun(new ArrayInput(array('chut')), new NullOutput()); - } - private function getKernel(array $bundles, $dispatcherExpected = null) { $kernel = $this->getMock("Symfony\Component\HttpKernel\KernelInterface"); @@ -80,8 +70,19 @@ private function getKernel(array $bundles, $dispatcherExpected = null) } else { $eventDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); $eventDispatcher - ->expects($this->atLeastOnce()) - ->method('dispatch'); + ->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') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/SilentCommand.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/SilentCommand.php deleted file mode 100644 index 1e3621b437c73..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/SilentCommand.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bundle\FrameworkBundle\Tests\Console\Fixtures; - - -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -class SilentCommand extends Command -{ - protected function execute(InputInterface $input, OutputInterface $output) - { - return 0; - } -} From 4f9a55a03a1442487a26dbcb84c3dcfbffef8141 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sun, 24 Mar 2013 10:08:12 +0100 Subject: [PATCH 3/3] refactored the implementation of how a console application can handle events --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 - .../Command/CacheClearCommand.php | 17 +--- .../Command/ContainerAwareCommand.php | 26 ------ .../FrameworkBundle/Console/ConsoleEvents.php | 43 --------- .../Tests/Console/ApplicationTest.php | 54 +---------- src/Symfony/Component/Console/Application.php | 67 ++++++++++++-- src/Symfony/Component/Console/CHANGELOG.md | 1 + .../Component/Console/ConsoleEvents.php | 55 ++++++++++++ .../Console/Event/ConsoleCommandEvent.php} | 15 ++-- .../Console}/Event/ConsoleEvent.php | 38 +++----- .../Event/ConsoleForExceptionEvent.php | 67 ++++++++++++++ .../Console}/Event/ConsoleTerminateEvent.php | 24 +++-- .../Console/Tests/ApplicationTest.php | 89 +++++++++++++++++++ src/Symfony/Component/Console/composer.json | 3 + 14 files changed, 317 insertions(+), 183 deletions(-) delete mode 100644 src/Symfony/Bundle/FrameworkBundle/Console/ConsoleEvents.php create mode 100644 src/Symfony/Component/Console/ConsoleEvents.php rename src/Symfony/{Bundle/FrameworkBundle/Tests/Console/Fixtures/FooCommand.php => Component/Console/Event/ConsoleCommandEvent.php} (54%) rename src/Symfony/{Bundle/FrameworkBundle => Component/Console}/Event/ConsoleEvent.php (56%) create mode 100644 src/Symfony/Component/Console/Event/ConsoleForExceptionEvent.php rename src/Symfony/{Bundle/FrameworkBundle => Component/Console}/Event/ConsoleTerminateEvent.php (54%) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 08abac098bb6b..df2e1ce1529aa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -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 diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index d2c08175a13a2..e2bade38a7dcd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -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 @@ * @author Francis Besset * @author Fabien Potencier */ -class CacheClearCommand extends Command +class CacheClearCommand extends ContainerAwareCommand { - private $container; - /** * {@inheritdoc} */ @@ -200,16 +197,4 @@ public function getRootDir() 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; - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php index b909e7e1a2273..035f5536ee8dc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php @@ -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 */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/ConsoleEvents.php b/src/Symfony/Bundle/FrameworkBundle/Console/ConsoleEvents.php deleted file mode 100644 index b8fee5fcee9f0..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Console/ConsoleEvents.php +++ /dev/null @@ -1,43 +0,0 @@ - - * - * 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 - */ -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'; -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index 8c83891ec0496..1427453b10650 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -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 @@ public function testBundleInterfaceImplementation() { $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 @@ public function testBundleCommandsAreRegistered() $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 @@ private function getKernel(array $bundles, $dispatcherExpected = null) ->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; } diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php index 34a1f91238cd1..f071bdab6ae90 100644 --- a/src/Symfony/Component/Console/Application.php +++ b/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; } /** @@ -911,6 +921,45 @@ public function getTerminalDimensions() 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. * diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index f0d67145cc762..f326f420664d5 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -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 diff --git a/src/Symfony/Component/Console/ConsoleEvents.php b/src/Symfony/Component/Console/ConsoleEvents.php new file mode 100644 index 0000000000000..23320e13b5cdd --- /dev/null +++ b/src/Symfony/Component/Console/ConsoleEvents.php @@ -0,0 +1,55 @@ + + * + * 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 + */ +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'; +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/FooCommand.php b/src/Symfony/Component/Console/Event/ConsoleCommandEvent.php similarity index 54% rename from src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/FooCommand.php rename to src/Symfony/Component/Console/Event/ConsoleCommandEvent.php index 513f60198f78b..c754299ae08fb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Fixtures/FooCommand.php +++ b/src/Symfony/Component/Console/Event/ConsoleCommandEvent.php @@ -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 + */ +class ConsoleCommandEvent extends ConsoleEvent { - protected function execute(InputInterface $input, OutputInterface $output) - { - return 0; - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php b/src/Symfony/Component/Console/Event/ConsoleEvent.php similarity index 56% rename from src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php rename to src/Symfony/Component/Console/Event/ConsoleEvent.php index 76a72d1e05d7e..ab620c4609a20 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleEvent.php +++ b/src/Symfony/Component/Console/Event/ConsoleEvent.php @@ -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 @@ */ 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. - * - * @param HelperSet $helperSet A HelperSet instance - */ - public function setHelperSet(HelperSet $helperSet) - { - $this->helperSet = $helperSet; - } - - /** - * Gets the helper set. + * Gets the command that is executed. * - * @return HelperSet A HelperSet instance + * @return Command A Command instance */ - public function getHelperSet() + public function getCommand() { - return $this->helperSet; + return $this->command; } /** - * Returns the input object + * Gets the input instance. * - * @return InputInterface + * @return InputInterface An InputInterface instance */ public function getInput() { @@ -66,9 +56,9 @@ public function getInput() } /** - * Returns the output object + * Gets the output instance. * - * @return OutputInterface + * @return OutputInterface An OutputInterface instance */ public function getOutput() { diff --git a/src/Symfony/Component/Console/Event/ConsoleForExceptionEvent.php b/src/Symfony/Component/Console/Event/ConsoleForExceptionEvent.php new file mode 100644 index 0000000000000..b67f99e7de14b --- /dev/null +++ b/src/Symfony/Component/Console/Event/ConsoleForExceptionEvent.php @@ -0,0 +1,67 @@ + + * + * 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 + */ +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; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleTerminateEvent.php b/src/Symfony/Component/Console/Event/ConsoleTerminateEvent.php similarity index 54% rename from src/Symfony/Bundle/FrameworkBundle/Event/ConsoleTerminateEvent.php rename to src/Symfony/Component/Console/Event/ConsoleTerminateEvent.php index 8ade530a85dc5..9f80cb70a0dbf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Event/ConsoleTerminateEvent.php +++ b/src/Symfony/Component/Console/Event/ConsoleTerminateEvent.php @@ -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 */ @@ -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() { diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index 23edd490b3658..92bc03bc4be3b 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/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 diff --git a/src/Symfony/Component/Console/composer.json b/src/Symfony/Component/Console/composer.json index a3c71521bb42b..c9de125d31d57 100644 --- a/src/Symfony/Component/Console/composer.json +++ b/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\\": "" } },