-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Debug] [WIP] Developer friendly Class Not Found and Undefined Function errors. #8156
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,8 +11,10 @@ | |
|
||
namespace Symfony\Component\Debug; | ||
|
||
use Symfony\Component\Debug\Exception\FatalErrorException; | ||
use Psr\Log\LoggerInterface; | ||
use Symfony\Component\Debug\Exception\ClassNotFoundException; | ||
use Symfony\Component\Debug\Exception\FatalErrorException; | ||
use Symfony\Component\Debug\Exception\UndefinedFunctionException; | ||
|
||
/** | ||
* ErrorHandler. | ||
|
@@ -40,6 +42,11 @@ class ErrorHandler | |
E_PARSE => 'Parse', | ||
); | ||
|
||
private $classNameToUseStatementSuggestions = array( | ||
'Request' => 'Symfony\Component\HttpFoundation\Request', | ||
'Response' => 'Symfony\Component\HttpFoundation\Response', | ||
); | ||
|
||
private $level; | ||
|
||
private $reservedMemory; | ||
|
@@ -150,15 +157,158 @@ public function handleFatal() | |
return; | ||
} | ||
|
||
$this->handleFatalError($error); | ||
} | ||
|
||
public function handleFatalError($error) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does it need to be public ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is only public now to facilitate testing. Protected or private would probably be better but it would require changing visibility in testing. I'm not opposed to the latter if we decide any of this makes sense. :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should only test the public interface. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I can tell it impossible to test as previously written. I added
I'm not super good at testing these kinds of things so other suggestions are more than welcome! The problem I see right now is that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, I think that keeping the method private and changing its visibility in a test is a better option (you can do it with reflection). I'd say it's better to "hack" in a test (as you called it), rather than open the visibility of a method. The production code, which is there just because it's needed in a test, smells. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd love more feedback on the overall idea and direction as opposed to discussing the visibility of this method any further. As I said right away I'm not opposed to changing the visibility but I am more interested in whether or not the general idea is worth pursuing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @simensen don't get me wrong. I love your idea, I think it's very useful since I often see newcomers having issues with solving this kind of problems. Your PR is definitely an improvement. I just don't want you to forget about that tiny detail of testing privates. I think we've said enough in this subject though. I'd try (if possible) to automatically guess a class namespace, just like @dpb587 suggested. |
||
{ | ||
// get current exception handler | ||
$exceptionHandler = set_exception_handler(function() {}); | ||
restore_exception_handler(); | ||
|
||
if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandler) { | ||
$level = isset($this->levels[$type]) ? $this->levels[$type] : $type; | ||
$level = isset($this->levels[$error['type']]) ? $this->levels[$error['type']] : $error['type']; | ||
$message = sprintf('%s: %s in %s line %d', $level, $error['message'], $error['file'], $error['line']); | ||
$exception = new FatalErrorException($message, 0, $type, $error['file'], $error['line']); | ||
$exception = new FatalErrorException($message, 0, $error['type'], $error['file'], $error['line']); | ||
|
||
if ($this->handleUndefinedFunctionError($exceptionHandler[0], $error, $exception)) { | ||
return; | ||
} | ||
|
||
if ($this->handleClassNotFoundError($exceptionHandler[0], $error, $exception)) { | ||
return; | ||
} | ||
|
||
$exceptionHandler[0]->handle($exception); | ||
} | ||
} | ||
|
||
private function handleUndefinedFunctionError($exceptionHandler, $error, $exception) | ||
{ | ||
$messageLen = strlen($error['message']); | ||
$notFoundSuffix = "()"; | ||
$notFoundSuffixLen = strlen($notFoundSuffix); | ||
if ($notFoundSuffixLen > $messageLen) { | ||
return false; | ||
} | ||
|
||
if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { | ||
return false; | ||
} | ||
|
||
$prefix = "Call to undefined function "; | ||
$prefixLen = strlen($prefix); | ||
if (0 !== strpos($error['message'], $prefix)) { | ||
return false; | ||
} | ||
|
||
$fullyQualifiedFunctionName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); | ||
if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedFunctionName, '\\')) { | ||
$functionName = substr($fullyQualifiedFunctionName, $namespaceSeparatorIndex + 1); | ||
$namespacePrefix = substr($fullyQualifiedFunctionName, 0, $namespaceSeparatorIndex); | ||
$message = sprintf( | ||
"Attempted to call function '%s' from namespace '%s' in %s line %d.", | ||
$functionName, | ||
$namespacePrefix, | ||
$error['file'], | ||
$error['line'] | ||
); | ||
} else { | ||
$functionName = $fullyQualifiedFunctionName; | ||
$message = sprintf( | ||
"Attempted to call function '%s' from the global namespace in %s line %d.", | ||
$functionName, | ||
$error['file'], | ||
$error['line'] | ||
); | ||
} | ||
|
||
$candidates = array(); | ||
foreach (get_defined_functions() as $type => $definedFunctionNames) { | ||
foreach ($definedFunctionNames as $definedFunctionName) { | ||
if (false !== $namespaceSeparatorIndex = strrpos($definedFunctionName, '\\')) { | ||
$definedFunctionNameBasename = substr($definedFunctionName, $namespaceSeparatorIndex + 1); | ||
} else { | ||
$definedFunctionNameBasename = $definedFunctionName; | ||
} | ||
|
||
if ($definedFunctionNameBasename === $functionName) { | ||
$candidates[] = '\\'.$definedFunctionName; | ||
} | ||
} | ||
} | ||
|
||
if ($candidates) { | ||
$message .= " Did you mean to call: " . implode(", ", array_map(function ($val) { | ||
return "'".$val."'"; | ||
}, $candidates)). "?"; | ||
} | ||
|
||
$exceptionHandler->handle(new UndefinedFunctionException( | ||
$message, | ||
$exception | ||
)); | ||
|
||
return true; | ||
} | ||
|
||
private function handleClassNotFoundError($exceptionHandler, $error, $exception) | ||
{ | ||
$messageLen = strlen($error['message']); | ||
$notFoundSuffix = "' not found"; | ||
$notFoundSuffixLen = strlen($notFoundSuffix); | ||
if ($notFoundSuffixLen > $messageLen) { | ||
return false; | ||
} | ||
|
||
if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) { | ||
return false; | ||
} | ||
|
||
foreach (array("class", "interface", "trait") as $typeName) { | ||
$prefix = ucfirst($typeName)." '"; | ||
$prefixLen = strlen($prefix); | ||
if (0 !== strpos($error['message'], $prefix)) { | ||
continue; | ||
} | ||
|
||
$fullyQualifiedClassName = substr($error['message'], $prefixLen, -$notFoundSuffixLen); | ||
if (false !== $namespaceSeparatorIndex = strrpos($fullyQualifiedClassName, '\\')) { | ||
$className = substr($fullyQualifiedClassName, $namespaceSeparatorIndex + 1); | ||
$namespacePrefix = substr($fullyQualifiedClassName, 0, $namespaceSeparatorIndex); | ||
$message = sprintf( | ||
"Attempted to load %s '%s' from namespace '%s' in %s line %d. Do you need to 'use' it from another namespace?", | ||
$typeName, | ||
$className, | ||
$namespacePrefix, | ||
$error['file'], | ||
$error['line'] | ||
); | ||
} else { | ||
$className = $fullyQualifiedClassName; | ||
$message = sprintf( | ||
"Attempted to load %s '%s' from the global namespace in %s line %d. Did you forget a use statement for this %s?", | ||
$typeName, | ||
$className, | ||
$error['file'], | ||
$error['line'], | ||
$typeName | ||
); | ||
} | ||
|
||
if (isset($this->classNameToUseStatementSuggestions[$className])) { | ||
$message .= sprintf( | ||
" Perhaps you need to add 'use %s' at the top of this file?", | ||
$this->classNameToUseStatementSuggestions[$className] | ||
); | ||
} | ||
|
||
$exceptionHandler->handle(new ClassNotFoundException( | ||
$message, | ||
$exception | ||
)); | ||
|
||
return true; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Debug\Exception; | ||
|
||
/** | ||
* Class (or Trait or Interface) Not Found Exception. | ||
* | ||
* @author Konstanton Myakshin <[email protected]> | ||
*/ | ||
class ClassNotFoundException extends \ErrorException | ||
{ | ||
public function __construct($message, \ErrorException $previous) | ||
{ | ||
parent::__construct( | ||
$message, | ||
$previous->getCode(), | ||
$previous->getSeverity(), | ||
$previous->getFile(), | ||
$previous->getLine(), | ||
$previous->getPrevious() | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <[email protected]> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\Debug\Exception; | ||
|
||
/** | ||
* Undefined Function Exception. | ||
* | ||
* @author Konstanton Myakshin <[email protected]> | ||
*/ | ||
class UndefinedFunctionException extends \ErrorException | ||
{ | ||
public function __construct($message, \ErrorException $previous) | ||
{ | ||
parent::__construct( | ||
$message, | ||
$previous->getCode(), | ||
$previous->getSeverity(), | ||
$previous->getFile(), | ||
$previous->getLine(), | ||
$previous->getPrevious() | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This map is totally arbitrary. Why would you include these classes and not others ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea proposed was to have some "common cases" accounted for. Things like
Request
andResponse
are easy to forget if you're building a new controller. There were a few other ideas that would have been far more heavy handed:use
and just expand a smaller curated listI prefer the latter but it would be a lot of work so I started out with a simple map to get the conversation started.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think @schmittjoh did some analysis as part of JMSDebugging
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or, similar to the usage of
get_defined_functions
, what about usingget_declared_classes
,get_declared_interfaces
, andget_declared_traits
? It might still need one of your three semi-static common case ideas sinceRequest
andResponse
aren't necessarily loaded by the time an error shows up.Also, thanks for your work - it will be much more convenient to copy paste a suggested fix than retyping it or tracking down the correct usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are good ideas. It will only work in the case that the classes, interfaces, and traits are loaded, but that is true for the functions stuff I already added. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lsmith77 What kind of research? Common incorrectly used (or missing
use
statement) classes? If so, that would be pretty useful in this case. :) Is it published anywhere?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
he collected data on exceptions