1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the Symfony package. |
5
|
|
|
* |
6
|
|
|
* (c) Fabien Potencier <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Symfony\Component\Debug\FatalErrorHandler; |
13
|
|
|
|
14
|
|
|
use Symfony\Component\Debug\Exception\ClassNotFoundException; |
15
|
|
|
use Symfony\Component\Debug\Exception\FatalErrorException; |
16
|
|
|
use Symfony\Component\Debug\DebugClassLoader; |
17
|
|
|
use Composer\Autoload\ClassLoader as ComposerClassLoader; |
18
|
|
|
use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader; |
19
|
|
|
use Symfony\Component\ClassLoader\UniversalClassLoader as SymfonyUniversalClassLoader; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* ErrorHandler for classes that do not exist. |
23
|
|
|
* |
24
|
|
|
* @author Fabien Potencier <[email protected]> |
25
|
|
|
*/ |
26
|
|
|
class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface |
27
|
|
|
{ |
28
|
|
|
/** |
29
|
|
|
* {@inheritdoc} |
30
|
|
|
*/ |
31
|
|
|
public function handleError(array $error, FatalErrorException $exception) |
32
|
|
|
{ |
33
|
|
|
$messageLen = strlen($error['message']); |
34
|
|
|
$notFoundSuffix = '\' not found'; |
35
|
|
|
$notFoundSuffixLen = strlen($notFoundSuffix); |
36
|
|
|
if ($notFoundSuffixLen > $messageLen) { |
37
|
|
|
return; |
38
|
|
|
} |
39
|
|
|
|
40
|
|
|
if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { |
41
|
|
|
return; |
42
|
|
|
} |
43
|
|
|
|
44
|
|
|
foreach (array('class', 'interface', 'trait') as $typeName) { |
45
|
|
|
$prefix = ucfirst($typeName).' \''; |
46
|
|
|
$prefixLen = strlen($prefix); |
47
|
|
|
if (0 !== strpos($error['message'], $prefix)) { |
48
|
|
|
continue; |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
$fullyQualifiedClassName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); |
52
|
|
|
if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { |
53
|
|
|
$className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); |
54
|
|
|
$namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); |
55
|
|
|
$message = sprintf('Attempted to load %s "%s" from namespace "%s".', $typeName, $className, $namespacePrefix); |
56
|
|
|
$tail = ' for another namespace?'; |
57
|
|
|
} else { |
58
|
|
|
$className = $fullyQualifiedClassName; |
59
|
|
|
$message = sprintf('Attempted to load %s "%s" from the global namespace.', $typeName, $className); |
60
|
|
|
$tail = '?'; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
if ($candidates = $this->getClassCandidates($className)) { |
64
|
|
|
$tail = array_pop($candidates).'"?'; |
65
|
|
|
if ($candidates) { |
|
|
|
|
66
|
|
|
$tail = ' for e.g. "'.implode('", "', $candidates).'" or "'.$tail; |
67
|
|
|
} else { |
68
|
|
|
$tail = ' for "'.$tail; |
69
|
|
|
} |
70
|
|
|
} |
71
|
|
|
$message .= "\nDid you forget a \"use\" statement".$tail; |
72
|
|
|
|
73
|
|
|
return new ClassNotFoundException($message, $exception); |
74
|
|
|
} |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Tries to guess the full namespace for a given class name. |
79
|
|
|
* |
80
|
|
|
* By default, it looks for PSR-0 and PSR-4 classes registered via a Symfony or a Composer |
81
|
|
|
* autoloader (that should cover all common cases). |
82
|
|
|
* |
83
|
|
|
* @param string $class A class name (without its namespace) |
84
|
|
|
* |
85
|
|
|
* @return array An array of possible fully qualified class names |
86
|
|
|
*/ |
87
|
|
|
private function getClassCandidates($class) |
88
|
|
|
{ |
89
|
|
|
if (!is_array($functions = spl_autoload_functions())) { |
90
|
|
|
return array(); |
91
|
|
|
} |
92
|
|
|
|
93
|
|
|
// find Symfony and Composer autoloaders |
94
|
|
|
$classes = array(); |
95
|
|
|
|
96
|
|
|
foreach ($functions as $function) { |
97
|
|
|
if (!is_array($function)) { |
98
|
|
|
continue; |
99
|
|
|
} |
100
|
|
|
// get class loaders wrapped by DebugClassLoader |
101
|
|
|
if ($function[0] instanceof DebugClassLoader) { |
102
|
|
|
$function = $function[0]->getClassLoader(); |
103
|
|
|
|
104
|
|
|
// @deprecated since version 2.5. Returning an object from DebugClassLoader::getClassLoader() is deprecated. |
105
|
|
|
if (is_object($function)) { |
106
|
|
|
$function = array($function); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
if (!is_array($function)) { |
110
|
|
|
continue; |
111
|
|
|
} |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader || $function[0] instanceof SymfonyUniversalClassLoader) { |
|
|
|
|
115
|
|
View Code Duplication |
foreach ($function[0]->getPrefixes() as $prefix => $paths) { |
|
|
|
|
116
|
|
|
foreach ($paths as $path) { |
117
|
|
|
$classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
} |
121
|
|
|
if ($function[0] instanceof ComposerClassLoader) { |
122
|
|
View Code Duplication |
foreach ($function[0]->getPrefixesPsr4() as $prefix => $paths) { |
|
|
|
|
123
|
|
|
foreach ($paths as $path) { |
124
|
|
|
$classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); |
125
|
|
|
} |
126
|
|
|
} |
127
|
|
|
} |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
return array_unique($classes); |
131
|
|
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* @param string $path |
135
|
|
|
* @param string $class |
136
|
|
|
* @param string $prefix |
137
|
|
|
* |
138
|
|
|
* @return array |
139
|
|
|
*/ |
140
|
|
|
private function findClassInPath($path, $class, $prefix) |
141
|
|
|
{ |
142
|
|
|
if (!$path = realpath($path.'/'.strtr($prefix, '\\_', '//')) ?: realpath($path.'/'.dirname(strtr($prefix, '\\_', '//'))) ?: realpath($path)) { |
143
|
|
|
return array(); |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
$classes = array(); |
147
|
|
|
$filename = $class.'.php'; |
148
|
|
|
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { |
149
|
|
|
if ($filename == $file->getFileName() && $class = $this->convertFileToClass($path, $file->getPathName(), $prefix)) { |
150
|
|
|
$classes[] = $class; |
151
|
|
|
} |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
return $classes; |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* @param string $path |
159
|
|
|
* @param string $file |
160
|
|
|
* @param string $prefix |
161
|
|
|
* |
162
|
|
|
* @return string|null |
163
|
|
|
*/ |
164
|
|
|
private function convertFileToClass($path, $file, $prefix) |
165
|
|
|
{ |
166
|
|
|
$candidates = array( |
167
|
|
|
// namespaced class |
168
|
|
|
$namespacedClass = str_replace(array($path.DIRECTORY_SEPARATOR, '.php', '/'), array('', '', '\\'), $file), |
169
|
|
|
// namespaced class (with target dir) |
170
|
|
|
$prefix.$namespacedClass, |
171
|
|
|
// namespaced class (with target dir and separator) |
172
|
|
|
$prefix.'\\'.$namespacedClass, |
173
|
|
|
// PEAR class |
174
|
|
|
str_replace('\\', '_', $namespacedClass), |
175
|
|
|
// PEAR class (with target dir) |
176
|
|
|
str_replace('\\', '_', $prefix.$namespacedClass), |
177
|
|
|
// PEAR class (with target dir and separator) |
178
|
|
|
str_replace('\\', '_', $prefix.'\\'.$namespacedClass), |
179
|
|
|
); |
180
|
|
|
|
181
|
|
|
if ($prefix) { |
182
|
|
|
$candidates = array_filter($candidates, function ($candidate) use ($prefix) { return 0 === strpos($candidate, $prefix); }); |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
// We cannot use the autoloader here as most of them use require; but if the class |
186
|
|
|
// is not found, the new autoloader call will require the file again leading to a |
187
|
|
|
// "cannot redeclare class" error. |
188
|
|
|
foreach ($candidates as $candidate) { |
189
|
|
|
if ($this->classExists($candidate)) { |
190
|
|
|
return $candidate; |
191
|
|
|
} |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
require_once $file; |
195
|
|
|
|
196
|
|
|
foreach ($candidates as $candidate) { |
197
|
|
|
if ($this->classExists($candidate)) { |
198
|
|
|
return $candidate; |
199
|
|
|
} |
200
|
|
|
} |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* @param string $class |
205
|
|
|
* |
206
|
|
|
* @return bool |
207
|
|
|
*/ |
208
|
|
|
private function classExists($class) |
209
|
|
|
{ |
210
|
|
|
return class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false)); |
211
|
|
|
} |
212
|
|
|
} |
213
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.