Skip to content

Commit

Permalink
feature #18140 [Console] Add console.ERROR event and deprecate consol…
Browse files Browse the repository at this point in the history
…e.EXCEPTION (wouterj)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[Console] Add console.ERROR event and deprecate console.EXCEPTION

| Q | A |
| --- | --- |
| Branch | master |
| Bug fix? | yes |
| New feature? | yes |
| BC breaks? | no |
| Deprecations? | yes |
| Tests pass? | yes |
| Fixed tickets | - |
| License | MIT |
| Doc PR | todo |
## The Problem

The current `console.EXCEPTION` event is only dispatched for exceptions during the execution of `Command#execute()`. All other exceptions (e.g. the ones thrown by listeners to events) are catched by the `try ... catch` loop in `Application#doRunCommand()`. This means that there is _no way to override exception handling_.
## The Solution

This PR adds a `console.ERROR` event which has the same scope as the default `try ... catch` loop. This allows to customize all exception handling.

In order to keep BC, a new event was created and `console.EXCEPTION` was deprecated.

Commits
-------

c02a4c9 Added a console.ERROR event
  • Loading branch information
fabpot committed Mar 22, 2017
2 parents 42e5b4e + c02a4c9 commit ba4d6bc
Show file tree
Hide file tree
Showing 12 changed files with 298 additions and 55 deletions.
7 changes: 7 additions & 0 deletions UPGRADE-3.3.md
Expand Up @@ -73,6 +73,13 @@ Debug

* The `ContextErrorException` class is deprecated. `\ErrorException` will be used instead in 4.0.

Console
-------

* The `console.exception` event and the related `ConsoleExceptionEvent` class
have been deprecated in favor of the `console.error` event and the `ConsoleErrorEvent`
class. The deprecated event and class will be removed in 4.0.

DependencyInjection
-------------------

Expand Down
3 changes: 3 additions & 0 deletions UPGRADE-4.0.md
Expand Up @@ -58,6 +58,9 @@ Console
$commandTester->execute();
```

* The `console.exception` event and the related `ConsoleExceptionEvent` class have
been removed in favor of the `console.error` event and the `ConsoleErrorEvent` class.

Debug
-----

Expand Down
37 changes: 31 additions & 6 deletions src/Symfony/Component/Console/Application.php
Expand Up @@ -33,6 +33,7 @@
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\Console\Exception\CommandNotFoundException;
Expand Down Expand Up @@ -118,16 +119,40 @@ public function run(InputInterface $input = null, OutputInterface $output = null
$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($this->runningCommand, $input, $output, $e, $e->getCode());
$this->dispatcher->dispatch(ConsoleEvents::ERROR, $event);

$e = $event->getError();

if ($event->isErrorHandled()) {
$e = null;
$exitCode = 0;
} else {
$exitCode = $e->getCode();
}

$event = new ConsoleTerminateEvent($this->runningCommand, $input, $output, $exitCode);
$this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
}

if (null !== $e) {
if (!$this->catchExceptions) {
throw $e;
}

if ($output instanceof ConsoleOutputInterface) {
$this->renderException($e, $output->getErrorOutput());
$this->renderException($exception, $output->getErrorOutput());
} else {
$this->renderException($e, $output);
$this->renderException($exception, $output);
}

$exitCode = $e->getCode();
Expand Down Expand Up @@ -863,17 +888,17 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
} catch (\Throwable $x) {
$e = new FatalThrowableError($x);
}

if (null !== $e) {
$event = new ConsoleExceptionEvent($command, $input, $output, $e, $e->getCode());
$event = new ConsoleExceptionEvent($command, $input, $output, $e, $e->getCode(), false);
$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();
}

$event = new ConsoleTerminateEvent($command, $input, $output, $e->getCode());
$this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);

throw $x;
}
} else {
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Component/Console/CHANGELOG.md
Expand Up @@ -8,6 +8,8 @@ CHANGELOG
* added `AddConsoleCommandPass` (originally in FrameworkBundle)
* [BC BREAK] `Input::getOption()` no longer returns the default value for options
with value optional explicitly passed empty
* added console.error event to catch exceptions thrown by other listeners
* deprecated console.exception event in favor of console.error

3.2.0
------
Expand Down
18 changes: 17 additions & 1 deletion src/Symfony/Component/Console/ConsoleEvents.php
Expand Up @@ -40,14 +40,30 @@ final class ConsoleEvents
const TERMINATE = 'console.terminate';

/**
* The EXCEPTION event occurs when an uncaught exception appears.
* The EXCEPTION event occurs when an uncaught exception appears
* while executing Command#run().
*
* This event allows you to deal with the exception or
* to modify the thrown exception.
*
* @Event("Symfony\Component\Console\Event\ConsoleExceptionEvent")
*
* @var string
*
* @deprecated The console.exception event is deprecated since version 3.3 and will be removed in 4.0. Use the console.error event instead.
*/
const EXCEPTION = 'console.exception';

/**
* The ERROR event occurs when an uncaught exception appears or
* a throwable error.
*
* This event allows you to deal with the exception/error or
* to modify the thrown exception.
*
* @Event("Symfony\Component\Console\Event\ConsoleErrorEvent")
*
* @var string
*/
const ERROR = 'console.error';
}
112 changes: 112 additions & 0 deletions src/Symfony/Component/Console/Event/ConsoleErrorEvent.php
@@ -0,0 +1,112 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Console\Event;

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
{
private $error;
private $handled = false;

public function __construct(Command $command, InputInterface $input, OutputInterface $output, $error, $exitCode)
{
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)));
}

$exception = $error;
if (!$error instanceof \Exception) {
$exception = new FatalThrowableError($error);
}
parent::__construct($command, $input, $output, $exception, $exitCode, false);

$this->error = $error;
}

/**
* Returns the thrown error/exception.
*
* @return \Throwable
*/
public function getError()
{
return $this->error;
}

/**
* Replaces the thrown error/exception.
*
* @param \Throwable $error
*/
public function setError($error)
{
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)));
}

$this->error = $error;
}

/**
* Marks the error/exception as handled.
*
* If it is not marked as handled, the error/exception will be displayed in
* the command output.
*/
public function markErrorAsHandled()
{
$this->handled = true;
}

/**
* Whether the error/exception is handled by a listener or not.
*
* If it is not yet handled, the error/exception will be displayed in the
* command output.
*
* @return bool
*/
public function isErrorHandled()
{
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);
}
}
4 changes: 2 additions & 2 deletions src/Symfony/Component/Console/Event/ConsoleEvent.php
Expand Up @@ -28,7 +28,7 @@ class ConsoleEvent extends Event
private $input;
private $output;

public function __construct(Command $command, InputInterface $input, OutputInterface $output)
public function __construct(Command $command = null, InputInterface $input, OutputInterface $output)
{
$this->command = $command;
$this->input = $input;
Expand All @@ -38,7 +38,7 @@ public function __construct(Command $command, InputInterface $input, OutputInter
/**
* Gets the command that is executed.
*
* @return Command A Command instance
* @return Command|null A Command instance
*/
public function getCommand()
{
Expand Down
11 changes: 9 additions & 2 deletions src/Symfony/Component/Console/Event/ConsoleExceptionEvent.php
Expand Up @@ -19,17 +19,24 @@
* Allows to handle exception thrown in a command.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @deprecated ConsoleExceptionEvent is deprecated since version 3.3 and will be removed in 4.0. Use ConsoleErrorEvent instead.
*/
class ConsoleExceptionEvent extends ConsoleEvent
{
private $exception;
private $exitCode;
private $handled = false;

public function __construct(Command $command, InputInterface $input, OutputInterface $output, \Exception $exception, $exitCode)
public function __construct(Command $command, InputInterface $input, OutputInterface $output, \Exception $exception, $exitCode, $deprecation = true)
{
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->setException($exception);
$this->exception = $exception;
$this->exitCode = (int) $exitCode;
}

Expand Down
Expand Up @@ -29,7 +29,7 @@ class ConsoleTerminateEvent extends ConsoleEvent
*/
private $exitCode;

public function __construct(Command $command, InputInterface $input, OutputInterface $output, $exitCode)
public function __construct(Command $command = null, InputInterface $input, OutputInterface $output, $exitCode)
{
parent::__construct($command, $input, $output);

Expand Down
Expand Up @@ -14,7 +14,7 @@
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\ConsoleErrorEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

Expand All @@ -31,15 +31,15 @@ public function __construct(LoggerInterface $logger = null)
$this->logger = $logger;
}

public function onConsoleException(ConsoleExceptionEvent $event)
public function onConsoleError(ConsoleErrorEvent $event)
{
if (null === $this->logger) {
return;
}

$exception = $event->getException();
$error = $event->getError();

$this->logger->error('Exception thrown while running command "{command}". Message: "{message}"', array('exception' => $exception, 'command' => $this->getInputString($event), 'message' => $exception->getMessage()));
$this->logger->error('Error thrown while running command "{command}". Message: "{message}"', array('error' => $error, 'command' => $this->getInputString($event), 'message' => $error->getMessage()));
}

public function onConsoleTerminate(ConsoleTerminateEvent $event)
Expand All @@ -60,7 +60,7 @@ public function onConsoleTerminate(ConsoleTerminateEvent $event)
public static function getSubscribedEvents()
{
return array(
ConsoleEvents::EXCEPTION => array('onConsoleException', -128),
ConsoleEvents::ERROR => array('onConsoleError', -128),
ConsoleEvents::TERMINATE => array('onConsoleTerminate', -128),
);
}
Expand Down

0 comments on commit ba4d6bc

Please sign in to comment.