8000 [Console] Allow Application to handle PHP 7 Errors by ogizanagi · Pull Request #20808 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Console] Allow Application to handle PHP 7 Errors #20808

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
[Console] Allow Application to handle PHP 7 Errors
  • Loading branch information
ogizanagi committed Dec 13, 2016
commit 64f9856a2fc3740bfcd17d3a3de17ac8400a42bd
4 changes: 4 additions & 0 deletions UPGRADE-4.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ Console

* Setting unknown style options is not supported anymore and throws an
exception.

* The signature of `Application::renderException(\Exception $e, OutputInterface $output)`
has changed for `Application::renderException(\Throwable $e, OutputInterface $output)`.
You must update your implementations.

Debug
-----
Expand Down
33 changes: 29 additions & 4 deletions src/Symfony/Component/Console/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Console;

use Symfony\Component\Console\Exception\ErrorException;
use Symfony\Component\Console\Exception\ExceptionInterface;
use Symfony\Component\Console\Helper\DebugFormatterHelper;
use Symfony\Component\Console\Helper\ProcessHelper;
Expand Down Expand Up @@ -62,6 +63,7 @@ class Application
private $name;
private $version;
private $catchExceptions = true;
private $catchErrors = false;
private $autoExit = true;
private $definition;
private $helperSet;
Expand Down Expand Up @@ -100,8 +102,6 @@ public function setDispatcher(EventDispatcherInterface $dispatcher)
* @param OutputInterface $output An Output instance
*
* @return int 0 if everything went fine, or an error code
*
* @throws \Exception When doRun returns Exception
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
Expand All @@ -124,7 +124,14 @@ public function run(InputInterface $input = null, OutputInterface $output = null
if (!$this->catchExceptions) {
throw $e;
}
} catch (\Error $e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn’t this be \Throwable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to 8000 others. Learn more.

Using Throwable would make the catchErrors flag catch both exceptions and errors, where there already is a dedicated flag for exceptions.
So, if we keep both flags (and I still think we need it currently, as I tried to explain in my previous comments, unless we find a better alternative :/ ), it should probably remains \Error here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed!

if (!$this->catchErrors) {
throw $e;
}
$e = new ErrorException($e);
}

if (isset($e)) {
if ($output instanceof ConsoleOutputInterface) {
$this->renderException($e, $output->getErrorOutput());
} else {
Expand Down Expand Up @@ -271,6 +278,22 @@ public function setCatchExceptions($boolean)
$this->catchExceptions = (bool) $boolean;
}

/**
* @return bool Whether errors are caught or not during commands execution
*/
public function areErrorsCaught()
{
return $this->catchErrors;
}

/**
* @param bool $boolean Whether to catch errors or not during commands execution
*/
public function setCatchErrors($boolean)
{
$this->catchErrors = (bool) $boolean;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The addition of these 2 methods does not seem related to this PR and should be removed.

Copy link
Contributor Author
@ogizanagi ogizanagi Dec 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • I doubt about the usefulness of areErrorsCaught right now, but this echoes areExceptionsCaught.
  • setCatchErrors is the way to tell the application instance to catch php 7 errors and cannot be removed (reasons below).

IMHO, we still have to differentiate exceptions from errors, as the last ones are currently handled in the full stack framework by the handler registered through the Debug component and the DebugHandlersListener from HttpKernel, whereas classic exceptions are handled by the Console component itself. Thus, relying on catchExceptions would change the behavior using the full stack, which can be annoying and considered a BC break. The handler used in full stack does extra work on the exception. For instance:

Using full stack:

[Symfony\Component\Debug\Exception\UndefinedMethodException]
Attempted to call an undefined method named "Option" of class "Symfony\Component\Console\Input\ArgvInput".
Did you mean to call e.g. "getOption", "getOptions", "getParameterOption", "hasOption", "hasParameterOption" or "setOption"?

versus the console component standalone, with the catchErrors flag:

[Error]
Call to undefined method Symfony\Component\Console\Input\ArgvInput::Option()

(stack traces are the same)

Thus the second catchErrors flag, otherwise you'll get the last sample using the full stack when an error occurred.

Also see #19813 (comment)


/**
* Gets whether to automatically exit after a command execution or not.
*
Expand Down Expand Up @@ -620,6 +643,10 @@ public static function getAbbreviations($names)
*/
public function renderException(\Exception $e, OutputInterface $output)
{
if ($e instanceof ErrorException) {
$e = $e->getError();
}

$output->writeln('', OutputInterface::VERBOSITY_QUIET);

do {
Expand Down Expand Up @@ -814,8 +841,6 @@ protected function configureIO(InputInterface $input, OutputInterface $output)
* @param OutputInterface $output An Output instance
*
* @return int 0 if everything went fine, or an error code
*
* @throws \Exception when the command being run threw an exception
*/
protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
{
Expand Down
26 changes: 26 additions & 0 deletions src/Symfony/Component/Console/Exception/ErrorException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Symfony\Component\Console\Exception;

/**
* Wrap a PHP 7 Error into an exception.
*
* @internal This class is used internally by Application and should never be used in user-land
*/
final class ErrorException extends \Exception
{
private $error;

public function __construct(\Error $error)
{
$this->error = $error;
}

/**
* @return \Error
*/
public function getError()
{
return $this->error;
}
}
33 changes: 33 additions & 0 deletions src/Symfony/Component/Console/Tests/ApplicationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,39 @@ public function testRunWithErrorFailingStatusCode()
$this->assertSame(1, $tester->getStatusCode(), 'Status code should be 1');
}

public function testErrorsAreNotCaughtByDefault()
{
$application = new Application();

$this->assertFalse($application->areErrorsCaught());
}

/**
* @requires PHP 7
*/
public function testCatchesErrors()
{
$application = new Application();

$application->setCatchErrors(true);
$application->setAutoExit(false);

$this->assertTrue($application->areErrorsCaught());

$application->register('foo')->setCode(function () {
throw new \Error('This error should be catch by Application::run');
});

$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo'));
$this->assertSame(1, $tester->getStatusCode(), 'Status code should be 1');
$this->assertContains(<<<'EOTXT'
[Error]
This error should be catch by Application::run
EOTXT
, $tester->getDisplay(true), 'The PHP error should be caught when catchErrors is true.');
}

public function testRunWithDispatcherSkippingCommand()
{
$application = new Application();
Expand Down
0