From f36075817ba1afd4c36d85dcb0c9ded417421f1b Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 22 Jun 2015 19:39:18 +0200 Subject: [PATCH] [Debug] Allow throwing from __toString() with `return trigger_error($e, E_USER_ERROR);` --- src/Symfony/Component/Debug/ErrorHandler.php | 47 ++++++++++++++++++- .../Debug/Tests/ErrorHandlerTest.php | 27 +++++++++++ .../Debug/Tests/Fixtures/ToStringThrower.php | 24 ++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Debug/Tests/Fixtures/ToStringThrower.php diff --git a/src/Symfony/Component/Debug/ErrorHandler.php b/src/Symfony/Component/Debug/ErrorHandler.php index 2d8a9167b2..5af132ddc3 100644 --- a/src/Symfony/Component/Debug/ErrorHandler.php +++ b/src/Symfony/Component/Debug/ErrorHandler.php @@ -100,6 +100,7 @@ class ErrorHandler private static $reservedMemory; private static $stackedErrors = array(); private static $stackedErrorLevels = array(); + private static $toStringException = null; /** * Same init value as thrownErrors. @@ -377,7 +378,10 @@ class ErrorHandler } 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 $throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context); } else { @@ -392,6 +396,47 @@ class ErrorHandler $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; } diff --git a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php index 852976e056..1441397be6 100644 --- a/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php @@ -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() { try { diff --git a/src/Symfony/Component/Debug/Tests/Fixtures/ToStringThrower.php b/src/Symfony/Component/Debug/Tests/Fixtures/ToStringThrower.php new file mode 100644 index 0000000000..40a5fb7f8c --- /dev/null +++ b/src/Symfony/Component/Debug/Tests/Fixtures/ToStringThrower.php @@ -0,0 +1,24 @@ +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); + } + } +}