Skip to content

Commit 529d0a9

Browse files
[8.x] Logs deprecations instead of treating them as exceptions (#1197)
* Logs deprecations instead of treating them as exceptions * Update logging.php Co-authored-by: Taylor Otwell <taylor@laravel.com>
1 parent dc963a0 commit 529d0a9

File tree

4 files changed

+262
-3
lines changed

4 files changed

+262
-3
lines changed

config/logging.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,19 @@
1919

2020
'default' => env('LOG_CHANNEL', 'stack'),
2121

22+
/*
23+
|--------------------------------------------------------------------------
24+
| Deprecations Log Channel
25+
|--------------------------------------------------------------------------
26+
|
27+
| This option controls the log channel that should be used to log warnings
28+
| regarding deprecated PHP and library features. This allows you to get
29+
| your application ready for upcoming major versions of dependencies.
30+
|
31+
*/
32+
33+
'deprecations' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
34+
2235
/*
2336
|--------------------------------------------------------------------------
2437
| Log Channels

phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<phpunit backupGlobals="false"
33
backupStaticAttributes="false"
4+
beStrictAboutTestsThatDoNotTestAnything="false"
45
bootstrap="tests/bootstrap.php"
56
colors="true"
67
convertErrorsToExceptions="true"

src/Concerns/RegistersExceptionHandlers.php

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
namespace Laravel\Lumen\Concerns;
44

55
use ErrorException;
6+
use Exception;
67
use Illuminate\Contracts\Debug\ExceptionHandler;
8+
use Illuminate\Log\LogManager;
79
use Laravel\Lumen\Exceptions\Handler;
810
use Symfony\Component\Console\Output\ConsoleOutput;
911
use Symfony\Component\ErrorHandler\Error\FatalError;
@@ -42,9 +44,7 @@ protected function registerErrorHandling()
4244
error_reporting(-1);
4345

4446
set_error_handler(function ($level, $message, $file = '', $line = 0) {
45-
if (error_reporting() & $level) {
46-
throw new ErrorException($message, 0, $level, $file, $line);
47-
}
47+
$this->handleError($level, $message, $file, $line);
4848
});
4949

5050
set_exception_handler(function ($e) {
@@ -56,6 +56,76 @@ protected function registerErrorHandling()
5656
});
5757
}
5858

59+
/**
60+
* Report PHP deprecations, or convert PHP errors to ErrorException instances.
61+
*
62+
* @param int $level
63+
* @param string $message
64+
* @param string $file
65+
* @param int $line
66+
* @param array $context
67+
* @return void
68+
*
69+
* @throws \ErrorException
70+
*/
71+
public function handleError($level, $message, $file = '', $line = 0, $context = [])
72+
{
73+
if (error_reporting() & $level) {
74+
if ($this->isDeprecation($level)) {
75+
return $this->handleDeprecation($message, $file, $line);
76+
}
77+
78+
throw new ErrorException($message, 0, $level, $file, $line);
79+
}
80+
}
81+
82+
/**
83+
* Reports a deprecation to the "deprecations" logger.
84+
*
85+
* @param string $message
86+
* @param string $file
87+
* @param int $line
88+
* @return void
89+
*/
90+
public function handleDeprecation($message, $file, $line)
91+
{
92+
try {
93+
$logger = $this->make('log');
94+
} catch (Exception $e) {
95+
return;
96+
}
97+
98+
if (! $logger instanceof LogManager) {
99+
return;
100+
}
101+
102+
$this->ensureDeprecationLoggerIsConfigured();
103+
104+
with($logger->channel('deprecations'), function ($log) use ($message, $file, $line) {
105+
$log->warning(sprintf('%s in %s on line %s',
106+
$message, $file, $line
107+
));
108+
});
109+
}
110+
111+
/**
112+
* Ensure the "deprecations" logger is configured.
113+
*
114+
* @return void
115+
*/
116+
protected function ensureDeprecationLoggerIsConfigured()
117+
{
118+
with($this->make('config'), function ($config) {
119+
if ($config->get('logging.channels.deprecations')) {
120+
return;
121+
}
122+
123+
$driver = $config->get('logging.deprecations') ?? 'null';
124+
125+
$config->set('logging.channels.deprecations', $config->get("logging.channels.{$driver}"));
126+
});
127+
}
128+
59129
/**
60130
* Handle the PHP shutdown event.
61131
*
@@ -80,6 +150,17 @@ protected function fatalErrorFromPhpError(array $error, $traceOffset = null)
80150
return new FatalError($error['message'], 0, $error, $traceOffset);
81151
}
82152

153+
/**
154+
* Determine if the error level is a deprecation.
155+
*
156+
* @param int $level
157+
* @return bool
158+
*/
159+
protected function isDeprecation($level)
160+
{
161+
return in_array($level, [E_DEPRECATED, E_USER_DEPRECATED]);
162+
}
163+
83164
/**
84165
* Determine if the error type is fatal.
85166
*

tests/HandleExceptionsTest.php

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<?php
2+
3+
use Illuminate\Config\Repository as Config;
4+
use Illuminate\Container\Container;
5+
use Illuminate\Log\LogManager;
6+
use Laravel\Lumen\Concerns\RegistersExceptionHandlers;
7+
use Mockery as m;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class HandleExceptionsTest extends TestCase
11+
{
12+
use RegistersExceptionHandlers;
13+
14+
protected function setUp(): void
15+
{
16+
$this->container = new Container;
17+
18+
$this->config = new Config();
19+
20+
$this->container->singleton('config', function () {
21+
return $this->config;
22+
});
23+
}
24+
25+
protected function tearDown(): void
26+
{
27+
$this->container::setInstance(null);
28+
29+
m::close();
30+
}
31+
32+
public function testPhpDeprecations()
33+
{
34+
$logger = m::mock(LogManager::class);
35+
$this->container->instance('log', $logger);
36+
$logger->shouldReceive('channel')->with('deprecations')->andReturnSelf();
37+
$logger->shouldReceive('warning')->with(sprintf('%s in %s on line %s',
38+
'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated',
39+
'/home/user/laravel/routes/web.php',
40+
17
41+
));
42+
43+
$this->handleError(
44+
E_DEPRECATED,
45+
'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated',
46+
'/home/user/laravel/routes/web.php',
47+
17
48+
);
49+
}
50+
51+
public function testUserDeprecations()
52+
{
53+
$logger = m::mock(LogManager::class);
54+
$this->container->instance('log', $logger);
55+
$logger->shouldReceive('channel')->with('deprecations')->andReturnSelf();
56+
$logger->shouldReceive('warning')->with(sprintf('%s in %s on line %s',
57+
'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated',
58+
'/home/user/laravel/routes/web.php',
59+
17
60+
));
61+
62+
$this->handleError(
63+
E_USER_DEPRECATED,
64+
'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated',
65+
'/home/user/laravel/routes/web.php',
66+
17
67+
);
68+
}
69+
70+
public function testErrors()
71+
{
72+
$logger = m::mock(LogManager::class);
73+
$this->container->instance('log', $logger);
74+
$logger->shouldNotReceive('channel');
75+
$logger->shouldNotReceive('warning');
76+
77+
$this->expectException(ErrorException::class);
78+
$this->expectExceptionMessage('Something went wrong');
79+
80+
$this->handleError(
81+
E_ERROR,
82+
'Something went wrong',
83+
'/home/user/laravel/src/Providers/AppServiceProvider.php',
84+
17
85+
);
86+
}
87+
88+
public function testEnsuresDeprecationsDriver()
89+
{
90+
$logger = m::mock(LogManager::class);
91+
$this->container->instance('log', $logger);
92+
$logger->shouldReceive('channel')->andReturnSelf();
93+
$logger->shouldReceive('warning');
94+
95+
$this->config->set('logging.channels.stack', [
96+
'driver' => 'stack',
97+
'channels' => ['single'],
98+
'ignore_exceptions' => false,
99+
]);
100+
$this->config->set('logging.deprecations', 'stack');
101+
102+
$this->handleError(
103+
E_USER_DEPRECATED,
104+
'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated',
105+
'/home/user/laravel/routes/web.php',
106+
17
107+
);
108+
109+
$this->assertEquals(
110+
[
111+
'driver' => 'stack',
112+
'channels' => ['single'],
113+
'ignore_exceptions' => false,
114+
],
115+
$this->config->get('logging.channels.deprecations')
116+
);
117+
}
118+
119+
public function testEnsuresNullDeprecationsDriver()
120+
{
121+
$logger = m::mock(LogManager::class);
122+
$this->container->instance('log', $logger);
123+
$logger->shouldReceive('channel')->andReturnSelf();
124+
$logger->shouldReceive('warning');
125+
126+
$this->config->set('logging.channels.null', [
127+
'driver' => 'monolog',
128+
'handler' => NullHandler::class,
129+
]);
130+
131+
$this->handleError(
132+
E_USER_DEPRECATED,
133+
'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated',
134+
'/home/user/laravel/routes/web.php',
135+
17
136+
);
137+
138+
$this->assertEquals(
139+
NullHandler::class,
140+
$this->config->get('logging.channels.deprecations.handler')
141+
);
142+
}
143+
144+
public function testNoDeprecationsDriverIfNoDeprecationsHereSend()
145+
{
146+
$this->assertEquals(null, $this->config->get('logging.deprecations'));
147+
$this->assertEquals(null, $this->config->get('logging.channels.deprecations'));
148+
}
149+
150+
public function testIgnoreDeprecationIfLoggerUnresolvable()
151+
{
152+
$this->handleError(
153+
E_DEPRECATED,
154+
'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated',
155+
'/home/user/laravel/routes/web.php',
156+
17
157+
);
158+
}
159+
160+
protected function make($abstract, array $parameters = [])
161+
{
162+
return $this->container->make($abstract, $parameters);
163+
}
164+
}

0 commit comments

Comments
 (0)