From e7fd05c2efea5c13e492b1d67d1afccfd95821b4 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 20 Oct 2022 14:45:59 +0800 Subject: [PATCH 1/4] feat: Opt-in logging of deprecations --- app/Config/Exceptions.php | 25 ++++++++++++++ system/Debug/Exceptions.php | 35 ++++++++++++++++++++ tests/system/Debug/ExceptionsTest.php | 47 +++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/app/Config/Exceptions.php b/app/Config/Exceptions.php index c695b7257660..ca0713a33711 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 and 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..adb5f1ad0c70 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -22,6 +22,7 @@ use Config\Exceptions as ExceptionsConfig; use Config\Paths; use ErrorException; +use Psr\Log\LogLevel; use Throwable; /** @@ -82,6 +83,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 = LogLevel::WARNING; + } } /** @@ -155,6 +160,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->handleDeprecationError($message, $file, $line); + } + if (! (error_reporting() & $severity)) { return; } @@ -328,6 +337,32 @@ 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 handleDeprecationError(string $message, ?string $file = null, ?int $line = null): bool + { + // Remove the trace of the error handler. + $trace = array_slice(debug_backtrace(), 2); + + 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($trace), + ] + ); + + return true; + } + // -------------------------------------------------------------------- // Display Methods // -------------------------------------------------------------------- diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index e98ba5e0b0aa..9a308588f900 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,55 @@ 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 testDeprecationsOnPhp81DoNotThrow(): void + { + $config = new ExceptionsConfig(); + + $config->logDeprecationsOnly = true; + $config->deprecationLogLevel = 'error'; + + $this->exception = new Exceptions($config, Services::request(), Services::response()); + $this->exception->initialize(); + + // this is only needed for IDEs not to complain that strlen does not accept explicit null + $maybeNull = PHP_VERSION_ID >= 80100 ? null : 'random string'; + + try { + strlen($maybeNull); + $this->assertLogContains('error', '[DEPRECATED] strlen(): '); + } catch (ErrorException $e) { + $this->fail('The catch block should not be reached.'); + } finally { + restore_error_handler(); + restore_exception_handler(); + } + } + + public function testSuppressedDeprecationsAreLogged(): void + { + $config = new ExceptionsConfig(); + + $config->logDeprecationsOnly = true; + $config->deprecationLogLevel = 'error'; + + $this->exception = new Exceptions($config, Services::request(), Services::response()); + $this->exception->initialize(); + + @trigger_error('Hello! I am a deprecation!', E_USER_DEPRECATED); + $this->assertLogContains('error', '[DEPRECATED] Hello! I am a deprecation!', false); + + restore_error_handler(); + restore_exception_handler(); + } + public function testDetermineViews(): void { $determineView = $this->getPrivateMethodInvoker($this->exception, 'determineView'); From 7d8ac783825f63799d35be1eca5ed01293472fdc Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Thu, 20 Oct 2022 19:33:55 +0800 Subject: [PATCH 2/4] Add explicit return type --- system/Debug/Exceptions.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index adb5f1ad0c70..262a0c5658ad 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -344,6 +344,11 @@ private function isDeprecationError(int $error): bool return ($error & $deprecations) !== 0; } + /** + * @noRector \Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector + * + * @return true + */ private function handleDeprecationError(string $message, ?string $file = null, ?int $line = null): bool { // Remove the trace of the error handler. From a795f41f55c65c80e00097efbdf13f3182edffb5 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Fri, 21 Oct 2022 12:50:40 +0800 Subject: [PATCH 3/4] Add documentation --- user_guide_src/source/changelogs/v4.3.0.rst | 5 ++++ user_guide_src/source/general/errors.rst | 28 ++++++++++++++++++++ user_guide_src/source/general/errors/012.php | 14 ++++++++++ user_guide_src/source/general/errors/013.php | 12 +++++++++ user_guide_src/source/general/errors/014.php | 4 +++ 5 files changed, 63 insertions(+) create mode 100644 user_guide_src/source/general/errors/012.php create mode 100644 user_guide_src/source/general/errors/013.php create mode 100644 user_guide_src/source/general/errors/014.php diff --git a/user_guide_src/source/changelogs/v4.3.0.rst b/user_guide_src/source/changelogs/v4.3.0.rst index ca233606a527..25b7c28bc6d9 100644 --- a/user_guide_src/source/changelogs/v4.3.0.rst +++ b/user_guide_src/source/changelogs/v4.3.0.rst @@ -176,6 +176,11 @@ Helpers and Functions - Added :php:func:`request()` and :php:func:`response()` functions. - Add :php:func:`decamelize()` function to convert camelCase to snake_case. +Error Handling +============== + +- You can now log deprecation errors instead of throwing them. See :ref:`logging_deprecation_errors` for details. + Others ====== diff --git a/user_guide_src/source/general/errors.rst b/user_guide_src/source/general/errors.rst index fb34f2b744ad..152d6f74a7bc 100644 --- a/user_guide_src/source/general/errors.rst +++ b/user_guide_src/source/general/errors.rst @@ -133,3 +133,31 @@ Since v4.3.0, you can specify the exit code for your Exception class to implemen ``HasExitCodeInterface``. When an exception implementing ``HasExitCodeInterface`` is caught by CodeIgniter's exception handler, the code returned from the ``getExitCode()`` method will become the exit code. + +.. _logging_deprecation_errors: + +Logging Deprecation Errors +========================== + +.. versionadded:: 4.3.0 + +By default, all errors reported by ``error_reporting()`` will be thrown as an ``ErrorException`` object. These +include both ``E_DEPRECATED`` and ``E_USER_DEPRECATED`` errors. With the surge in use of PHP 8.1+, many users +may see exceptions thrown for `passing null to non-nullable arguments of internal functions `_. +To ease the migration to PHP 8.1, you can instruct CodeIgniter to log the deprecations instead of throwing them. + +First, make sure your copy of ``Config\Exceptions`` is updated with the two new properties and set as follows: + +.. literalinclude:: errors/012.php + +Next, depending on the log level you set in ``Config\Exceptions::$deprecationLogLevel``, check whether the +logger threshold defined in ``Config\Logger::$threshold`` covers the deprecation log level. If not, adjust +it accordingly. + +.. literalinclude:: errors/013.php + +After that, subsequent deprecations will be logged instead of thrown. + +This feature also works with user deprecations: + +.. literalinclude:: errors/014.php diff --git a/user_guide_src/source/general/errors/012.php b/user_guide_src/source/general/errors/012.php new file mode 100644 index 000000000000..bf70772b97e9 --- /dev/null +++ b/user_guide_src/source/general/errors/012.php @@ -0,0 +1,14 @@ + Date: Fri, 21 Oct 2022 21:43:42 +0800 Subject: [PATCH 4/4] Remove unneeded argument in assertLogContains. --- tests/system/Debug/ExceptionsTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index 9a308588f900..b34a8fddca7a 100644 --- a/tests/system/Debug/ExceptionsTest.php +++ b/tests/system/Debug/ExceptionsTest.php @@ -76,7 +76,7 @@ public function testSuppressedDeprecationsAreLogged(): void $this->exception->initialize(); @trigger_error('Hello! I am a deprecation!', E_USER_DEPRECATED); - $this->assertLogContains('error', '[DEPRECATED] Hello! I am a deprecation!', false); + $this->assertLogContains('error', '[DEPRECATED] Hello! I am a deprecation!'); restore_error_handler(); restore_exception_handler();