Skip to content

Commit a4d3201

Browse files
authored
Merge pull request #6705 from paulbalandan/deprecations-log
feat: Opt-in logging of deprecations
2 parents 4c3b162 + 33a2ba5 commit a4d3201

File tree

8 files changed

+175
-0
lines changed

8 files changed

+175
-0
lines changed

app/Config/Exceptions.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Config;
44

55
use CodeIgniter\Config\BaseConfig;
6+
use Psr\Log\LogLevel;
67

78
/**
89
* Setup how the exception handler works.
@@ -49,4 +50,28 @@ class Exceptions extends BaseConfig
4950
* ex. ['server', 'setup/password', 'secret_token']
5051
*/
5152
public array $sensitiveDataInTrace = [];
53+
54+
/**
55+
* --------------------------------------------------------------------------
56+
* LOG DEPRECATIONS INSTEAD OF THROWING?
57+
* --------------------------------------------------------------------------
58+
* By default, CodeIgniter converts deprecations into exceptions. Also,
59+
* starting in PHP 8.1 will cause a lot of deprecated usage warnings.
60+
* Use this option to temporarily cease the warnings and instead log those.
61+
* This option also works for user deprecations.
62+
*/
63+
public bool $logDeprecationsOnly = false;
64+
65+
/**
66+
* --------------------------------------------------------------------------
67+
* LOG LEVEL THRESHOLD FOR DEPRECATIONS
68+
* --------------------------------------------------------------------------
69+
* If `$logDeprecationsOnly` is set to `true`, this sets the log level
70+
* to which the deprecation will be logged. This should be one of the log
71+
* levels recognized by PSR-3.
72+
*
73+
* The related `Config\Logger::$threshold` should be adjusted, if needed,
74+
* to capture logging the deprecations.
75+
*/
76+
public string $deprecationLogLevel = LogLevel::WARNING;
5277
}

system/Debug/Exceptions.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Config\Exceptions as ExceptionsConfig;
2323
use Config\Paths;
2424
use ErrorException;
25+
use Psr\Log\LogLevel;
2526
use Throwable;
2627

2728
/**
@@ -82,6 +83,10 @@ public function __construct(ExceptionsConfig $config, $request, ResponseInterfac
8283
if (! isset($this->config->sensitiveDataInTrace)) {
8384
$this->config->sensitiveDataInTrace = [];
8485
}
86+
if (! isset($this->config->logDeprecationsOnly, $this->config->deprecationLogLevel)) {
87+
$this->config->logDeprecationsOnly = false;
88+
$this->config->deprecationLogLevel = LogLevel::WARNING;
89+
}
8590
}
8691

8792
/**
@@ -155,6 +160,10 @@ public function exceptionHandler(Throwable $exception)
155160
*/
156161
public function errorHandler(int $severity, string $message, ?string $file = null, ?int $line = null)
157162
{
163+
if ($this->isDeprecationError($severity) && $this->config->logDeprecationsOnly) {
164+
return $this->handleDeprecationError($message, $file, $line);
165+
}
166+
158167
if (! (error_reporting() & $severity)) {
159168
return;
160169
}
@@ -328,6 +337,37 @@ protected function determineCodes(Throwable $exception): array
328337
return [$statusCode, $exitStatus];
329338
}
330339

340+
private function isDeprecationError(int $error): bool
341+
{
342+
$deprecations = E_DEPRECATED | E_USER_DEPRECATED;
343+
344+
return ($error & $deprecations) !== 0;
345+
}
346+
347+
/**
348+
* @noRector \Rector\DeadCode\Rector\ClassMethod\RemoveUselessReturnTagRector
349+
*
350+
* @return true
351+
*/
352+
private function handleDeprecationError(string $message, ?string $file = null, ?int $line = null): bool
353+
{
354+
// Remove the trace of the error handler.
355+
$trace = array_slice(debug_backtrace(), 2);
356+
357+
log_message(
358+
$this->config->deprecationLogLevel,
359+
"[DEPRECATED] {message} in {errFile} on line {errLine}.\n{trace}",
360+
[
361+
'message' => $message,
362+
'errFile' => clean_path($file ?? ''),
363+
'errLine' => $line ?? 0,
364+
'trace' => self::renderBacktrace($trace),
365+
]
366+
);
367+
368+
return true;
369+
}
370+
331371
// --------------------------------------------------------------------
332372
// Display Methods
333373
// --------------------------------------------------------------------

tests/system/Debug/ExceptionsTest.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use CodeIgniter\Test\ReflectionHelper;
2020
use Config\Exceptions as ExceptionsConfig;
2121
use Config\Services;
22+
use ErrorException;
2223
use RuntimeException;
2324

2425
/**
@@ -32,9 +33,55 @@ final class ExceptionsTest extends CIUnitTestCase
3233

3334
protected function setUp(): void
3435
{
36+
parent::setUp();
37+
3538
$this->exception = new Exceptions(new ExceptionsConfig(), Services::request(), Services::response());
3639
}
3740

41+
/**
42+
* @requires PHP >= 8.1
43+
*/
44+
public function testDeprecationsOnPhp81DoNotThrow(): void
45+
{
46+
$config = new ExceptionsConfig();
47+
48+
$config->logDeprecationsOnly = true;
49+
$config->deprecationLogLevel = 'error';
50+
51+
$this->exception = new Exceptions($config, Services::request(), Services::response());
52+
$this->exception->initialize();
53+
54+
// this is only needed for IDEs not to complain that strlen does not accept explicit null
55+
$maybeNull = PHP_VERSION_ID >= 80100 ? null : 'random string';
56+
57+
try {
58+
strlen($maybeNull);
59+
$this->assertLogContains('error', '[DEPRECATED] strlen(): ');
60+
} catch (ErrorException $e) {
61+
$this->fail('The catch block should not be reached.');
62+
} finally {
63+
restore_error_handler();
64+
restore_exception_handler();
65+
}
66+
}
67+
68+
public function testSuppressedDeprecationsAreLogged(): void
69+
{
70+
$config = new ExceptionsConfig();
71+
72+
$config->logDeprecationsOnly = true;
73+
$config->deprecationLogLevel = 'error';
74+
75+
$this->exception = new Exceptions($config, Services::request(), Services::response());
76+
$this->exception->initialize();
77+
78+
@trigger_error('Hello! I am a deprecation!', E_USER_DEPRECATED);
79+
$this->assertLogContains('error', '[DEPRECATED] Hello! I am a deprecation!');
80+
81+
restore_error_handler();
82+
restore_exception_handler();
83+
}
84+
3885
public function testDetermineViews(): void
3986
{
4087
$determineView = $this->getPrivateMethodInvoker($this->exception, 'determineView');

user_guide_src/source/changelogs/v4.3.0.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ Helpers and Functions
176176
- Added :php:func:`request()` and :php:func:`response()` functions.
177177
- Add :php:func:`decamelize()` function to convert camelCase to snake_case.
178178

179+
Error Handling
180+
==============
181+
182+
- You can now log deprecation errors instead of throwing them. See :ref:`logging_deprecation_errors` for details.
183+
179184
Others
180185
======
181186

user_guide_src/source/general/errors.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,31 @@ Since v4.3.0, you can specify the exit code for your Exception class to implemen
133133
``HasExitCodeInterface``.
134134

135135
When an exception implementing ``HasExitCodeInterface`` is caught by CodeIgniter's exception handler, the code returned from the ``getExitCode()`` method will become the exit code.
136+
137+
.. _logging_deprecation_errors:
138+
139+
Logging Deprecation Errors
140+
==========================
141+
142+
.. versionadded:: 4.3.0
143+
144+
By default, all errors reported by ``error_reporting()`` will be thrown as an ``ErrorException`` object. These
145+
include both ``E_DEPRECATED`` and ``E_USER_DEPRECATED`` errors. With the surge in use of PHP 8.1+, many users
146+
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>`_.
147+
To ease the migration to PHP 8.1, you can instruct CodeIgniter to log the deprecations instead of throwing them.
148+
149+
First, make sure your copy of ``Config\Exceptions`` is updated with the two new properties and set as follows:
150+
151+
.. literalinclude:: errors/012.php
152+
153+
Next, depending on the log level you set in ``Config\Exceptions::$deprecationLogLevel``, check whether the
154+
logger threshold defined in ``Config\Logger::$threshold`` covers the deprecation log level. If not, adjust
155+
it accordingly.
156+
157+
.. literalinclude:: errors/013.php
158+
159+
After that, subsequent deprecations will be logged instead of thrown.
160+
161+
This feature also works with user deprecations:
162+
163+
.. literalinclude:: errors/014.php
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace Config;
4+
5+
use CodeIgniter\Config\BaseConfig;
6+
use Psr\Log\LogLevel;
7+
8+
class Exceptions extends BaseConfig
9+
{
10+
// ... other properties
11+
12+
public bool $logDeprecationsOnly = true;
13+
public string $deprecationLogLevel = LogLevel::WARNING; // this should be one of the log levels supported by PSR-3
14+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Config;
4+
5+
use CodeIgniter\Config\BaseConfig;
6+
7+
class Logger extends BaseConfig
8+
{
9+
// .. other properties
10+
11+
public $threshold = 5; // originally 4 but changed to 5 to log the warnings from the deprecations
12+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
3+
@trigger_error('Do not use this class!', E_USER_DEPRECATED);
4+
// Your logs should contain a record with a message like: "[DEPRECATED] Do not use this class!"

0 commit comments

Comments
 (0)