feature #15076 [Debug] Allow throwing from __toString() with return 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);`
This commit is contained in:
commit
93e69e4735
@ -100,6 +100,7 @@ class ErrorHandler
|
|||||||
private static $reservedMemory;
|
private static $reservedMemory;
|
||||||
private static $stackedErrors = array();
|
private static $stackedErrors = array();
|
||||||
private static $stackedErrorLevels = array();
|
private static $stackedErrorLevels = array();
|
||||||
|
private static $toStringException = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Same init value as thrownErrors.
|
* Same init value as thrownErrors.
|
||||||
@ -377,7 +378,10 @@ class ErrorHandler
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($throw) {
|
if ($throw) {
|
||||||
if (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
|
if (null !== self::$toStringException) {
|
||||||
|
$throw = self::$toStringException;
|
||||||
|
self::$toStringException = null;
|
||||||
|
} elseif (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
|
||||||
// Checking for class existence is a work around for https://bugs.php.net/42098
|
// Checking for class existence is a work around for https://bugs.php.net/42098
|
||||||
$throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context);
|
$throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context);
|
||||||
} else {
|
} else {
|
||||||
@ -392,6 +396,47 @@ class ErrorHandler
|
|||||||
$throw->errorHandlerCanary = new ErrorHandlerCanary();
|
$throw->errorHandlerCanary = new ErrorHandlerCanary();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (E_USER_ERROR & $type) {
|
||||||
|
$backtrace = $backtrace ?: $throw->getTrace();
|
||||||
|
|
||||||
|
for ($i = 1; isset($backtrace[$i]); ++$i) {
|
||||||
|
if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function'])
|
||||||
|
&& '__toString' === $backtrace[$i]['function']
|
||||||
|
&& '->' === $backtrace[$i]['type']
|
||||||
|
&& !isset($backtrace[$i - 1]['class'])
|
||||||
|
&& ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function'])
|
||||||
|
) {
|
||||||
|
// Here, we know trigger_error() has been called from __toString().
|
||||||
|
// HHVM is fine with throwing from __toString() but PHP triggers a fatal error instead.
|
||||||
|
// A small convention allows working around the limitation:
|
||||||
|
// given a caught $e exception in __toString(), quitting the method with
|
||||||
|
// `return trigger_error($e, E_USER_ERROR);` allows this error handler
|
||||||
|
// to make $e get through the __toString() barrier.
|
||||||
|
|
||||||
|
foreach ($context as $e) {
|
||||||
|
if (($e instanceof \Exception || $e instanceof \Throwable) && $e->__toString() === $message) {
|
||||||
|
if (1 === $i) {
|
||||||
|
// On HHVM
|
||||||
|
$throw = $e;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self::$toStringException = $e;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (1 < $i) {
|
||||||
|
// On PHP (not on HHVM), display the original error message instead of the default one.
|
||||||
|
$this->handleException($throw);
|
||||||
|
|
||||||
|
// Stop the process by giving back the error to the native handler.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw $throw;
|
throw $throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,6 +268,33 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testHandleUserError()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$handler = ErrorHandler::register();
|
||||||
|
$handler->throwAt(0, true);
|
||||||
|
|
||||||
|
$e = null;
|
||||||
|
$x = new \Exception('Foo');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$f = new Fixtures\ToStringThrower($x);
|
||||||
|
$f .= ''; // Trigger $f->__toString()
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertSame($x, $e);
|
||||||
|
|
||||||
|
restore_error_handler();
|
||||||
|
restore_exception_handler();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
restore_error_handler();
|
||||||
|
restore_exception_handler();
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function testHandleException()
|
public function testHandleException()
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Symfony\Component\Debug\Tests\Fixtures;
|
||||||
|
|
||||||
|
class ToStringThrower
|
||||||
|
{
|
||||||
|
private $exception;
|
||||||
|
|
||||||
|
public function __construct(\Exception $e)
|
||||||
|
{
|
||||||
|
$this->exception = $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
throw $this->exception;
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// Using user_error() here is on purpose so we do not forget
|
||||||
|
// that this alias also should work alongside with trigger_error().
|
||||||
|
return user_error($e, E_USER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user