diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 6355908b88688..28e93d30221e1 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -28,6 +28,7 @@
use Symfony\Component\Finder\Finder;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\Config\FileLocator;
+use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\PropertyAccess\PropertyAccessor;
use Symfony\Component\Serializer\Encoder\YamlEncoder;
use Symfony\Component\Serializer\Encoder\CsvEncoder;
@@ -37,6 +38,7 @@
use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer;
use Symfony\Component\Workflow;
use Symfony\Component\Yaml\Yaml;
+use Symfony\Component\Console\Application;
/**
* FrameworkExtension.
@@ -82,6 +84,11 @@ public function load(array $configs, ContainerBuilder $container)
$loader->load('fragment_renderer.xml');
+ $container->addResource(new ClassExistenceResource(Application::class));
+ if (class_exists(Application::class)) {
+ $loader->load('console.xml');
+ }
+
// Property access is used by both the Form and the Validator component
$loader->load('property_access.xml');
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
new file mode 100644
index 0000000000000..585622bca8379
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index 8739a41da40a0..add66921d391d 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -21,7 +21,7 @@
"symfony/class-loader": "~3.2",
"symfony/dependency-injection": "~3.3",
"symfony/config": "~3.3",
- "symfony/event-dispatcher": "~2.8|~3.0",
+ "symfony/event-dispatcher": "~3.3",
"symfony/http-foundation": "~3.1",
"symfony/http-kernel": "~3.3",
"symfony/polyfill-mbstring": "~1.0",
diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md
index 4b23e6fe86ba6..c24c24c5d1cb5 100644
--- a/src/Symfony/Component/Console/CHANGELOG.md
+++ b/src/Symfony/Component/Console/CHANGELOG.md
@@ -4,6 +4,7 @@ CHANGELOG
3.3.0
-----
+* added `ExceptionListener`
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
3.2.0
diff --git a/src/Symfony/Component/Console/EventListener/ExceptionListener.php b/src/Symfony/Component/Console/EventListener/ExceptionListener.php
new file mode 100644
index 0000000000000..58c57065834a8
--- /dev/null
+++ b/src/Symfony/Component/Console/EventListener/ExceptionListener.php
@@ -0,0 +1,79 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\EventListener;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Event\ConsoleEvent;
+use Symfony\Component\Console\ConsoleEvents;
+use Symfony\Component\Console\Event\ConsoleExceptionEvent;
+use Symfony\Component\Console\Event\ConsoleTerminateEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * @author James Halsall
+ * @author Robin Chalas
+ */
+class ExceptionListener implements EventSubscriberInterface
+{
+ private $logger;
+
+ public function __construct(LoggerInterface $logger = null)
+ {
+ $this->logger = $logger;
+ }
+
+ public function onConsoleException(ConsoleExceptionEvent $event)
+ {
+ if (null === $this->logger) {
+ return;
+ }
+
+ $exception = $event->getException();
+
+ $this->logger->error('Exception thrown while running command "{command}". Message: "{message}"', array('exception' => $exception, 'command' => $this->getInputString($event), 'message' => $exception->getMessage()));
+ }
+
+ public function onConsoleTerminate(ConsoleTerminateEvent $event)
+ {
+ if (null === $this->logger) {
+ return;
+ }
+
+ $exitCode = $event->getExitCode();
+
+ if (0 === $exitCode) {
+ return;
+ }
+
+ $this->logger->error('Command "{command}" exited with code "{code}"', array('command' => $this->getInputString($event), 'code' => $exitCode));
+ }
+
+ public static function getSubscribedEvents()
+ {
+ return array(
+ ConsoleEvents::EXCEPTION => array('onConsoleException', -128),
+ ConsoleEvents::TERMINATE => array('onConsoleTerminate', -128),
+ );
+ }
+
+ private static function getInputString(ConsoleEvent $event)
+ {
+ $commandName = $event->getCommand()->getName();
+ $input = $event->getInput();
+
+ if (method_exists($input, '__toString')) {
+ return str_replace(array("'$commandName'", "\"$commandName\""), $commandName, (string) $input);
+ }
+
+ return $commandName;
+ }
+}
diff --git a/src/Symfony/Component/Console/Tests/EventListener/ExceptionListenerTest.php b/src/Symfony/Component/Console/Tests/EventListener/ExceptionListenerTest.php
new file mode 100644
index 0000000000000..c7e6890b45f4d
--- /dev/null
+++ b/src/Symfony/Component/Console/Tests/EventListener/ExceptionListenerTest.php
@@ -0,0 +1,125 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Tests\EventListener;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Event\ConsoleExceptionEvent;
+use Symfony\Component\Console\Event\ConsoleTerminateEvent;
+use Symfony\Component\Console\EventListener\ExceptionListener;
+use Symfony\Component\Console\Input\ArgvInput;
+use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\StringInput;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ExceptionListenerTest extends \PHPUnit_Framework_TestCase
+{
+ public function testOnConsoleException()
+ {
+ $exception = new \RuntimeException('An error occurred');
+
+ $logger = $this->getLogger();
+ $logger
+ ->expects($this->once())
+ ->method('error')
+ ->with('Exception thrown while running command "{command}". Message: "{message}"', array('exception' => $exception, 'command' => 'test:run --foo=baz buzz', 'message' => 'An error occurred'))
+ ;
+
+ $listener = new ExceptionListener($logger);
+ $listener->onConsoleException($this->getConsoleExceptionEvent($exception, new ArgvInput(array('console.php', 'test:run', '--foo=baz', 'buzz')), 1));
+ }
+
+ public function testOnConsoleTerminateForNonZeroExitCodeWritesToLog()
+ {
+ $logger = $this->getLogger();
+ $logger
+ ->expects($this->once())
+ ->method('error')
+ ->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
+ ;
+
+ $listener = new ExceptionListener($logger);
+ $listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 255));
+ }
+
+ public function testOnConsoleTerminateForZeroExitCodeDoesNotWriteToLog()
+ {
+ $logger = $this->getLogger();
+ $logger
+ ->expects($this->never())
+ ->method('error')
+ ;
+
+ $listener = new ExceptionListener($logger);
+ $listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 0));
+ }
+
+ public function testGetSubscribedEvents()
+ {
+ $this->assertEquals(
+ array(
+ 'console.exception' => array('onConsoleException', -128),
+ 'console.terminate' => array('onConsoleTerminate', -128),
+ ),
+ ExceptionListener::getSubscribedEvents()
+ );
+ }
+
+ public function testAllKindsOfInputCanBeLogged()
+ {
+ $logger = $this->getLogger();
+ $logger
+ ->expects($this->exactly(3))
+ ->method('error')
+ ->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run --foo=bar', 'code' => 255))
+ ;
+
+ $listener = new ExceptionListener($logger);
+ $listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run', '--foo=bar')), 255));
+ $listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArrayInput(array('name' => 'test:run', '--foo' => 'bar')), 255));
+ $listener->onConsoleTerminate($this->getConsoleTerminateEvent(new StringInput('test:run --foo=bar'), 255));
+ }
+
+ public function testCommandNameIsDisplayedForNonStringableInput()
+ {
+ $logger = $this->getLogger();
+ $logger
+ ->expects($this->once())
+ ->method('error')
+ ->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
+ ;
+
+ $listener = new ExceptionListener($logger);
+ $listener->onConsoleTerminate($this->getConsoleTerminateEvent($this->getMockBuilder(InputInterface::class)->getMock(), 255));
+ }
+
+ private function getLogger()
+ {
+ return $this->getMockForAbstractClass(LoggerInterface::class);
+ }
+
+ private function getConsoleExceptionEvent(\Exception $exception, InputInterface $input, $exitCode)
+ {
+ return new ConsoleExceptionEvent(new Command('test:run'), $input, $this->getOutput(), $exception, $exitCode);
+ }
+
+ private function getConsoleTerminateEvent(InputInterface $input, $exitCode)
+ {
+ return new ConsoleTerminateEvent(new Command('test:run'), $input, $this->getOutput(), $exitCode);
+ }
+
+ private function getOutput()
+ {
+ return $this->getMockBuilder(OutputInterface::class)->getMock();
+ }
+}