diff --git a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php index 280efaa87158..7914695678c7 100644 --- a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php +++ b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php @@ -6,6 +6,7 @@ use Exception; use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Foundation\Application; +use Illuminate\Log\LogManager; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\ErrorHandler\Error\FatalError; use Throwable; @@ -52,7 +53,7 @@ public function bootstrap(Application $app) } /** - * Convert PHP errors to ErrorException instances. + * Report PHP deprecations, or convert PHP errors to ErrorException instances. * * @param int $level * @param string $message @@ -66,10 +67,57 @@ public function bootstrap(Application $app) public function handleError($level, $message, $file = '', $line = 0, $context = []) { if (error_reporting() & $level) { + if ($this->isDeprecation($level)) { + return $this->handleDeprecation($message, $file, $line); + } + throw new ErrorException($message, 0, $level, $file, $line); } } + /** + * Reports a deprecation to the "deprecations" logger. + * + * @param string $message + * @param string $file + * @param int $line + * @return void + */ + public function handleDeprecation($message, $file, $line) + { + try { + $logger = $this->app->make(LogManager::class); + } catch (Exception $e) { + return; + } + + $this->ensureDeprecationLoggerIsConfigured(); + + with($logger->channel('deprecations'), function ($log) use ($message, $file, $line) { + $log->warning(sprintf('%s in %s on line %s', + $message, $file, $line + )); + }); + } + + /** + * Ensure the "deprecations" logger is configured. + * + * @return void + */ + protected function ensureDeprecationLoggerIsConfigured() + { + with($this->app['config'], function ($config) { + if ($config->get('logging.channels.deprecations')) { + return; + } + + $driver = $config->get('logging.deprecations') ?? 'null'; + + $config->set('logging.channels.deprecations', $config->get("logging.channels.{$driver}")); + }); + } + /** * Handle an uncaught exception from the application. * @@ -143,6 +191,17 @@ protected function fatalErrorFromPhpError(array $error, $traceOffset = null) return new FatalError($error['message'], 0, $error, $traceOffset); } + /** + * Determine if the error level is a deprecation. + * + * @param int $level + * @return bool + */ + protected function isDeprecation($level) + { + return in_array($level, [E_DEPRECATED, E_USER_DEPRECATED]); + } + /** * Determine if the error type is fatal. * diff --git a/tests/Foundation/Bootstrap/HandleExceptionsTest.php b/tests/Foundation/Bootstrap/HandleExceptionsTest.php new file mode 100644 index 000000000000..14a314322268 --- /dev/null +++ b/tests/Foundation/Bootstrap/HandleExceptionsTest.php @@ -0,0 +1,170 @@ +container = Container::setInstance(new Container); + + $this->config = new Config(); + + $this->container->singleton('config', function () { + return $this->config; + }); + + $this->handleExceptions = new HandleExceptions(); + + with(new ReflectionClass($this->handleExceptions), function ($reflection) { + $property = tap($reflection->getProperty('app'))->setAccessible(true); + + $property->setValue( + $this->handleExceptions, + $this->container + ); + }); + } + + protected function tearDown(): void + { + Container::setInstance(null); + } + + public function testPhpDeprecations() + { + $logger = m::mock(LogManager::class); + $this->container->instance(LogManager::class, $logger); + $logger->shouldReceive('channel')->with('deprecations')->andReturnSelf(); + $logger->shouldReceive('warning')->with(sprintf('%s in %s on line %s', + 'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated', + '/home/user/laravel/routes/web.php', + 17 + )); + + $this->handleExceptions->handleError( + E_DEPRECATED, + 'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated', + '/home/user/laravel/routes/web.php', + 17 + ); + } + + public function testUserDeprecations() + { + $logger = m::mock(LogManager::class); + $this->container->instance(LogManager::class, $logger); + $logger->shouldReceive('channel')->with('deprecations')->andReturnSelf(); + $logger->shouldReceive('warning')->with(sprintf('%s in %s on line %s', + 'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated', + '/home/user/laravel/routes/web.php', + 17 + )); + + $this->handleExceptions->handleError( + E_USER_DEPRECATED, + 'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated', + '/home/user/laravel/routes/web.php', + 17 + ); + } + + public function testErrors() + { + $logger = m::mock(LogManager::class); + $this->container->instance(LogManager::class, $logger); + $logger->shouldNotReceive('channel'); + $logger->shouldNotReceive('warning'); + + $this->expectException(ErrorException::class); + $this->expectExceptionMessage('Something went wrong'); + + $this->handleExceptions->handleError( + E_ERROR, + 'Something went wrong', + '/home/user/laravel/src/Providers/AppServiceProvider.php', + 17 + ); + } + + public function testEnsuresDeprecationsDriver() + { + $logger = m::mock(LogManager::class); + $this->container->instance(LogManager::class, $logger); + $logger->shouldReceive('channel')->andReturnSelf(); + $logger->shouldReceive('warning'); + + $this->config->set('logging.channels.stack', [ + 'driver' => 'stack', + 'channels' => ['single'], + 'ignore_exceptions' => false, + ]); + $this->config->set('logging.deprecations', 'stack'); + + $this->handleExceptions->handleError( + E_USER_DEPRECATED, + 'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated', + '/home/user/laravel/routes/web.php', + 17 + ); + + $this->assertEquals( + [ + 'driver' => 'stack', + 'channels' => ['single'], + 'ignore_exceptions' => false, + ], + $this->config->get('logging.channels.deprecations') + ); + } + + public function testEnsuresNullDeprecationsDriver() + { + $logger = m::mock(LogManager::class); + $this->container->instance(LogManager::class, $logger); + $logger->shouldReceive('channel')->andReturnSelf(); + $logger->shouldReceive('warning'); + + $this->config->set('logging.channels.null', [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ]); + + $this->handleExceptions->handleError( + E_USER_DEPRECATED, + 'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated', + '/home/user/laravel/routes/web.php', + 17 + ); + + $this->assertEquals( + NullHandler::class, + $this->config->get('logging.channels.deprecations.handler') + ); + } + + public function testNoDeprecationsDriverIfNoDeprecationsHereSend() + { + $this->assertEquals(null, $this->config->get('logging.deprecations')); + $this->assertEquals(null, $this->config->get('logging.channels.deprecations')); + } + + public function testIgnoreDeprecationIfLoggerUnresolvable() + { + $this->handleExceptions->handleError( + E_DEPRECATED, + 'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated', + '/home/user/laravel/routes/web.php', + 17 + ); + } +}