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

Skip to content

[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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 153 additions & 3 deletions src/Symfony/Component/Debug/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -40,6 +42,11 @@ class ErrorHandler
E_PARSE => 'Parse',
);

private $classNameToUseStatementSuggestions = array(
'Request' => 'Symfony\Component\HttpFoundation\Request',
'Response' => 'Symfony\Component\HttpFoundation\Response',
Copy link
Member

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 ?

Copy link
Contributor Author

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 and Response 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:

  1. Build a static class map for all of the Symfony components that might be installed
  2. Do some analysis/poll for classes that people frequently forget to use and just expand a smaller curated list
  3. Dynamically build out a class map at runtime based on the current autoloader rules

I prefer the latter but it would be a lot of work so I started out with a simple map to get the conversation started.

Copy link
Contributor

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

Copy link
Contributor

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 using get_declared_classes, get_declared_interfaces, and get_declared_traits? It might still need one of your three semi-static common case ideas since Request and Response 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.

Copy link
Contributor Author

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. :)

Copy link
Contributor Author

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?

Copy link
Contributor

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

);

private $level;

private $reservedMemory;
Expand Down Expand Up @@ -150,15 +157,158 @@ public function handleFatal()
return;
}

$this->handleFatalError($error);
}

public function handleFatalError($error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it need to be public ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should only test the public interface. handleFatalError()'s behaviour should be tested through handleFatal().

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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. handleFatal gets its $error from get_last_error and there is no way that I know of to inject this dependency from the outside.

I added handleFatalError to split the code and allow for testing of the functionality after the error has been retrieved from get_last_error.

handleFatalError is currently implemented as a public method because I wanted to test it easily. I would have preferred this to be a private or protected method but it would have required me to hack the visibility in the test and I thought this was easier to get the conversations tarted.

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 handleFatal relies on get_last_error and I don't know to inject something there short of what I've already done.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
}
32 changes: 32 additions & 0 deletions src/Symfony/Component/Debug/Exception/ClassNotFoundException.php
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()
);
}
}
Loading