8000 feature #10201 [Debug] error stacking + fatal screaming + case testin… · symfony/symfony@3e8f33a · GitHub
[go: up one dir, main page]

Skip to content

Commit 3e8f33a

Browse files
committed
feature #10201 [Debug] error stacking + fatal screaming + case testing (nicolas-grekas)
This PR was merged into the 2.5-dev branch. Discussion ---------- [Debug] error stacking + fatal screaming + case testing | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | yes | Tests pass? | yes | Fixed tickets | no | License | MIT | Doc PR | no Ported from https://github.com/nicolas-grekas/Patchwork/ Three enhancements for Symfony debug mode: - detect case mismatches between loaded class name, its declared name and its source file name - dismiss hard to debug blank pages related to non-catchable-@-silenced fatal errors (```@(new Toto) + parse error in Toto.php``` => enjoy debugging) - work around https://bugs.php.net/42098 / https://bugs.php.net/54054 / https://bugs.php.net/60149 / https://bugs.php.net/65322 (fixed in 5.5.5) An other thing I didn't port is scope isolation: the current `require` in autoloaders is done in their scope, so local $vars / $this (with access to private props/methods) is easily accessible from required files. Shouldn't proper separation prevent that? Commits ------- 6de362b [Debug] error stacking+fatal screaming+case testing
2 parents 838dc7e + 6de362b commit 3e8f33a

File tree

5 files changed

+276
-108
lines changed

5 files changed

+276
-108
lines changed

src/Symfony/Component/Debug/DebugClassLoader.php

Lines changed: 86 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,46 +14,67 @@
1414
/**
1515
* Autoloader checking if the class is really defined in the file found.
1616
*
17-
* The ClassLoader will wrap all registered autoloaders providing a
18-
* findFile method and will throw an exception if a file is found but does
17+
* The ClassLoader will wrap all registered autoloaders
18+
* and will throw an exception if a file is found but does
1919
* not declare the class.
2020
*
2121
* @author Fabien Potencier <fabien@symfony.com>
2222
* @author Christophe Coevoet <stof@notk.org>
23+
* @author Nicolas Grekas <p@tchwork.com>
2324
*
2425
* @api
2526
*/
2627
class DebugClassLoader
2728
{
28-
private $classFinder;
29+
private $classLoader;
30+
private $isFinder;
31+
private $wasFinder;
2932

3033
/**
3134
* Constructor.
3235
*
33-
* @param object $classFinder
36+
* @param callable|object $classLoader
3437
*
3538
* @api
39+
* @deprecated since 2.5, passing an object is deprecated and support for it will be removed in 3.0
3640
*/
37-
public function __construct($classFinder)
41+
public function __construct($classLoader)
3842
{
39-
$this->classFinder = $classFinder;
43+
$this->wasFinder = is_object($classLoader) && method_exists($classLoader, 'findFile');
44+
45+
if ($this->wasFinder) {
46+
$this->classLoader = array($classLoader, 'loadClass');
47+
$this->isFinder = true;
48+
} else {
49+
$this->classLoader = $classLoader;
50+
$this->isFinder = is_array($classLoader) && method_exists($classLoader[0], 'findFile');
51+
}
4052
}
4153

4254
/**
4355
* Gets the wrapped class loader.
4456
*
45-
* @return object a class loader instance
57+
* @return callable|object a class loader
58+
*
59+
* @deprecated since 2.5, returning an object is deprecated and support for it will be removed in 3.0
4660
*/
4761
public function getClassLoader()
4862
{
49-
return $this->classFinder;
63+
if ($this->wasFinder) {
64+
return $this->classLoader[0];
65+
} else {
66+
return $this->classLoader;
67+
}
5068
}
5169

5270
/**
53-
* Replaces all autoloaders implementing a findFile method by a DebugClassLoader wrapper.
71+
* Wraps all autoloaders
5472
*/
5573
public static function enable()
5674
{
75+
// Ensures we don't hit https://bugs.php.net/42098
76+
class_exists(__NAMESPACE__.'\ErrorHandler', true);
77+
5778
if (!is_array($functions = spl_autoload_functions())) {
5879
return;
5980
}
@@ -63,8 +84,8 @@ public static function enable()
6384
}
6485

6586
foreach ($functions as $function) {
66-
if (is_array($function) && !$function[0] instanceof self && method_exists($function[0], 'findFile')) {
67-
$function = array(new static($function[0]), 'loadClass');
87+
if (!is_array($function) || !$function[0] instanceof self) {
88+
$function = array(new static($function), 'loadClass');
6889
}
6990

7091
spl_autoload_register($function);
@@ -86,7 +107,7 @@ public static function disable()
86107

87108
foreach ($functions as $function) {
88109
if (is_array($function) && $function[0] instanceof self) {
89-
$function[0] = $function[0]->getClassLoader();
110+
$function = $function[0]->getClassLoader();
90111
}
91112

92113
spl_autoload_register($function);
@@ -99,10 +120,14 @@ public static function disable()
99120
* @param string $class A class name to resolve to file
100121
*
101122
* @return string|null
123+
*
124+
* @deprecated Deprecated since 2.5, to be removed in 3.0.
102125
*/
103126
public function findFile($class)
104127
{
105-
return $this->classFinder->findFile($class);
128+
if ($this->wasFinder) {
129+
return $this->classLoader[0]->findFile($class);
130+
}
106131
}
107132

108133
/**
@@ -116,10 +141,55 @@ public function findFile($class)
116141
*/
117142
public function loadClass($class)
118143
{
119-
if ($file = $this->classFinder->findFile($class)) {
120-
require $file;
144+
ErrorHandler::stackErrors();
145+
146+
try {
147+
if ($this->isFinder) {
148+
if ($file = $this->classLoader[0]->findFile($class)) {
149+
require $file;
150+
}
151+
} else {
152+
call_user_func($this->classLoader, $class);
153+
$file = false;
154+
}
155+
} catch (\Exception $e) {
156+
ErrorHandler::unstackErrors();
157+
158+
throw $e;
159+
}
160+
161+
ErrorHandler::unstackErrors();
162+
163+
$exists = class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false));
164+
165+
if ($exists) {
166+
$name = new \ReflectionClass($class);
167+
$name = $name->getName();
168+
169+
if ($name !== $class) {
170+
throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: %s vs %s', $class, $name));
171+
}
172+
}
173+
174+
if ($file) {
175+
if ('\\' == $class[0]) {
176+
$class = substr($class, 1);
177+
}
178+
179+
$i = -1;
180+
$tail = str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php';
181+
$len = strlen($tail);
182+
183+
do {
184+
$tail = substr($tail, $i+1);
185+
$len -= $i+1;
186+
187+
if (! substr_compare($file, $tail, -$len, $len, true) && substr_compare($file, $tail, -$len, $len, false)) {
188+
throw new \RuntimeException(sprintf('Case mismatch between class and source file names: %s vs %s', $class, $file));
189+
}
190+
} while (false !== $i = strpos($tail, '\\'));
121191

122-
if (!class_exists($class, false) && !interface_exists($class, false) && (!function_exists('trait_exists') || !trait_exists($class, false))) {
192+
if (! $exists) {
123193
if (false !== strpos($class, '/')) {
124194
throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class));
125195
}

src/Symfony/Component/Debug/ErrorHandler.php

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
*
2626
* @author Fabien Potencier <fabien@symfony.com>
2727
* @author Konstantin Myakshin <koc-dp@yandex.ru>
28+
* @author Nicolas Grekas <p@tchwork.com>
2829
*/
2930
class ErrorHandler
3031
{
@@ -57,6 +58,10 @@ class ErrorHandler
5758
*/
5859
private static $loggers = array();
5960

61+
private static $stackedErrors = array();
62+
63+
private static $stackedErrorLevels = array();
64+
6065
/**
6166
* Registers the error handler.
6267
*
@@ -121,45 +126,46 @@ public function handle($level, $message, $file = 'unknown', $line = 0, $context
121126

122127
if ($level & (E_USER_DEPRECATED | E_DEPRECATED)) {
123128
if (isset(self::$loggers['deprecation'])) {
124-
if (version_compare(PHP_VERSION, '5.4', '<')) {
125-
$stack = array_map(
126-
function ($row) {
127-
unset($row['args']);
128-
129-
return $row;
130-
},
131-
array_slice(debug_backtrace(false), 0, 10)
132-
);
129+
if (self::$stackedErrorLevels) {
130+
self::$stackedErrors[] = func_get_args();
133131
} else {
134-
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
135-
}
132+
if (version_compare(PHP_VERSION, '5.4', '<')) {
133+
$stack = array_map(
134+
function ($row) {
135+
unset($row['args']);
136+
137+
return $row;
138+
},
139+
array_slice(debug_backtrace(false), 0, 10)
140+
);
141+
} else {
142+
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
143+
}
136144

137-
self::$loggers['deprecation']->warning($message, array('type' => self::TYPE_DEPRECATION, 'stack' => $stack));
145+
self::$loggers['deprecation']->warning($message, array('type' => self::TYPE_DEPRECATION, 'stack' => $stack));
146+
}
138147
}
139148

140149
return true;
141150
}
142151

143152
if ($this->displayErrors && error_reporting() & $level && $this->level & $level) {
144-
// make sure the ContextErrorException class is loaded (https://bugs.php.net/bug.php?id=65322)
145-
if (!class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
146-
require __DIR__.'/Exception/ContextErrorException.php';
147-
}
148-
149-
$exception = new ContextErrorException(sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line), 0, $level, $file, $line, $context);
150-
151153
// Exceptions thrown from error handlers are sometimes not caught by the exception
152154
// handler, so we invoke it directly (https://bugs.php.net/bug.php?id=54275)
153-
$exceptionHandler = set_exception_handler(function () {});
155+
$exceptionHandler = set_exception_handler('var_dump');
154156
restore_exception_handler();
155157

156158
if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandler) {
157-
$exceptionHandler[0]->handle($exception);
159+
if (self::$stackedErrorLevels) {
160+
self::$stackedErrors[] = func_get_args();
158161

159-
if (!class_exists('Symfony\Component\Debug\Exception\DummyException')) {
160-
require __DIR__.'/Exception/DummyException.php';
162+
return true;
161163
}
162164

165+
$exception = sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line);
166+
$exception = new ContextErrorException($exception, 0, $level, $file, $line, $context);
167+
$exceptionHandler[0]->handle($exception);
168+
163169
// we must stop the PHP script execution, as the exception has
164170
// already been dealt with, so, let's throw an exception that
165171
// will be caught by a dummy exception handler
@@ -179,13 +185,61 @@ function ($row) {
179185
return false;
180186
}
181187

188+
/**
189+
* Configure the error handler for delayed handling.
190+
* Ensures also that non-catchable fatal errors are never silenced.
191+
*
192+
* As shown by http://bugs.php.net/42098 and http://bugs.php.net/60724
193+
* PHP has a compile stage where it behaves unusually. To workaround it,
194+
* we plug an error handler that only stacks errors for later.
195+
*
196+
* The most important feature of this is to prevent
197+
* autoloading until unstackErrors() is called.
198+
*/
199+
public static function stackErrors()
200+
{
201+
self::$stackedErrorLevels[] = error_reporting(error_reporting() | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR);
202+
}
203+
204+
/**
205+
* Unstacks stacked errors and forwards to the regular handler
206+
*/
207+
public static function unstackErrors()
208+
{
209+
$level = array_pop(self::$stackedErrorLevels);
210+
211+
if (null !== $level) {
212+
error_reporting($level);
213+
}
214+
215+
if (empty(self::$stackedErrorLevels)) {
216+
$errors = self::$stackedErrors;
217+
self::$stackedErrors = array();
218+
219+
$errorHandler = set_error_handler('var_dump');
220+
restore_error_handler();
221+
222+
if ($errorHandler) {
223+
foreach ($errors as $e) {
224+
call_user_func_array($errorHandler, $e);
225+
}
226+
}
227+
}
228+
}
229+
182230
public function handleFatal()
183231
{
184-
if (null === $error = error_get_last()) {
232+
$this->reservedMemory = '';
233+
$error = error_get_last();
234+
235+
while (self::$stackedErrorLevels) {
236+
static::unstackErrors();
237+
}
238+
239+
if (null === $error) {
185240
return;
186241
}
187242

188-
$this->reservedMemory = '';
189243
$type = $error['type'];
190244
if (0 === $this->level || !in_array($type, array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) {
191245
return;
@@ -206,7 +260,7 @@ public function handleFatal()
206260
}
207261

208262
// get current exception handler
209-
$exceptionHandler = set_exception_handler(function () {});
263+
$exceptionHandler = set_exception_handler('var_dump');
210264
restore_exception_handler();
211265

212266
if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandler) {

0 commit comments

Comments
 (0)
0