Chris@0: Chris@0: * Chris@0: * For the full copyright and license information, please view the LICENSE Chris@0: * file that was distributed with this source code. Chris@0: */ Chris@0: Chris@0: namespace Symfony\Component\Debug\FatalErrorHandler; Chris@0: Chris@17: use Composer\Autoload\ClassLoader as ComposerClassLoader; Chris@17: use Symfony\Component\ClassLoader\ClassLoader as SymfonyClassLoader; Chris@17: use Symfony\Component\Debug\DebugClassLoader; Chris@0: use Symfony\Component\Debug\Exception\ClassNotFoundException; Chris@0: use Symfony\Component\Debug\Exception\FatalErrorException; Chris@0: Chris@0: /** Chris@0: * ErrorHandler for classes that do not exist. Chris@0: * Chris@0: * @author Fabien Potencier Chris@0: */ Chris@0: class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface Chris@0: { Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function handleError(array $error, FatalErrorException $exception) Chris@0: { Chris@17: $messageLen = \strlen($error['message']); Chris@0: $notFoundSuffix = '\' not found'; Chris@17: $notFoundSuffixLen = \strlen($notFoundSuffix); Chris@0: if ($notFoundSuffixLen > $messageLen) { Chris@0: return; Chris@0: } Chris@0: Chris@0: if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { Chris@0: return; Chris@0: } Chris@0: Chris@17: foreach (['class', 'interface', 'trait'] as $typeName) { Chris@0: $prefix = ucfirst($typeName).' \''; Chris@17: $prefixLen = \strlen($prefix); Chris@0: if (0 !== strpos($error['message'], $prefix)) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: $fullyQualifiedClassName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); Chris@0: if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { Chris@0: $className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); Chris@0: $namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); Chris@0: $message = sprintf('Attempted to load %s "%s" from namespace "%s".', $typeName, $className, $namespacePrefix); Chris@0: $tail = ' for another namespace?'; Chris@0: } else { Chris@0: $className = $fullyQualifiedClassName; Chris@0: $message = sprintf('Attempted to load %s "%s" from the global namespace.', $typeName, $className); Chris@0: $tail = '?'; Chris@0: } Chris@0: Chris@0: if ($candidates = $this->getClassCandidates($className)) { Chris@0: $tail = array_pop($candidates).'"?'; Chris@0: if ($candidates) { Chris@0: $tail = ' for e.g. "'.implode('", "', $candidates).'" or "'.$tail; Chris@0: } else { Chris@0: $tail = ' for "'.$tail; Chris@0: } Chris@0: } Chris@0: $message .= "\nDid you forget a \"use\" statement".$tail; Chris@0: Chris@0: return new ClassNotFoundException($message, $exception); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tries to guess the full namespace for a given class name. Chris@0: * Chris@0: * By default, it looks for PSR-0 and PSR-4 classes registered via a Symfony or a Composer Chris@0: * autoloader (that should cover all common cases). Chris@0: * Chris@0: * @param string $class A class name (without its namespace) Chris@0: * Chris@0: * @return array An array of possible fully qualified class names Chris@0: */ Chris@0: private function getClassCandidates($class) Chris@0: { Chris@17: if (!\is_array($functions = spl_autoload_functions())) { Chris@17: return []; Chris@0: } Chris@0: Chris@0: // find Symfony and Composer autoloaders Chris@17: $classes = []; Chris@0: Chris@0: foreach ($functions as $function) { Chris@17: if (!\is_array($function)) { Chris@0: continue; Chris@0: } Chris@0: // get class loaders wrapped by DebugClassLoader Chris@0: if ($function[0] instanceof DebugClassLoader) { Chris@0: $function = $function[0]->getClassLoader(); Chris@0: Chris@17: if (!\is_array($function)) { Chris@0: continue; Chris@0: } Chris@0: } Chris@0: Chris@0: if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader) { Chris@0: foreach ($function[0]->getPrefixes() as $prefix => $paths) { Chris@0: foreach ($paths as $path) { Chris@0: $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); Chris@0: } Chris@0: } Chris@0: } Chris@0: if ($function[0] instanceof ComposerClassLoader) { Chris@0: foreach ($function[0]->getPrefixesPsr4() as $prefix => $paths) { Chris@0: foreach ($paths as $path) { Chris@0: $classes = array_merge($classes, $this->findClassInPath($path, $class, $prefix)); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: return array_unique($classes); Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param string $path Chris@0: * @param string $class Chris@0: * @param string $prefix Chris@0: * Chris@0: * @return array Chris@0: */ Chris@0: private function findClassInPath($path, $class, $prefix) Chris@0: { Chris@17: if (!$path = realpath($path.'/'.strtr($prefix, '\\_', '//')) ?: realpath($path.'/'.\dirname(strtr($prefix, '\\_', '//'))) ?: realpath($path)) { Chris@17: return []; Chris@0: } Chris@0: Chris@17: $classes = []; Chris@0: $filename = $class.'.php'; Chris@0: foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { Chris@0: if ($filename == $file->getFileName() && $class = $this->convertFileToClass($path, $file->getPathName(), $prefix)) { Chris@0: $classes[] = $class; Chris@0: } Chris@0: } Chris@0: Chris@0: return $classes; Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param string $path Chris@0: * @param string $file Chris@0: * @param string $prefix Chris@0: * Chris@0: * @return string|null Chris@0: */ Chris@0: private function convertFileToClass($path, $file, $prefix) Chris@0: { Chris@17: $candidates = [ Chris@0: // namespaced class Chris@17: $namespacedClass = str_replace([$path.\DIRECTORY_SEPARATOR, '.php', '/'], ['', '', '\\'], $file), Chris@0: // namespaced class (with target dir) Chris@0: $prefix.$namespacedClass, Chris@0: // namespaced class (with target dir and separator) Chris@0: $prefix.'\\'.$namespacedClass, Chris@0: // PEAR class Chris@0: str_replace('\\', '_', $namespacedClass), Chris@0: // PEAR class (with target dir) Chris@0: str_replace('\\', '_', $prefix.$namespacedClass), Chris@0: // PEAR class (with target dir and separator) Chris@0: str_replace('\\', '_', $prefix.'\\'.$namespacedClass), Chris@17: ]; Chris@0: Chris@0: if ($prefix) { Chris@0: $candidates = array_filter($candidates, function ($candidate) use ($prefix) { return 0 === strpos($candidate, $prefix); }); Chris@0: } Chris@0: Chris@0: // We cannot use the autoloader here as most of them use require; but if the class Chris@0: // is not found, the new autoloader call will require the file again leading to a Chris@0: // "cannot redeclare class" error. Chris@0: foreach ($candidates as $candidate) { Chris@0: if ($this->classExists($candidate)) { Chris@0: return $candidate; Chris@0: } Chris@0: } Chris@0: Chris@0: require_once $file; Chris@0: Chris@0: foreach ($candidates as $candidate) { Chris@0: if ($this->classExists($candidate)) { Chris@0: return $candidate; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * @param string $class Chris@0: * Chris@0: * @return bool Chris@0: */ Chris@0: private function classExists($class) Chris@0: { Chris@0: return class_exists($class, false) || interface_exists($class, false) || trait_exists($class, false); Chris@0: } Chris@0: }