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(); + } +}