feature #33155 [ErrorHandler] Added call() method utility to turns any PHP error into \ErrorException (yceruto)

This PR was merged into the 4.4 branch.

Discussion
----------

[ErrorHandler] Added call() method utility to turns any PHP error into \ErrorException

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | https://github.com/symfony/symfony/issues/32936
| License       | MIT
| Doc PR        | symfony/symfony-docs#...

**Issue**

There is no easy way to catch PHP warnings, though some progress has been made in this area for PHP 8.0 (https://wiki.php.net/rfc/consistent_type_errors).

**Before**
```php
$file = file_get_contents('unknown.txt');
// PHP Warning:  file_get_contents(unknown.txt): failed to open stream: No such file or directory

// workaround:
$file = @file_get_contents('unknown.txt');
if (false === $file) {
    $e = error_get_last();
    throw new \ErrorException($e['message'], 0, $e['type'], $e['file'], $e['line']);
}
```

**After**
```php
$file = ErrorHandler::call('file_get_contents', 'unknown.txt');

// or
$file = ErrorHandler::call(static function () {
    return file_get_contents('unknown.txt');
});

// or (PHP 7.4)
$file = ErrorHandler::call(fn () => file_get_contents('unknown.txt'));
```

All credits to @nicolas-grekas https://github.com/symfony/symfony/issues/32936#issuecomment-518152681 and @vudaltsov for the idea.

Commits
-------

0faa855b5e Added ErrorHandler::call() method utility to turns any PHP warnings into `\ErrorException`
This commit is contained in:
Fabien Potencier 2019-08-18 10:09:38 +02:00
commit 98e86816ad
3 changed files with 79 additions and 1 deletions

View File

@ -5,3 +5,4 @@ CHANGELOG
-----
* added the component
* added `ErrorHandler::call()` method utility to turn any PHP error into `\ErrorException`

View File

@ -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) {

View File

@ -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 {