From 7e655b736c0c980f0c7e39540d5bddfe45b7edc0 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Mon, 17 Oct 2022 18:31:13 +0800 Subject: [PATCH] feat: Opt-in logging of deprecations --- app/Config/Exceptions.php | 25 +++++++++++++ system/Debug/Exceptions.php | 31 ++++++++++++++++ tests/system/Debug/ExceptionsTest.php | 51 +++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/app/Config/Exceptions.php b/app/Config/Exceptions.php index c695b7257660..839bd2b640d1 100644 --- a/app/Config/Exceptions.php +++ b/app/Config/Exceptions.php @@ -3,6 +3,7 @@ namespace Config; use CodeIgniter\Config\BaseConfig; +use Psr\Log\LogLevel; /** * Setup how the exception handler works. @@ -49,4 +50,28 @@ class Exceptions extends BaseConfig * ex. ['server', 'setup/password', 'secret_token'] */ public array $sensitiveDataInTrace = []; + + /** + * -------------------------------------------------------------------------- + * LOG DEPRECATIONS INSTEAD OF THROWING? + * -------------------------------------------------------------------------- + * By default, CodeIgniter converts deprecations into exceptions. Also, + * starting in PHP 8.1 will cause a lot of deprecated usage warnings. + * Use this option to temporarily cease the warnings ang instead log those. + * This option also works for user deprecations. + */ + public bool $logDeprecationsOnly = false; + + /** + * -------------------------------------------------------------------------- + * LOG LEVEL THRESHOLD FOR DEPRECATIONS + * -------------------------------------------------------------------------- + * If `$logDeprecationsOnly` is set to `true`, this sets the log level + * to which the deprecation will be logged. This should be one of the log + * levels recognized by PSR-3. + * + * The related `Config\Logger::$threshold` should be adjusted, if needed, + * to capture logging the deprecations. + */ + public string $deprecationLogLevel = LogLevel::WARNING; } diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 1e976fc6be1a..aa2ef2d6a505 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -82,6 +82,10 @@ public function __construct(ExceptionsConfig $config, $request, ResponseInterfac if (! isset($this->config->sensitiveDataInTrace)) { $this->config->sensitiveDataInTrace = []; } + if (! isset($this->config->logDeprecationsOnly, $this->config->deprecationLogLevel)) { + $this->config->logDeprecationsOnly = false; + $this->config->deprecationLogLevel = 'warning'; + } } /** @@ -155,6 +159,10 @@ public function exceptionHandler(Throwable $exception) */ public function errorHandler(int $severity, string $message, ?string $file = null, ?int $line = null) { + if ($this->isDeprecationError($severity) && $this->config->logDeprecationsOnly) { + return $this->deprecationErrorHandler($message, $file, $line); + } + if (! (error_reporting() & $severity)) { return; } @@ -328,6 +336,29 @@ protected function determineCodes(Throwable $exception): array return [$statusCode, $exitStatus]; } + private function isDeprecationError(int $error): bool + { + $deprecations = E_DEPRECATED | E_USER_DEPRECATED; + + return ($error & $deprecations) !== 0; + } + + private function deprecationErrorHandler(string $message, ?string $file = null, ?int $line = null): bool + { + log_message( + $this->config->deprecationLogLevel, + "[DEPRECATED] {message} in {errFile} on line {errLine}.\n{trace}", + [ + 'message' => $message, + 'errFile' => clean_path($file ?? ''), + 'errLine' => $line ?? 0, + 'trace' => self::renderBacktrace(debug_backtrace()), + ] + ); + + return true; + } + // -------------------------------------------------------------------- // Display Methods // -------------------------------------------------------------------- diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index e98ba5e0b0aa..d7f30bcdcd1e 100644 --- a/tests/system/Debug/ExceptionsTest.php +++ b/tests/system/Debug/ExceptionsTest.php @@ -19,6 +19,7 @@ use CodeIgniter\Test\ReflectionHelper; use Config\Exceptions as ExceptionsConfig; use Config\Services; +use ErrorException; use RuntimeException; /** @@ -32,9 +33,59 @@ final class ExceptionsTest extends CIUnitTestCase protected function setUp(): void { + parent::setUp(); + $this->exception = new Exceptions(new ExceptionsConfig(), Services::request(), Services::response()); } + /** + * @requires PHP >= 8.1 + */ + public function testDeprecationsDoNotThrow(): void + { + $config = new ExceptionsConfig(); + + $config->logDeprecationsOnly = true; + $config->deprecationLogLevel = 'error'; + + $this->exception = new Exceptions($config, Services::request(), Services::response()); + $this->exception->initialize(); + + $maybeNull = 'random string'; + if (PHP_VERSION_ID >= 80100) { + $maybeNull = null; + } + + try { + strlen($maybeNull); + $this->assertLogged('error', '[DEPRECATED] strlen(): ', false); + } catch (ErrorException $e) { + $this->fail('The catch block should not be reached.'); + } finally { + restore_error_handler(); + restore_exception_handler(); + } + } + + public function testSuppressedDeprecationsDoNotThrow(): void + { + $config = new ExceptionsConfig(); + + $config->logDeprecationsOnly = true; + $config->deprecationLogLevel = 'error'; + + $this->exception = new Exceptions($config, Services::request(), Services::response()); + $this->exception->initialize(); + + try { + @trigger_error('Hello! I am a deprecation!', E_USER_DEPRECATED); + $this->assertLogged('error', '[DEPRECATED] Hello! I am a deprecation!', false); + } finally { + restore_error_handler(); + restore_exception_handler(); + } + } + public function testDetermineViews(): void { $determineView = $this->getPrivateMethodInvoker($this->exception, 'determineView');