diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 806a06197cafc..9fd21a6d558a6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -66,6 +66,7 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('web.xml'); $loader->load('services.xml'); $loader->load('fragment_renderer.xml'); + $loader->load('console.xml'); // A translator must always be registered (as support is included by // default in the Form component). If disabled, an identity translator 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..62d408977c3b9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/Symfony/Component/Console/EventListener/ExceptionListener.php b/src/Symfony/Component/Console/EventListener/ExceptionListener.php new file mode 100644 index 0000000000000..afc09c356f4ac --- /dev/null +++ b/src/Symfony/Component/Console/EventListener/ExceptionListener.php @@ -0,0 +1,90 @@ + + * + * 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\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleExceptionEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * Console exception listener. + * + * Attempts to log exceptions or abnormal terminations of console commands. + * + * @author James Halsall + */ +class ExceptionListener implements EventSubscriberInterface +{ + /** + * @var LoggerInterface + */ + protected $logger; + + /** + * Constructor. + * + * @param LoggerInterface $logger A logger + */ + public function __construct(LoggerInterface $logger = null) + { + $this->logger = $logger; + } + + /** + * Handles console command exception. + * + * @param ConsoleExceptionEvent $event Console event + */ + public function onKernelException(ConsoleExceptionEvent $event) + { + if (null === $this->logger) { + return; + } + + $exception = $event->getException(); + $input = (string) $event->getInput(); + + $this->logger->error('Exception thrown while running command: "{command}". Message: "{message}"', array('exception' => $exception, 'command' => $input, 'message' => $exception->getMessage())); + } + + /** + * Handles termination of console command. + * + * @param ConsoleTerminateEvent $event Console event + */ + public function onKernelTerminate(ConsoleTerminateEvent $event) + { + if (null === $this->logger) { + return; + } + + $exitCode = $event->getExitCode(); + + if ($exitCode === 0) { + return; + } + + $input = (string) $event->getInput(); + + $this->logger->error('Command "{command}" exited with status code "{code}"', array('command' => (string) $input, 'code' => $exitCode)); + } + + public static function getSubscribedEvents() + { + return array( + ConsoleEvents::EXCEPTION => array('onKernelException', -128), + ConsoleEvents::TERMINATE => array('onKernelTerminate', -128), + ); + } +} 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..76c414e935205 --- /dev/null +++ b/src/Symfony/Component/Console/Tests/EventListener/ExceptionListenerTest.php @@ -0,0 +1,98 @@ + + * + * 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\ArrayInput; +use Symfony\Component\Console\Tests\Output\TestOutput; + +class ExceptionListenerTest extends \PHPUnit_Framework_TestCase +{ + public function testOnKernelException() + { + $logger = $this->getLogger(); + $listener = new ExceptionListener($logger); + + $exception = new \RuntimeException('An error occurred'); + + $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')) + ; + + $input = array( + 'name' => 'test:run', + '--foo' => 'baz', + 'bar' => 'buzz' + ); + + $listener->onKernelException($this->getConsoleExceptionEvent($exception, $input, 1)); + } + + public function testOnKernelTerminateForNonZeroExitCodeWritesToLog() + { + $logger = $this->getLogger(); + $listener = new ExceptionListener($logger); + + $logger + ->expects($this->once()) + ->method('error') + ->with('Command "{command}" exited with status code "{code}"', array('command' => '\'test:run\'', 'code' => 255)) + ; + + $listener->onKernelTerminate($this->getConsoleTerminateEvent(array('name' => 'test:run'), 255)); + } + + public function testOnKernelTerminateForZeroExitCodeDoesNotWriteToLog() + { + $logger = $this->getLogger(); + $listener = new ExceptionListener($logger); + + $logger + ->expects($this->never()) + ->method('error') + ; + + $listener->onKernelTerminate($this->getConsoleTerminateEvent(array('name' => 'test:run'), 0)); + } + + public function testGetSubscribedEvents() + { + $this->assertEquals( + array( + 'console.exception' => array('onKernelException', -128), + 'console.terminate' => array('onKernelTerminate', -128), + ), + ExceptionListener::getSubscribedEvents() + ); + } + + private function getLogger() + { + return $this->getMockForAbstractClass(LoggerInterface::class); + } + + private function getConsoleExceptionEvent(\Exception $exception, $input, $exitCode) + { + return new ConsoleExceptionEvent(new Command('test:run'), new ArrayInput($input), new TestOutput(), $exception, $exitCode); + } + + private function getConsoleTerminateEvent($input, $exitCode) + { + return new ConsoleTerminateEvent(new Command('test:run'), new ArrayInput($input), new TestOutput(), $exitCode); + } +}