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

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

wants to merge 2 commits into from

Conversation

ogizanagi
Copy link
Contributor

@ogizanagi ogizanagi commented Dec 7, 2016

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets N/A
License MIT
Doc PR not yet

This allows to handle nicely any \Throwable when using the console component standalone with PHP 7.

With the full-stack framework, a throwable is caught and handled by the Debug component ErrorHandler and the exception handler registered by Symfony\Component\HttpKernel\EventListener\DebugHandlersListener. (see #20797)

@chalasr
Copy link
Member

chalasr commented Dec 7, 2016

It's surely debatable but I would use $catchErrors instead of $catchThrowables since exceptions are throwable.

*/
public function areThrowablesCaught()
{
return $this->catchExceptions;
Copy link
Member

Choose a reason for hiding this comment

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

should be return $this->catchThrowables I guess

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

$this->assertTrue($application->areThrowablesCaught());
Copy link
Member

Choose a reason for hiding this comment

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

maybe a test should be added for ensuring they are not caught by default?

*/
public function renderException(\Exception $e, OutputInterface $output)
public function renderException(/*\Throwable */$e, OutputInterface $output)
Copy link
Member

Choose a reason for hiding this comment

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

removing the typehint is a BC break for classes extending the method (and there are such classes out there)

Copy link
Contributor Author

@ogizanagi ogizanagi Dec 7, 2016

Choose a reason for hiding this comment

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

Indeed. For now I've deprecated the renderException method in favor of renderThrowable.

The typehint is still commented and would be uncommented as soon as the PHP 5 support is dropped. I guess it's likely to happen for 4.0, so I can add it to UPGRADE-4.0.md file?

@@ -62,6 +62,7 @@ class Application
private $name;
private $version;
private $catchExceptions = true;
private $catchThrowables = false;
Copy link
Member

Choose a reason for hiding this comment

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

I don't think having a second flag makes sense.

Copy link
Contributor Author

@ogizanagi ogizanagi Dec 7, 2016

Choose a reason for hiding this comment

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

What would you suggest instead?
We still have to differentiate exceptions from other throwables, as the last ones are currently handled in the fullstack by an exception handler, whereas exceptions are handled by the Console component itself.

We could make catchExceptions accept bit flags (Application::CATCH_EXCEPTIONS, Application::CATCH_THROWABLES) and consider true the same as Application::CATCH_EXCEPTIONS only. But I don't like it semantically and would complicate things.

@stof stof added the BC Break label Dec 7, 2016
@ogizanagi
Copy link
Contributor Author

ogizanagi commented Dec 7, 2016

It's surely debatable but I would use $catchErrors instead of $catchThrowables since exceptions are throwable.

I asked myself about that. But it might be misleading for PHP 5 users (actual php 5 errors won't be handled by the application). Also throwables does not mean a PHP error, but any other class implementing \Throwable.

@chalasr
Copy link
Member

chalasr commented Dec 7, 2016

Also throwables does not mean a PHP error, but any other class implementing \Throwable.

From the PHP manual:

PHP classes cannot implement the Throwable interface directly

Which class being neither a subclass of \Error nor \Exception could be throwable?

@ogizanagi
Copy link
Contributor Author

PHP classes cannot implement the Throwable interface directly

You're right. Forget about this argument.

Still it might be misleading for PHP 5 users. Unless we register renderThrowable/Error as an error handler if catchThrowables/catchErrors is true ?

@ogizanagi ogizanagi changed the title [Console] Allow Application to catch any throwable [Console] Allow Application to handle errors Dec 7, 2016
@ogizanagi
Copy link
Contributor Author

ogizanagi commented Dec 7, 2016

How bad would it be to handle errors by using the Debug component as in 1cf2545 ? Calling Debug::enable outside from a bootstrapping file looks hazardous to me, but what could be proposed instead, or how to make it safer/reliable? Should we consider that the end-user opted-in by using running this application with catchErrors and accept it may have a wider effect?
Just asking, but we should probably not do anything for PHP 5 after all and stick to PHP 7 errors handling...

@fabpot
Copy link
Member

fabpot commented Dec 8, 2016

I would not rename renderException(). The whole Throwable/Error/Exception hierarchy is there in PHP for BC. There is no need to expose that to our users. At the end of the day, they are dealing with exceptions.

@ogizanagi
Copy link
Contributor Author

ogizanagi commented Dec 8, 2016

Fair enough. Last chance for this then is to create a special exception wrapping an \Error, allowing to pass it to renderException.

@ogizanagi
Copy link
Contributor Author

There is no more BC break nor deprecation here AFAIK. So the label can be removed :)

The typehint will however change in 4.0 without BC layer between, as it was done in 3.0 (#13756). Or we can keep the internal ErrorException and keep the typehint on \Exception.

@fabpot fabpot removed the BC Break label Dec 10, 2016
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)

@ogizanagi ogizanagi changed the title [Console] Allow Application to handle errors [Console] Allow Application to handle PHP 7 Errors Dec 10, 2016
fabpot added a commit that referenced this pull request Dec 13, 2016
This PR was squashed before being merged into the 2.7 branch (closes #20813).

Discussion
----------

[Console] Review Application docblocks

| Q             | A
| ------------- | ---
| Branch?       | 2.7
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | N/A
| License       | MIT
| Doc PR        | N/A

~I know there must be a lot of other places in the core where there is some repeated or useless informations in docblocks, but everytime I dig into the `Application` class, I see this, and I don't want to repeat things for consistency when adding new methods 😅  (for instance in #20808, the `setCatchThrowables / areThrowablesCaught ` methods do not need a docblock description IMHO).~

~This PR adapts docblocks where:~

- ~A docblock description is not required, as everything can be expressed in the `@return / @param` argument (the case mentioned above)~
- ~Information is redundant between description and tags, and the context does not have to be reminded again:~

```diff

    /**
     * Adds an array of command objects.
     *
     * If a Command is not enabled it will not be added.
     *
-     * @param Command[] $commands An array of commands
+     * @param Command[] $commands
     */
    public function addCommands(array $commands)
    {
        foreach ($commands as $command) {
            $this->add($command);
        }
    }
```

Commits
-------

d8c18cc [Console] Review Application docblocks
@ogizanagi
Copy link
Contributor Author

I've just realized the debug component was already required by the console component, thus I don't need the internal Symfony\Component\Console\Exception\ErrorException but can directly use FatalThrowableError (apparently symfony/debug is only required for this class).

I've also removed the notice in UPGRADE-4.0.md, unless we indeed decide to handle \Throwable in Application::renderException().

@chalasr
Copy link
Member

chalasr commented Feb 28, 2017

Rethinking about this, I think the $catchErrors flag should be reverted and errors should always be converted and handled as any exception.

@ogizanagi
Copy link
Contributor Author

ogizanagi commented Feb 28, 2017

Still doesn't look sensible to me to loose informations brung by the Symfony\Component\Debug\ErrorHandler and output by the console thanks to the Symfony\Component\HttpKernel\EventListener\DebugHandlersListener (as mentioned in #20808 (comment)).

If I apply the following patch:

PATCH 1 (handle errors/exceptions the same way)
diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php
index fd12aa0..6175332 100644
--- a/src/Symfony/Component/Console/Application.php
+++ b/src/Symfony/Component/Console/Application.php
@@ -36,6 +36,7 @@ use Symfony\Component\Console\Event\ConsoleExceptionEvent;
 use Symfony\Component\Console\Event\ConsoleTerminateEvent;
 use Symfony\Component\Console\Exception\CommandNotFoundException;
 use Symfony\Component\Console\Exception\LogicException;
+use Symfony\Component\Debug\ErrorHandler;
 use Symfony\Component\Debug\Exception\FatalThrowableError;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
@@ -62,7 +63,6 @@ class Application
     private $name;
     private $version;
     private $catchExceptions = true;
-    private $catchErrors = false;
     private $autoExit = true;
     private $definition;
     private $helperSet;
@@ -124,9 +124,10 @@ class Application
                 throw $e;
             }
         } catch (\Error $e) {
-            if (!$this->catchErrors) {
+            if (!$this->catchExceptions) {
                 throw $e;
             }
+
             $e = new FatalThrowableError($e);
         }
 
@@ -278,22 +279,6 @@ class Application
     }
 
     /**
-     * @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;
-    }
-
-    /**
      * Gets whether to automatically exit after a command execution or not.
      *
      * @return bool Whether to automatically exit after a command execution or not

the output changes from:

capture d ecran 2017-02-28 a 19 10 14

to

capture d ecran 2017-02-28 a 19 12 08

If I try to detect the Symfony ErrorHandler was registered using this patch:

PATCH 2 (detect error handler)
diff --git a/src/Symfony/Component/Console/Application.php b/src/Symfony/Component/Console/Application.php
index fd12aa0..ddc81f1 100644
--- a/src/Symfony/Component/Console/Application.php
+++ b/src/Symfony/Component/Console/Application.php
@@ -36,6 +36,7 @@ use Symfony\Component\Console\Event\ConsoleExceptionEvent;
 use Symfony\Component\Console\Event\ConsoleTerminateEvent;
 use Symfony\Component\Console\Exception\CommandNotFoundException;
 use Symfony\Component\Console\Exception\LogicException;
+use Symfony\Component\Debug\ErrorHandler;
 use Symfony\Component\Debug\Exception\FatalThrowableError;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 
@@ -62,7 +63,6 @@ class Application
     private $name;
     private $version;
     private $catchExceptions = true;
-    private $catchErrors = false;
     private $autoExit = true;
     private $definition;
     private $helperSet;
@@ -124,9 +124,18 @@ class Application
                 throw $e;
             }
         } catch (\Error $e) {
-            if (!$this->catchErrors) {
+            if (!$this->catchExceptions) {
+                throw $e;
+            }
+
+            $handler = set_exception_handler('var_dump');
+            $handler = is_array($handler) ? $handler[0] : null;
+            restore_exception_handler();
+            // Symfony Debug error handler was registered, let it handle the error:
+            if ($handler instanceof ErrorHandler) {
                 throw $e;
             }
+
             $e = new FatalThrowableError($e);
         }
 
@@ -278,22 +287,6 @@ class Application
     }
 
     /**
-     * @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;
-    }
-
-    /**
      * Gets whether to automatically exit after a command execution or not.
      *
      * @return bool Whether to automatically exit after a command execution or not

problem is solved using the fullstack. But that's wrong, as it only tells the ErrorHandler was set, like in any app using Debug::enable(). It doesn't ensure the following has been registered as exception handler (this is what is done by the DebugHandlersListener):

function ($e) use ($app, $output) {
    $app->renderException($e, $output);
};

So any standalone console app using the Debug component would get the raw output instead of being nicely handled by the console component.

Maybe we could try looking if the DebugHandlersListener is registered, but doing such a thing in the Application class looks inappropriate.

Also we should remember that despite the issue I'm trying to describe is about Symfony's ErrorHandler, someone using its own way to output errors to console would encounter the same issues if we're suddenly handling errors in the Application class.

So, yes, I can revert changes to handle errors and exceptions exactly the same way, without this flag (PATCH 1). I just want to be sure everyone is aware of this. To me it's a stumbling block. 😕

At least, the new flag is opt-in and won't cause more troubles than adding 1 line to enable errors catching in a standalone console app.

However, if anyone has another clean and BC solution regarding this, I'll be glad to give it a try. :)

@fabpot
Copy link
Member

fabpot commented Mar 22, 2017

Is it still relevant? We fixed quite a few bugs related to that recently.

@ogizanagi
Copy link
Contributor Author

I need to check, but last time I did, it was still needed to me as trying to explain in my last comment. 😕

I didn't follow much changes lately. Which commits should have enhanced the situation regarding this?

@@ -124,7 +123,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 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!

@ogizanagi
Copy link
Contributor Author

ogizanagi commented Mar 29, 2017

Since #18140 was merged ( ping @wouterj ), this one does not make sense anymore as is (or should revert some changes of #18140). But as explained, catching errors indistinctly from exceptions could be considered as a BC break, as error handlers are not used anymore. Using the same catchExceptions flag for both exceptions & errors prevents us from having a clean path to ensure the BC.

So you can close this as soon as the BC break is acknowledged but considered acceptable to you. But this is also a DX regression in the case of the FrameworkBundle, which used to register a handler & enhance the messages, but doesn't anymore now. :/

@chalasr
Copy link
Member

chalasr commented Apr 3, 2017

Could you confirm #22261 fixes the BC break? I'm not able to add a relevant test for it

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Apr 4, 2017

I think we can close this one, I'm going to revisit the issues spotted here as introduced by #18140 separately.

@fabpot fabpot closed this Apr 4, 2017
@ogizanagi ogizanagi deleted the console/app_throwable branch April 4, 2017 15:05
fabpot added a commit that referenced this pull request Apr 26, 2017
…as-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[Console] Review console.ERROR related behavior

| Q             | A
| ------------- | ---
| Branch?       | 3.3
| Bug fix?      | yes
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

This PR is a follow up of #18140 that I wanted to do since a few weeks.
It enhances this work with fixes and behavior changes.
It embeds #22435 and resolves issues like the one described in #20808.

- makes ConsoleErrorEvent *not* extend the deprecated ConsoleExceptionEvent
- replace ConsoleErrorEvent::markErrorAsHandled by ConsoleErrorEvent::setExitCode
- triggers the deprecation in a more appropriate moment
- renames ExceptionListener to ErrorListener
- tweaks the behavior in relation to  #22435

Commits
-------

a7c67c9 [Console] Review console.ERROR related behavior
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants