From 9d0266579a55029f1c30f2c9cc01b6587c3bc8ef Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Tue, 19 Oct 2021 16:20:54 +0100 Subject: [PATCH 1/2] Logs deprecations instead of treating them as exceptions --- config/logging.php | 13 ++ phpunit.xml.dist | 1 + src/Concerns/RegistersExceptionHandlers.php | 87 ++++++++++- tests/HandleExceptionsTest.php | 164 ++++++++++++++++++++ 4 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 tests/HandleExceptionsTest.php diff --git a/config/logging.php b/config/logging.php index 783e8908..4d763646 100644 --- a/config/logging.php +++ b/config/logging.php @@ -19,6 +19,19 @@ 'default' => env('LOG_CHANNEL', 'stack'), + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log driver that should be used to log deprecated + | code still in use by your application. This allows you to prepare your + | application's code for upcoming major versions of your dependencies. + | + */ + + 'deprecations' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + /* |-------------------------------------------------------------------------- | Log Channels diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f240d8b5..1fd04d4a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,6 +1,7 @@ handleError($level, $message, $file, $line); }); set_exception_handler(function ($e) { @@ -56,6 +56,76 @@ protected function registerErrorHandling() }); } + /** + * Report PHP deprecations, or convert PHP errors to ErrorException instances. + * + * @param int $level + * @param string $message + * @param string $file + * @param int $line + * @param array $context + * @return void + * + * @throws \ErrorException + */ + 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->make('log'); + } catch (Exception $e) { + return; + } + + if (! $logger instanceof LogManager) { + 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->make('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 the PHP shutdown event. * @@ -80,6 +150,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/HandleExceptionsTest.php b/tests/HandleExceptionsTest.php new file mode 100644 index 00000000..8d8ab315 --- /dev/null +++ b/tests/HandleExceptionsTest.php @@ -0,0 +1,164 @@ +container = new Container; + + $this->config = new Config(); + + $this->container->singleton('config', function () { + return $this->config; + }); + } + + protected function tearDown(): void + { + $this->container::setInstance(null); + + m::close(); + } + + public function testPhpDeprecations() + { + $logger = m::mock(LogManager::class); + $this->container->instance('log', $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->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('log', $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->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('log', $logger); + $logger->shouldNotReceive('channel'); + $logger->shouldNotReceive('warning'); + + $this->expectException(ErrorException::class); + $this->expectExceptionMessage('Something went wrong'); + + $this->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('log', $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->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('log', $logger); + $logger->shouldReceive('channel')->andReturnSelf(); + $logger->shouldReceive('warning'); + + $this->config->set('logging.channels.null', [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ]); + + $this->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->handleError( + E_DEPRECATED, + 'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated', + '/home/user/laravel/routes/web.php', + 17 + ); + } + + protected function make($abstract, array $parameters = []) + { + return $this->container->make($abstract, $parameters); + } +} From d39ffc1a65c7b82e90599d360b4be22d273a9c83 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 19 Oct 2021 10:31:14 -0500 Subject: [PATCH 2/2] Update logging.php --- config/logging.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/logging.php b/config/logging.php index 4d763646..47313d5c 100644 --- a/config/logging.php +++ b/config/logging.php @@ -24,9 +24,9 @@ | Deprecations Log Channel |-------------------------------------------------------------------------- | - | This option controls the log driver that should be used to log deprecated - | code still in use by your application. This allows you to prepare your - | application's code for upcoming major versions of your dependencies. + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. | */