Skip to content

[Debug] Allow throwing from __toString() with return trigger_error($e, E_USER_ERROR); #15076

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

Merged
merged 1 commit into from
Jun 25, 2015

Conversation

nicolas-grekas
Copy link
Member

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

@nicolas-grekas
Copy link
Member Author

When one does return trigger_error($exception, E_USER_ERROR); from a __toString() method,
it is possible to convert the call to make it do the same as throw $exception;, which is otherwise forbidden from happening in __toString in PHP.
This convention plays nice with any error handler (even the native one). And when a compliant error handler is used (like ours if this PR gets merged) it can be used to throw from __toString().

try {
throw $this->exception;
} catch (\Exception $e) {
return user_error($e, E_USER_ERROR);
Copy link
Contributor

Choose a reason for hiding this comment

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

we use trigger_error everywhere else

Copy link
Member Author

Choose a reason for hiding this comment

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

This is why I use user_error here: to make sure we do not forget about this alias.

Copy link
Member Author

Choose a reason for hiding this comment

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

Comment added about that

Copy link
Contributor

Choose a reason for hiding this comment

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

But then there is no test for trigger_error is there?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's true...

@nicolas-grekas
Copy link
Member Author

The full workaround is the following:

  • return trigger_error($e, E_USER_ERROR); triggers the error handler
  • $e is casted to string automatically and given to the error handler as $message
  • the error handler looks up for the original exception in its $context variable, searching for a matching index whose value is an exception whose string representation is exactly $message
  • the matched $e exception is temporarily stored in a private $toStringException static property
  • the return value of trigger_error is always a boolean, so we exit __toString() by returning this boolean
  • which triggers an E_RECOVERABLE_ERROR, because this is the behavior of __toString() when a not-string value is returned
  • which calls the error handler again
  • that checks the $toStringException property and throws it (yes, it is allowed at this point).

On HHVM, things are much simpler, because throwing from __toString is allowed.
The behavior ends up being the same (and that's the reason why the provided test case passes on HHVM also).

When no error handler is plugged, PHP dies with a fatal error (as usual in this situation), and with a message full of details: the exception string contains its type+message+stack trace excerpt.

@Tobion
Copy link
Contributor

Tobion commented Jun 23, 2015

the return value of trigger_error is always null

It actually is a boolean

@nicolas-grekas
Copy link
Member Author

It actually is a boolean

thanks, updated

@@ -377,7 +378,10 @@ public function handleError($type, $message, $file, $line, array $context, array
}

if ($throw) {
if (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
if (isset(self::$toStringException)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

normally we do null !== self::$toStringException

Copy link
Member Author

Choose a reason for hiding this comment

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

updated

@Tobion
Copy link
Contributor

Tobion commented Jun 23, 2015

How did you find out about this hack? Never seen it before. We also had to add stupid workarounds in PSR-7 StreamInterface and UriInterface since throwing an exception in toString is not allowed in PHP.

@nicolas-grekas
Copy link
Member Author

I don't remember exactly if I discovered this hack by myself or not, but I used it a few years ago in my own (now history) framework (nicolas-grekas/Patchwork@0f1f1fd)

@Tobion
Copy link
Contributor

Tobion commented Jun 23, 2015

What if I just want to create an exception without catching it first. Something like the following which I guess cannot work. This is why I thought a helper method might be useful, which throws the exception, catches it again and then calls trigger error.

class Uri {
    public function __toString() {
        return trigger_error(new \LogicException('Given URI components cannot be represented as URI reference'), E_USER_ERROR);
    }
}

Or do you get a stack trace with a trigger_error anyway?

    public function __toString() {
        return trigger_error('Given URI components cannot be represented as URI reference', E_USER_ERROR);
    }

@nicolas-grekas
Copy link
Member Author

If you do not follow the convention (which is the case in both your examples), then you will end up in the if after the foreach:
https://github.com/symfony/symfony/pull/15076/files#diff-e7d086f2c2086fdb8f108e74f11dac5bR429

which will handle it as uncaught exception, then kill the process (by returning false).
We have no better choice here since throwing would also kill the process with a fatal error.

@fabpot
Copy link
Member

fabpot commented Jun 25, 2015

👍

1 similar comment
@stof
Copy link
Member

stof commented Jun 25, 2015

👍

@nicolas-grekas nicolas-grekas merged commit f360758 into symfony:2.8 Jun 25, 2015
nicolas-grekas added a commit that referenced this pull request Jun 25, 2015
…trigger_error($e, E_USER_ERROR);` (nicolas-grekas)

This PR was merged into the 2.8 branch.

Discussion
----------

[Debug] Allow throwing from __toString() with `return trigger_error($e, E_USER_ERROR);`

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

Commits
-------

f360758 [Debug] Allow throwing from __toString() with `return trigger_error($e, E_USER_ERROR);`
@nicolas-grekas nicolas-grekas deleted the debug-tostring branch June 25, 2015 09:03
nicolas-grekas added a commit that referenced this pull request Jun 25, 2015
…_toString() (nicolas-grekas)

This PR was merged into the 2.3 branch.

Discussion
----------

[HttpFoundation] Use convention to allow throwing from __toString()

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

Corollary to #15076, works without it.

Commits
-------

8982c32 [HttpFoundation] Use convention to allow throwing from __toString()
@fabpot fabpot mentioned this pull request Nov 16, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants