Skip to content

Commit

Permalink
Add Console ExceptionListener
Browse files Browse the repository at this point in the history
Handle non string-castable inputs

Cleanup input for display

Naming changes

InputInterface doesnt have a toString()

Logger must be private

Remove useless doc blocks

Tweak tests
  • Loading branch information
chalasr committed Jan 23, 2017
1 parent 42a345c commit cf6baf6
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 62 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
3.3.0
-----

* added `ExceptionListener`
* added `AddConsoleCommandPass` (originally in FrameworkBundle)

3.2.0
Expand Down
57 changes: 23 additions & 34 deletions EventListener/ExceptionListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,79 +12,68 @@
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;

/**
* Console exception listener.
*
* Attempts to log exceptions or abnormal terminations of console commands.
*
* @author James Halsall <james.t.halsall@googlemail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ExceptionListener implements EventSubscriberInterface
{
/**
* @var LoggerInterface
*/
protected $logger;

/**
* Constructor.
*
* @param LoggerInterface $logger A logger
*/
private $logger;

public function __construct(LoggerInterface $logger = null)
{
$this->logger = $logger;
}

/**
* Handles console command exception.
*
* @param ConsoleExceptionEvent $event Console event
*/
public function onKernelException(ConsoleExceptionEvent $event)
public function onConsoleException(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()));
$this->logger->error('Exception thrown while running command "{command}". Message: "{message}"', array('exception' => $exception, 'command' => $this->getInputString($event), 'message' => $exception->getMessage()));
}

/**
* Handles termination of console command.
*
* @param ConsoleTerminateEvent $event Console event
*/
public function onKernelTerminate(ConsoleTerminateEvent $event)
public function onConsoleTerminate(ConsoleTerminateEvent $event)
{
if (null === $this->logger) {
return;
}

$exitCode = $event->getExitCode();

if ($exitCode === 0) {
if (0 === $exitCode) {
return;
}

$input = (string) $event->getInput();

$this->logger->error('Command "{command}" exited with status code "{code}"', array('command' => (string) $input, 'code' => $exitCode));
$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('onKernelException', -128),
ConsoleEvents::TERMINATE => array('onKernelTerminate', -128),
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;
}
}
83 changes: 55 additions & 28 deletions Tests/EventListener/ExceptionListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,83 +16,110 @@
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\Tests\Output\TestOutput;
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 testOnKernelException()
public function testOnConsoleException()
{
$logger = $this->getLogger();
$listener = new ExceptionListener($logger);

$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'))
->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));
$listener = new ExceptionListener($logger);
$listener->onConsoleException($this->getConsoleExceptionEvent($exception, new ArgvInput(array('console.php', 'test:run', '--foo=baz', 'buzz')), 1));
}

public function testOnKernelTerminateForNonZeroExitCodeWritesToLog()
public function testOnConsoleTerminateForNonZeroExitCodeWritesToLog()
{
$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))
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
;

$listener->onKernelTerminate($this->getConsoleTerminateEvent(array('name' => 'test:run'), 255));
$listener = new ExceptionListener($logger);
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 255));
}

public function testOnKernelTerminateForZeroExitCodeDoesNotWriteToLog()
public function testOnConsoleTerminateForZeroExitCodeDoesNotWriteToLog()
{
$logger = $this->getLogger();
$listener = new ExceptionListener($logger);

$logger
->expects($this->never())
->method('error')
;

$listener->onKernelTerminate($this->getConsoleTerminateEvent(array('name' => 'test:run'), 0));
$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('onKernelException', -128),
'console.terminate' => array('onKernelTerminate', -128),
'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, $input, $exitCode)
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 ConsoleExceptionEvent(new Command('test:run'), new ArrayInput($input), new TestOutput(), $exception, $exitCode);
return new ConsoleTerminateEvent(new Command('test:run'), $input, $this->getOutput(), $exitCode);
}

private function getConsoleTerminateEvent($input, $exitCode)
private function getOutput()
{
return new ConsoleTerminateEvent(new Command('test:run'), new ArrayInput($input), new TestOutput(), $exitCode);
return $this->getMockBuilder(OutputInterface::class)->getMock();
}
}

0 comments on commit cf6baf6

Please sign in to comment.