Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7786fe4

Browse files
committed
merged branch fabpot/class-not-found-exception (PR #8553)
This PR was merged into the master branch. Discussion ---------- [Debug] Developer friendly Class Not Found and Undefined Function errors This is a followup of #8156 | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #8156 | License | MIT | Doc PR | n/a Here is the original description from #8156: Per a discussion with @weaverryan and others, I took a crack at enhancing the exception display for class not found errors and undefined function errors. It is not the cleanest solution but this is a work in progress to see whether or not following this path makes sense. # Class Names ## Class Not Found: Unknown Class (Global Namespace) ```php <?php new WhizBangFactory(); ``` > Attempted to load class 'WhizBangFactory' from the global namespace in foo.php line 12. Did you forget a use statement for this class? ## Class Not Found: Unknown Class (Full Namespace) ```php <?php namespace Foo\Bar; new WhizBangFactory(); ``` > Attempted to load class 'WhizBangFactory' from namespace 'Foo\Bar' in foo.php line 12. Do you need to 'use' it from another namespace? ## Class Not Found: Well Known Class (Global Namespace) ```php <?php new Request(); ``` > Attempted to load class 'Request' from the global namespace in foo.php line 12. Did you forget a use statement for this class? Perhaps you need to add 'use Symfony\Component\HttpFoundation\Request' at the top of this file? ## Class Not Found: Well Known Class (Full Namespace) ```php <?php namespace Foo\Bar; new Request(); ``` > Attempted to load class 'Request' from namespace 'Foo\Bar' in foo.php line 12. Do you need to 'use' it from another namespace? Perhaps you need to add 'use Symfony\Component\HttpFoundation\Request' at the top of this file? # Functions ## Undefined Function (Global Namespace) ```php <?php // example.php: // namespace Acme\Example; // function test_namespaced_function() // { // } include "example.php"; test_namespaced_function() ``` > Attempted to call function 'test_namespaced_function' from the global namespace in foo.php line 12. Did you mean to call: '\acme\example\test_namespaced_function'? ## Undefined Function (Full Namespace) ```php <?php namespace Foo\Bar\Baz; // example.php: // namespace Acme\Example; // function test_namespaced_function() // { // } include "example.php"; test_namespaced_function() ``` > Attempted to call function 'test_namespaced_function' from namespace 'Foo\Bar\Baz' in foo.php line 12. Did you mean to call: '\acme\example\test_namespaced_function'? ## Undefined Function: Unknown Function (Global Namespace) ```php <?php test_namespaced_function() ``` > Attempted to call function 'test_namespaced_function' from the global namespace in foo.php line 12. ## Undefined Function: Unknown Function (Full Namespace) ```php <?php namespace Foo\Bar\Baz; test_namespaced_function() ``` > Attempted to call function 'test_namespaced_function' from namespace 'Foo\Bar\Baz' in foo.php line 12. Commits ------- bde67f0 fixed an error message 80e19e2 [Debug] added some missing phpdocs 968764b [Debug] refactored unit tests cefa1b5 [Debug] moved special fatal error handlers to their own classes 53ab284 [Debug] made Debug find FQCN automatically based on well-known autoloaders 208ca5f [Debug] made guessing of possible class names more flexible a0b1585 [Debug] fixed CS 6671945 Developer friendly Class Not Found and Undefined Function errors.
2 parents aa3e474 + bde67f0 commit 7786fe4

13 files changed

+655
-8
lines changed

src/Symfony/Component/ClassLoader/DebugClassLoader.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ public function __construct($classFinder)
3939
$this->classFinder = $classFinder;
4040
}
4141

42+
/**
43+
* Gets the wrapped class loader.
44+
*
45+
* @return object a class loader instance
46+
*/
47+
public function getClassLoader()
48+
{
49+
return $this->classFinder;
50+
}
51+
4252
/**
4353
* Replaces all autoloaders implementing a findFile method by a DebugClassLoader wrapper.
4454
*/

src/Symfony/Component/Debug/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
2.4.0
5+
-----
6+
7+
* improved error messages for not found classes and functions
8+
49
2.3.0
510
-----
611

src/Symfony/Component/Debug/ErrorHandler.php

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111

1212
namespace Symfony\Component\Debug;
1313

14-
use Symfony\Component\Debug\Exception\FatalErrorException;
15-
use Symfony\Component\Debug\Exception\ContextErrorException;
1614
use Psr\Log\LoggerInterface;
15+
use Symfony\Component\Debug\Exception\ContextErrorException;
16+
use Symfony\Component\Debug\Exception\FatalErrorException;
17+
use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler;
18+
use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler;
19+
use Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface;
1720

1821
/**
1922
* ErrorHandler.
@@ -55,7 +58,7 @@ class ErrorHandler
5558
/**
5659
* Registers the error handler.
5760
*
58-
* @param integer $level The level at which the conversion to Exception is done (null to use the error_reporting() value and 0 to disable)
61+
* @param integer $level The level at which the conversion to Exception is done (null to use the error_reporting() value and 0 to disable)
5962
* @param Boolean $displayErrors Display errors (for dev environment) or just log they (production usage)
6063
*
6164
* @return The registered error handler
@@ -74,16 +77,32 @@ public static function register($level = null, $displayErrors = true)
7477
return $handler;
7578
}
7679

80+
/**
81+
* Sets the level at which the conversion to Exception is done.
82+
*
83+
* @param integer|null $level The level (null to use the error_reporting() value and 0 to disable)
84+
*/
7785
public function setLevel($level)
7886
{
7987
$this->level = null === $level ? error_reporting() : $level;
8088
}
8189

90+
/**
91+
* Sets the display_errors flag value.
92+
*
93+
* @param integer $displayErrors The display_errors flag value
94+
*/
8295
public function setDisplayErrors($displayErrors)
8396
{
8497
$this->displayErrors = $displayErrors;
8598
}
8699

100+
/**
101+
* Sets a logger for the given channel.
102+
*
103+
* @param LoggerInterface $logger A logger interface
104+
* @param string $channel The channel associated with the logger (deprecation or emergency)
105+
*/
87106
public static function setLogger(LoggerInterface $logger, $channel = 'deprecation')
88107
{
89108
self::$loggers[$channel] = $logger;
@@ -157,10 +176,37 @@ public function handleFatal()
157176
restore_exception_handler();
158177

159178
if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandler) {
160-
$level = isset($this->levels[$type]) ? $this->levels[$type] : $type;
161-
$message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']);
162-
$exception = new FatalErrorException($message, 0, $type, $error['file'], $error['line']);
163-
$exceptionHandler[0]->handle($exception);
179+
$this->handleFatalError($exceptionHandler[0], $error);
164180
}
165181
}
182+
183+
/**
184+
* Gets the fatal error handlers.
185+
*
186+
* Override this method if you want to define more fatal error handlers.
187+
*
188+
* @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface
189+
*/
190+
protected function getFatalErrorHandlers()
191+
{
192+
return array(
193+
new UndefinedFunctionFatalErrorHandler(),
194+
new ClassNotFoundFatalErrorHandler(),
195+
);
196+
}
197+
198+
private function handleFatalError(ExceptionHandler $exceptionHandler, array $error)
199+
{
200+
$level = isset($this->levels[$error['type']]) ? $this->levels[$error['type']] : $error['type'];
201+
$message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']);
202+
$exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line']);
203+
204+
foreach ($this->getFatalErrorHandlers() as $handler) {
205+
if ($ex = $handler->handleError($error, $exception)) {
206+
return $exceptionHandler->handle($ex);
207+
}
208+
}
209+
210+
$exceptionHandler->handle($exception);
211+
}
166212
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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\Exception;
13+
14+
/**
15+
* Class (or Trait or Interface) Not Found Exception.
16+
*
17+
* @author Konstanton Myakshin <[email protected]>
18+
*/
19+
class ClassNotFoundException extends FatalErrorException
20+
{
21+
public function __construct($message, \ErrorException $previous)
22+
{
23+
parent::__construct(
24+
$message,
25+
$previous->getCode(),
26+
$previous->getSeverity(),
27+
$previous->getFile(),
28+
$previous->getLine(),
29+
$previous->getPrevious()
30+
);
31+
}
32+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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\Exception;
13+
14+
/**
15+
* Undefined Function Exception.
16+
*
17+
* @author Konstanton Myakshin <[email protected]>
18+
*/
19+
class UndefinedFunctionException extends FatalErrorException
20+
{
21+
public function __construct($message, \ErrorException $previous)
22+
{
23+
parent::__construct(
24+
$message,
25+
$previous->getCode(),
26+
$previous->getSeverity(),
27+
$previous->getFile(),
28+
$previous->getLine(),
29+
$previous->getPrevious()
30+
);
31+
}
32+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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 Composer\Autoload\ClassLoader as ComposerClassLoader;
17+
use Symfony\Component\ClassLoader as SymfonyClassLoader;
18+
use Symfony\Component\ClassLoader\DebugClassLoader;
19+
20+
/**
21+
* ErrorHandler for classes that do not exist.
22+
*
23+
* @author Fabien Potencier <[email protected]>
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(
55+
'Attempted to load %s "%s" from namespace "%s" in %s line %d. Do you need to "use" it from another namespace?',
56+
$typeName,
57+
$className,
58+
$namespacePrefix,
59+
$error['file'],
60+
$error['line']
61+
);
62+
} else {
63+
$className = $fullyQualifiedClassName;
64+
$message = sprintf(
65+
'Attempted to load %s "%s" from the global namespace in %s line %d. Did you forget a use statement for this %s?',
66+
$typeName,
67+
$className,
68+
$error['file'],
69+
$error['line'],
70+
$typeName
71+
);
72+
}
73+
74+
if ($classes = $this->getClassCandidates($className)) {
75+
$message .= sprintf(' Perhaps you need to add a use statement for one of the following: %s.', implode(', ', $classes));
76+
}
77+
78+
return new ClassNotFoundException($message, $exception);
79+
}
80+
}
81+
82+
/**
83+
* Tries to guess the full namespace for a given class name.
84+
*
85+
* By default, it looks for PSR-0 classes registered via a Symfony or a Composer
86+
* autoloader (that should cover all common cases).
87+
*
88+
* @param string $class A class name (without its namespace)
89+
*
90+
* @return array An array of possible fully qualified class names
91+
*/
92+
private function getClassCandidates($class)
93+
{
94+
if (!is_array($functions = spl_autoload_functions())) {
95+
return array();
96+
}
97+
98+
// find Symfony and Composer autoloaders
99+
$classes = array();
100+
foreach ($functions as $function) {
101+
if (!is_array($function)) {
102+
continue;
103+
}
104+
105+
// get class loaders wrapped by DebugClassLoader
106+
if ($function[0] instanceof DebugClassLoader && method_exists($function[0], 'getClassLoader')) {
107+
$function[0] = $function[0]->getClassLoader();
108+
}
109+
110+
if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader) {
111+
foreach ($function[0]->getPrefixes() as $paths) {
112+
foreach ($paths as $path) {
113+
$classes = array_merge($classes, $this->findClassInPath($function[0], $path, $class));
114+
}
115+
}
116+
}
117+
}
118+
119+
return $classes;
120+
}
121+
122+
private function findClassInPath($loader, $path, $class)
123+
{
124+
if (!$path = realpath($path)) {
125+
continue;
126+
}
127+
128+
$classes = array();
129+
$filename = $class.'.php';
130+
foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) {
131+
if ($filename == $file->getFileName() && $class = $this->convertFileToClass($loader, $path, $file->getPathName())) {
132+
$classes[] = $class;
133+
}
134+
}
135+
136+
return $classes;
137+
}
138+
139+
private function convertFileToClass($loader, $path, $file)
140+
{
141+
// We cannot use the autoloader here as most of them use require; but if the class
142+
// is not found, the new autoloader call will require the file again leading to a
143+
// "cannot redeclare class" error.
144+
require_once $file;
145+
146+
$file = str_replace(array($path.'/', '.php'), array('', ''), $file);
147+
148+
// is it a namespaced class?
149+
$class = str_replace('/', '\\', $file);
150+
if (class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false))) {
151+
return $class;
152+
}
153+
154+
// is it a PEAR-like class name instead?
155+
$class = str_replace('/', '_', $file);
156+
if (class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false))) {
157+
return $class;
158+
}
159+
}
160+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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\FatalErrorException;
15+
16+
/**
17+
* Attempts to convert fatal errors to exceptions.
18+
*
19+
* @author Fabien Potencier <[email protected]>
20+
*/
21+
interface FatalErrorHandlerInterface
22+
{
23+
/**
24+
* Attempts to convert an error into an exception.
25+
*
26+
* @param array $error An array as returned by error_get_last()
27+
* @param FatalErrorException $exception A FatalErrorException instance
28+
*
29+
* @return FatalErrorException|null A FatalErrorException instance if the class is able to convert the error, null otherwise
30+
*/
31+
public function handleError(array $error, FatalErrorException $exception);
32+
}

0 commit comments

Comments
 (0)