comparison vendor/symfony/debug/FatalErrorHandler/ClassNotFoundFatalErrorHandler.php @ 0:4c8ae668cc8c

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