diff --git a/src/Symfony/Component/ErrorHandler/CHANGELOG.md b/src/Symfony/Component/ErrorHandler/CHANGELOG.md index 094072510d..c7c245a439 100644 --- a/src/Symfony/Component/ErrorHandler/CHANGELOG.md +++ b/src/Symfony/Component/ErrorHandler/CHANGELOG.md @@ -5,3 +5,4 @@ CHANGELOG ----- * added the component + * added `ErrorHandler::call()` method utility to turn any PHP error into `\ErrorException` diff --git a/src/Symfony/Component/ErrorHandler/ErrorHandler.php b/src/Symfony/Component/ErrorHandler/ErrorHandler.php index 14ab7022fa..2b0d1184fe 100644 --- a/src/Symfony/Component/ErrorHandler/ErrorHandler.php +++ b/src/Symfony/Component/ErrorHandler/ErrorHandler.php @@ -153,6 +153,32 @@ class ErrorHandler return $handler; } + /** + * Calls a function and turns any PHP error into \ErrorException. + * + * @return mixed What $function(...$arguments) returns + * + * @throws \ErrorException When $function(...$arguments) triggers a PHP error + */ + public static function call(callable $function, ...$arguments) + { + set_error_handler(static function (int $type, string $message, string $file, int $line) { + if (__FILE__ === $file) { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); + $file = $trace[2]['file'] ?? $file; + $line = $trace[2]['line'] ?? $line; + } + + throw new \ErrorException($message, 0, $type, $file, $line); + }); + + try { + return $function(...$arguments); + } finally { + restore_error_handler(); + } + } + public function __construct(BufferingLogger $bootstrappingLogger = null) { if ($bootstrappingLogger) { diff --git a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php index c087aed2f3..6f8f56ef69 100644 --- a/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php +++ b/src/Symfony/Component/ErrorHandler/Tests/ErrorHandlerTest.php @@ -123,11 +123,62 @@ class ErrorHandlerTest extends TestCase } // dummy function to test trace in error handler. - private static function triggerNotice($that) + public static function triggerNotice($that) { $that->assertSame('', $foo.$foo.$bar); } + public function testFailureCall() + { + $this->expectException(\ErrorException::class); + $this->expectExceptionMessage('fopen(unknown.txt): failed to open stream: No such file or directory'); + + ErrorHandler::call('fopen', 'unknown.txt', 'r'); + } + + public function testCallRestoreErrorHandler() + { + $prev = set_error_handler('var_dump'); + try { + ErrorHandler::call('fopen', 'unknown.txt', 'r'); + $this->fail('An \ErrorException should have been raised'); + } catch (\ErrorException $e) { + $prev = set_error_handler($prev); + restore_error_handler(); + } finally { + restore_error_handler(); + } + + $this->assertSame('var_dump', $prev); + } + + public function testCallErrorExceptionInfo() + { + try { + ErrorHandler::call([self::class, 'triggerNotice'], $this); + $this->fail('An \ErrorException should have been raised'); + } catch (\ErrorException $e) { + $trace = $e->getTrace(); + $this->assertSame(E_NOTICE, $e->getSeverity()); + $this->assertSame(__FILE__, $e->getFile()); + $this->assertSame('Undefined variable: foo', $e->getMessage()); + $this->assertSame(0, $e->getCode()); + $this->assertSame('Symfony\Component\ErrorHandler\{closure}', $trace[0]['function']); + $this->assertSame(ErrorHandler::class, $trace[0]['class']); + $this->assertSame('triggerNotice', $trace[1]['function']); + $this->assertSame(__CLASS__, $trace[1]['class']); + } + } + + public function testSuccessCall() + { + touch($filename = tempnam(sys_get_temp_dir(), 'sf_error_handler_')); + + self::assertIsResource(ErrorHandler::call('fopen', $filename, 'r')); + + unlink($filename); + } + public function testConstruct() { try {