[Console] Review console.ERROR related behavior

This commit is contained in:
Nicolas Grekas 2017-04-13 20:34:34 +02:00
parent b9ee33fd03
commit a7c67c9ab2
9 changed files with 115 additions and 150 deletions

View File

@ -6,7 +6,7 @@
<services>
<service id="console.exception_listener" class="Symfony\Component\Console\EventListener\ExceptionListener" public="false">
<service id="console.error_listener" class="Symfony\Component\Console\EventListener\ErrorListener" public="false">
<argument type="service" id="logger" on-invalid="null" />
<tag name="kernel.event_subscriber" />
<tag name="monolog.logger" channel="console" />

View File

@ -119,46 +119,30 @@ class Application
$output = new ConsoleOutput();
}
if (null !== $this->dispatcher && $this->dispatcher->hasListeners(ConsoleEvents::EXCEPTION)) {
@trigger_error(sprintf('The "ConsoleEvents::EXCEPTION" event is deprecated since Symfony 3.3 and will be removed in 4.0. Listen to the "ConsoleEvents::ERROR" event instead.'), E_USER_DEPRECATED);
}
$this->configureIO($input, $output);
try {
$e = null;
$exitCode = $this->doRun($input, $output);
} catch (\Exception $e) {
$exception = $e;
} catch (\Throwable $e) {
$exception = new FatalThrowableError($e);
}
if (null !== $e && null !== $this->dispatcher) {
$event = new ConsoleErrorEvent($input, $output, $e, $e->getCode(), $this->runningCommand);
$this->dispatcher->dispatch(ConsoleEvents::ERROR, $event);
$e = $event->getError();
if ($event->isErrorHandled()) {
$e = null;
$exitCode = 0;
} else {
if (!$e instanceof \Exception) {
throw $e;
}
$exitCode = $e->getCode();
}
$event = new ConsoleTerminateEvent($this->runningCommand, $input, $output, $exitCode);
$this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
} catch (\Exception $x) {
$e = $x;
} catch (\Throwable $x) {
$e = new FatalThrowableError($x);
}
if (null !== $e) {
if (!$this->catchExceptions) {
throw $e;
throw $x;
}
if ($output instanceof ConsoleOutputInterface) {
$this->renderException($exception, $output->getErrorOutput());
$this->renderException($e, $output->getErrorOutput());
} else {
$this->renderException($exception, $output);
$this->renderException($e, $output);
}
$exitCode = $e->getCode();
@ -214,8 +198,26 @@ class Application
$input = new ArrayInput(array('command' => $this->defaultCommand));
}
// the command name MUST be the first element of the input
$command = $this->find($name);
try {
$e = $this->runningCommand = null;
// the command name MUST be the first element of the input
$command = $this->find($name);
} catch (\Exception $e) {
} catch (\Throwable $e) {
}
if (null !== $e) {
if (null !== $this->dispatcher) {
$event = new ConsoleErrorEvent($input, $output, $e);
$this->dispatcher->dispatch(ConsoleEvents::ERROR, $event);
$e = $event->getError();
if (0 === $event->getExitCode()) {
return 0;
}
}
throw $e;
}
$this->runningCommand = $command;
$exitCode = $this->doRunCommand($command, $input, $output);
@ -864,13 +866,7 @@ class Application
}
if (null === $this->dispatcher) {
try {
return $command->run($input, $output);
} catch (\Exception $e) {
throw $e;
} catch (\Throwable $e) {
throw new FatalThrowableError($e);
}
return $command->run($input, $output);
}
// bind before the console.command event, so the listeners have access to input options/arguments
@ -882,37 +878,45 @@ class Application
}
$event = new ConsoleCommandEvent($command, $input, $output);
$this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event);
if ($event->commandShouldRun()) {
try {
$e = null;
try {
$e = null;
$this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event);
if ($event->commandShouldRun()) {
$exitCode = $command->run($input, $output);
} catch (\Exception $x) {
$e = $x;
} catch (\Throwable $x) {
$e = new FatalThrowableError($x);
} else {
$exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
}
if (null !== $e) {
$event = new ConsoleExceptionEvent($command, $input, $output, $e, $e->getCode(), false);
} catch (\Exception $e) {
} catch (\Throwable $e) {
}
if (null !== $e) {
if ($this->dispatcher->hasListeners(ConsoleEvents::EXCEPTION)) {
$x = $e instanceof \Exception ? $e : new FatalThrowableError($e);
$event = new ConsoleExceptionEvent($command, $input, $output, $x, $x->getCode());
$this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event);
if ($e !== $event->getException()) {
@trigger_error('The "console.exception" event is deprecated since version 3.3 and will be removed in 4.0. Use the "console.error" event instead.', E_USER_DEPRECATED);
$x = $e = $event->getException();
if ($x !== $event->getException()) {
$e = $event->getException();
}
throw $x;
}
} else {
$exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED;
$event = new ConsoleErrorEvent($input, $output, $e, $command);
$this->dispatcher->dispatch(ConsoleEvents::ERROR, $event);
$e = $event->getError();
if (0 === $exitCode = $event->getExitCode()) {
$e = null;
}
}
$event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
$this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
if (null !== $e) {
throw $e;
}
return $event->getExitCode();
}

View File

@ -55,8 +55,7 @@ final class ConsoleEvents
const EXCEPTION = 'console.exception';
/**
* The ERROR event occurs when an uncaught exception appears or
* a throwable error.
* The ERROR event occurs when an uncaught exception or error appears.
*
* This event allows you to deal with the exception/error or
* to modify the thrown exception.

View File

@ -15,31 +15,22 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Debug\Exception\FatalThrowableError;
/**
* Allows to handle throwables thrown while running a command.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class ConsoleErrorEvent extends ConsoleExceptionEvent
final class ConsoleErrorEvent extends ConsoleEvent
{
private $error;
private $handled = false;
private $exitCode;
public function __construct(InputInterface $input, OutputInterface $output, $error, $exitCode, Command $command = null)
public function __construct(InputInterface $input, OutputInterface $output, $error, Command $command = null)
{
if (!$error instanceof \Throwable && !$error instanceof \Exception) {
throw new InvalidArgumentException(sprintf('The error passed to ConsoleErrorEvent must be an instance of \Throwable or \Exception, "%s" was passed instead.', is_object($error) ? get_class($error) : gettype($error)));
}
parent::__construct($command, $input, $output);
$exception = $error;
if (!$error instanceof \Exception) {
$exception = new FatalThrowableError($error);
}
parent::__construct($command, $input, $output, $exception, $exitCode, false);
$this->error = $error;
$this->setError($error);
}
/**
@ -67,46 +58,26 @@ class ConsoleErrorEvent extends ConsoleExceptionEvent
}
/**
* Marks the error/exception as handled.
* Sets the exit code.
*
* If it is not marked as handled, the error/exception will be displayed in
* the command output.
* @param int $exitCode The command exit code
*/
public function markErrorAsHandled()
public function setExitCode($exitCode)
{
$this->handled = true;
$this->exitCode = (int) $exitCode;
$r = new \ReflectionProperty($this->error, 'code');
$r->setAccessible(true);
$r->setValue($this->error, $this->exitCode);
}
/**
* Whether the error/exception is handled by a listener or not.
* Gets the exit code.
*
* If it is not yet handled, the error/exception will be displayed in the
* command output.
*
* @return bool
* @return int The command exit code
*/
public function isErrorHandled()
public function getExitCode()
{
return $this->handled;
}
/**
* @deprecated Since version 3.3, to be removed in 4.0. Use getError() instead
*/
public function getException()
{
@trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Use ConsoleErrorEvent::getError() instead.', __METHOD__), E_USER_DEPRECATED);
return parent::getException();
}
/**
* @deprecated Since version 3.3, to be removed in 4.0. Use setError() instead
*/
public function setException(\Exception $exception)
{
@trigger_error(sprintf('The %s() method is deprecated since version 3.3 and will be removed in 4.0. Use ConsoleErrorEvent::setError() instead.', __METHOD__), E_USER_DEPRECATED);
parent::setException($exception);
return null !== $this->exitCode ? $this->exitCode : ($this->error->getCode() ?: 1);
}
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Console\Event;
@trigger_error(sprintf('The "%s" class is deprecated since version 3.3 and will be removed in 4.0. Use the ConsoleErrorEvent instead.', ConsoleExceptionEvent::class), E_USER_DEPRECATED);
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@ -20,22 +22,18 @@ use Symfony\Component\Console\Output\OutputInterface;
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated ConsoleExceptionEvent is deprecated since version 3.3 and will be removed in 4.0. Use ConsoleErrorEvent instead.
* @deprecated since version 3.3, to be removed in 4.0. Use ConsoleErrorEvent instead.
*/
class ConsoleExceptionEvent extends ConsoleEvent
{
private $exception;
private $exitCode;
public function __construct(Command $command = null, InputInterface $input, OutputInterface $output, \Exception $exception, $exitCode, $deprecation = true)
public function __construct(Command $command, InputInterface $input, OutputInterface $output, \Exception $exception, $exitCode)
{
if ($deprecation) {
@trigger_error(sprintf('The %s class is deprecated since version 3.3 and will be removed in 4.0. Use the ConsoleErrorEvent instead.', __CLASS__), E_USER_DEPRECATED);
}
parent::__construct($command, $input, $output);
$this->exception = $exception;
$this->setException($exception);
$this->exitCode = (int) $exitCode;
}

View File

@ -29,7 +29,7 @@ class ConsoleTerminateEvent extends ConsoleEvent
*/
private $exitCode;
public function __construct(Command $command = null, InputInterface $input, OutputInterface $output, $exitCode)
public function __construct(Command $command, InputInterface $input, OutputInterface $output, $exitCode)
{
parent::__construct($command, $input, $output);

View File

@ -12,9 +12,9 @@
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\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@ -22,7 +22,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
* @author James Halsall <james.t.halsall@googlemail.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class ExceptionListener implements EventSubscriberInterface
class ErrorListener implements EventSubscriberInterface
{
private $logger;

View File

@ -1045,7 +1045,7 @@ class ApplicationTest extends TestCase
$dispatcher->addListener('console.error', function (ConsoleErrorEvent $event) {
$event->getOutput()->write('silenced.');
$event->markErrorAsHandled();
$event->setExitCode(0);
});
$dispatcher->addListener('console.command', function () {
@ -1063,7 +1063,7 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'));
$this->assertContains('before.error.silenced.after.', $tester->getDisplay());
$this->assertEquals(0, $tester->getStatusCode());
$this->assertEquals(ConsoleCommandEvent::RETURN_CODE_DISABLED, $tester->getStatusCode());
}
public function testConsoleErrorEventIsTriggeredOnCommandNotFound()
@ -1073,7 +1073,6 @@ class ApplicationTest extends TestCase
$this->assertNull($event->getCommand());
$this->assertInstanceOf(CommandNotFoundException::class, $event->getError());
$event->getOutput()->write('silenced command not found');
$event->markErrorAsHandled();
});
$application = new Application();
@ -1083,12 +1082,12 @@ class ApplicationTest extends TestCase
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'unknown'));
$this->assertContains('silenced command not found', $tester->getDisplay());
$this->assertEquals(0, $tester->getStatusCode());
$this->assertEquals(1, $tester->getStatusCode());
}
/**
* @group legacy
* @expectedDeprecation The "console.exception" event is deprecated since version 3.3 and will be removed in 4.0. Use the "console.error" event instead.
* @expectedDeprecation The "ConsoleEvents::EXCEPTION" event is deprecated since Symfony 3.3 and will be removed in 4.0. Listen to the "ConsoleEvents::ERROR" event instead.
*/
public function testLegacyExceptionListenersAreStillTriggered()
{
@ -1115,13 +1114,6 @@ class ApplicationTest extends TestCase
public function testRunWithError()
{
if (method_exists($this, 'expectException')) {
$this->expectException('Exception');
$this->expectExceptionMessage('dymerr');
} else {
$this->setExpectedException('Exception', 'dymerr');
}
$application = new Application();
$application->setAutoExit(false);
$application->setCatchExceptions(false);
@ -1133,7 +1125,13 @@ class ApplicationTest extends TestCase
});
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'dym'));
try {
$tester->run(array('command' => 'dym'));
$this->fail('Error expected.');
} catch (\Error $e) {
$this->assertSame('dymerr', $e->getMessage());
}
}
/**
@ -1143,6 +1141,7 @@ class ApplicationTest extends TestCase
{
$application = new Application();
$application->setAutoExit(false);
$application->setCatchExceptions(false);
$application->setDispatcher(new EventDispatcher());
$application->register('dym')->setCode(function (InputInterface $input, OutputInterface $output) {
@ -1154,8 +1153,7 @@ class ApplicationTest extends TestCase
try {
$tester->run(array('command' => 'dym'));
$this->fail('->run() should rethrow PHP errors if not handled via ConsoleErrorEvent.');
} catch (\Throwable $e) {
$this->assertInstanceOf('Error', $e);
} catch (\Error $e) {
$this->assertSame($e->getMessage(), 'Class \'UnknownClass\' not found');
}
}
@ -1378,7 +1376,7 @@ class ApplicationTest extends TestCase
$event->getOutput()->writeln('after.');
if (!$skipCommand) {
$event->setExitCode(113);
$event->setExitCode(ConsoleCommandEvent::RETURN_CODE_DISABLED);
}
});
$dispatcher->addListener('console.error', function (ConsoleErrorEvent $event) {

View File

@ -16,7 +16,7 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\EventListener\ExceptionListener;
use Symfony\Component\Console\EventListener\ErrorListener;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\Input;
@ -24,36 +24,36 @@ use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ExceptionListenerTest extends TestCase
class ErrorListenerTest extends TestCase
{
public function testOnConsoleError()
{
$exception = new \RuntimeException('An error occurred');
$error = new \TypeError('An error occurred');
$logger = $this->getLogger();
$logger
->expects($this->once())
->method('error')
->with('Error thrown while running command "{command}". Message: "{message}"', array('error' => $exception, 'command' => 'test:run --foo=baz buzz', 'message' => 'An error occurred'))
->with('Error thrown while running command "{command}". Message: "{message}"', array('error' => $error, 'command' => 'test:run --foo=baz buzz', 'message' => 'An error occurred'))
;
$listener = new ExceptionListener($logger);
$listener->onConsoleError($this->getConsoleErrorEvent($exception, new ArgvInput(array('console.php', 'test:run', '--foo=baz', 'buzz')), 1, new Command('test:run')));
$listener = new ErrorListener($logger);
$listener->onConsoleError(new ConsoleErrorEvent(new ArgvInput(array('console.php', 'test:run', '--foo=baz', 'buzz')), $this->getOutput(), $error, new Command('test:run')));
}
public function testOnConsoleErrorWithNoCommandAndNoInputString()
{
$exception = new \RuntimeException('An error occurred');
$error = new \RuntimeException('An error occurred');
$logger = $this->getLogger();
$logger
->expects($this->once())
->method('error')
->with('An error occurred while using the console. Message: "{message}"', array('error' => $exception, 'message' => 'An error occurred'))
->with('An error occurred while using the console. Message: "{message}"', array('error' => $error, 'message' => 'An error occurred'))
;
$listener = new ExceptionListener($logger);
$listener->onConsoleError($this->getConsoleErrorEvent($exception, new NonStringInput(), 1));
$listener = new ErrorListener($logger);
$listener->onConsoleError(new ConsoleErrorEvent(new NonStringInput(), $this->getOutput(), $error));
}
public function testOnConsoleTerminateForNonZeroExitCodeWritesToLog()
@ -65,7 +65,7 @@ class ExceptionListenerTest extends TestCase
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
;
$listener = new ExceptionListener($logger);
$listener = new ErrorListener($logger);
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 255));
}
@ -77,7 +77,7 @@ class ExceptionListenerTest extends TestCase
->method('error')
;
$listener = new ExceptionListener($logger);
$listener = new ErrorListener($logger);
$listener->onConsoleTerminate($this->getConsoleTerminateEvent(new ArgvInput(array('console.php', 'test:run')), 0));
}
@ -88,7 +88,7 @@ class ExceptionListenerTest extends TestCase
'console.error' => array('onConsoleError', -128),
'console.terminate' => array('onConsoleTerminate', -128),
),
ExceptionListener::getSubscribedEvents()
ErrorListener::getSubscribedEvents()
);
}
@ -101,7 +101,7 @@ class ExceptionListenerTest extends TestCase
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run --foo=bar', 'code' => 255))
;
$listener = new ExceptionListener($logger);
$listener = new ErrorListener($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));
@ -116,7 +116,7 @@ class ExceptionListenerTest extends TestCase
->with('Command "{command}" exited with code "{code}"', array('command' => 'test:run', 'code' => 255))
;
$listener = new ExceptionListener($logger);
$listener = new ErrorListener($logger);
$listener->onConsoleTerminate($this->getConsoleTerminateEvent($this->getMockBuilder(InputInterface::class)->getMock(), 255));
}
@ -125,11 +125,6 @@ class ExceptionListenerTest extends TestCase
return $this->getMockForAbstractClass(LoggerInterface::class);
}
private function getConsoleErrorEvent(\Exception $exception, InputInterface $input, $exitCode, Command $command = null)
{
return new ConsoleErrorEvent($input, $this->getOutput(), $exception, $exitCode, $command);
}
private function getConsoleTerminateEvent(InputInterface $input, $exitCode)
{
return new ConsoleTerminateEvent(new Command('test:run'), $input, $this->getOutput(), $exitCode);