Skip to content

Commit 93e69e4

Browse files
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);`
2 parents bd66434 + f360758 commit 93e69e4

File tree

3 files changed

+97
-1
lines changed

3 files changed

+97
-1
lines changed

src/Symfony/Component/Debug/ErrorHandler.php

+46-1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class ErrorHandler
100100
private static $reservedMemory;
101101
private static $stackedErrors = array();
102102
private static $stackedErrorLevels = array();
103+
private static $toStringException = null;
103104

104105
/**
105106
* Same init value as thrownErrors.
@@ -377,7 +378,10 @@ public function handleError($type, $message, $file, $line, array $context, array
377378
}
378379

379380
if ($throw) {
380-
if (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
381+
if (null !== self::$toStringException) {
382+
$throw = self::$toStringException;
383+
self::$toStringException = null;
384+
} elseif (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
381385
// Checking for class existence is a work around for https://bugs.php.net/42098
382386
$throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context);
383387
} else {
@@ -392,6 +396,47 @@ public function handleError($type, $message, $file, $line, array $context, array
392396
$throw->errorHandlerCanary = new ErrorHandlerCanary();
393397
}
394398

399+
if (E_USER_ERROR & $type) {
400+
$backtrace = $backtrace ?: $throw->getTrace();
401+
402+
for ($i = 1; isset($backtrace[$i]); ++$i) {
403+
if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function'])
404+
&& '__toString' === $backtrace[$i]['function']
405+
&& '->' === $backtrace[$i]['type']
406+
&& !isset($backtrace[$i - 1]['class'])
407+
&& ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function'])
408+
) {
409+
// Here, we know trigger_error() has been called from __toString().
410+
// HHVM is fine with throwing from __toString() but PHP triggers a fatal error instead.
411+
// A small convention allows working around the limitation:
412+
// given a caught $e exception in __toString(), quitting the method with
413+
// `return trigger_error($e, E_USER_ERROR);` allows this error handler
414+
// to make $e get through the __toString() barrier.
415+
416+
foreach ($context as $e) {
417+
if (($e instanceof \Exception || $e instanceof \Throwable) && $e->__toString() === $message) {
418+
if (1 === $i) {
419+
// On HHVM
420+
$throw = $e;
421+
break;
422+
}
423+
self::$toStringException = $e;
424+
425+
return true;
426+
}
427+
}
428+
429+
if (1 < $i) {
430+
// On PHP (not on HHVM), display the original error message instead of the default one.
431+
$this->handleException($throw);
432+
433+
// Stop the process by giving back the error to the native handler.
434+
return false;
435+
}
436+
}
437+
}
438+
}
439+
395440
throw $throw;
396441
}
397442

src/Symfony/Component/Debug/Tests/ErrorHandlerTest.php

+27
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,33 @@ public function testHandleError()
268268
}
269269
}
270270

271+
public function testHandleUserError()
272+
{
273+
try {
274+
$handler = ErrorHandler::register();
275+
$handler->throwAt(0, true);
276+
277+
$e = null;
278+
$x = new \Exception('Foo');
279+
280+
try {
281+
$f = new Fixtures\ToStringThrower($x);
282+
$f .= ''; // Trigger $f->__toString()
283+
} catch (\Exception $e) {
284+
}
285+
286+
$this->assertSame($x, $e);
287+
288+
restore_error_handler();
289+
restore_exception_handler();
290+
} catch (\Exception $e) {
291+
restore_error_handler();
292+
restore_exception_handler();
293+
294+
throw $e;
295+
}
296+
}
297+
271298
public function testHandleException()
272299
{
273300
try {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Symfony\Component\Debug\Tests\Fixtures;
4+
5+
class ToStringThrower
6+
{
7+
private $exception;
8+
9+
public function __construct(\Exception $e)
10+
{
11+
$this->exception = $e;
12+
}
13+
14+
public function __toString()
15+
{
16+
try {
17+
throw $this->exception;
18+
} catch (\Exception $e) {
19+
// Using user_error() here is on purpose so we do not forget
20+
// that this alias also should work alongside with trigger_error().
21+
return user_error($e, E_USER_ERROR);
22+
}
23+
}
24+
}

0 commit comments

Comments
 (0)