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

Lines changed: 46 additions & 1 deletion
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

Lines changed: 27 additions & 0 deletions
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 {
Lines changed: 24 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy