Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Opt-in logging of deprecations #6705

Merged
merged 4 commits into from
Oct 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/Config/Exceptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Config;

use CodeIgniter\Config\BaseConfig;
use Psr\Log\LogLevel;

/**
* Setup how the exception handler works.
Expand Down Expand Up @@ -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;
}
40 changes: 40 additions & 0 deletions system/Debug/Exceptions.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Config\Exceptions as ExceptionsConfig;
use Config\Paths;
use ErrorException;
use Psr\Log\LogLevel;
use Throwable;

/**
Expand Down Expand Up @@ -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;
}
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -328,6 +337,37 @@ 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;
}

paulbalandan marked this conversation as resolved.
Show resolved Hide resolved
/**
* @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.
$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
// --------------------------------------------------------------------
Expand Down
47 changes: 47 additions & 0 deletions tests/system/Debug/ExceptionsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use CodeIgniter\Test\ReflectionHelper;
use Config\Exceptions as ExceptionsConfig;
use Config\Services;
use ErrorException;
use RuntimeException;

/**
Expand All @@ -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
MGatner marked this conversation as resolved.
Show resolved Hide resolved
*/
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!');

restore_error_handler();
restore_exception_handler();
}

public function testDetermineViews(): void
{
$determineView = $this->getPrivateMethodInvoker($this->exception, 'determineView');
Expand Down
5 changes: 5 additions & 0 deletions user_guide_src/source/changelogs/v4.3.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
======

Expand Down
28 changes: 28 additions & 0 deletions user_guide_src/source/general/errors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://wiki.php.net/rfc/deprecate_null_to_scalar_internal_arg>`_.
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
14 changes: 14 additions & 0 deletions user_guide_src/source/general/errors/012.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Config;

use CodeIgniter\Config\BaseConfig;
use Psr\Log\LogLevel;

class Exceptions extends BaseConfig
{
// ... other properties

public bool $logDeprecationsOnly = true;
public string $deprecationLogLevel = LogLevel::WARNING; // this should be one of the log levels supported by PSR-3
}
12 changes: 12 additions & 0 deletions user_guide_src/source/general/errors/013.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Config;

use CodeIgniter\Config\BaseConfig;

class Logger extends BaseConfig
{
// .. other properties

public $threshold = 5; // originally 4 but changed to 5 to log the warnings from the deprecations
}
4 changes: 4 additions & 0 deletions user_guide_src/source/general/errors/014.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php

@trigger_error('Do not use this class!', E_USER_DEPRECATED);
// Your logs should contain a record with a message like: "[DEPRECATED] Do not use this class!"