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