Compare commits

...

22 Commits

Author SHA1 Message Date
Fabien Potencier
5eec1e0a1f Merge branch '2.4'
* 2.4:
  Check headers sent before sending PHP response
  Fix issue symfony/symfony#10345 '[FrameworkBundle][Console] container:debug --parameter="" not working anymore'
  Fixed ACE domain checks on UrlValidator (#10031)
  handle array root element
2014-03-26 19:07:49 +01:00
Nicolas Grekas
efb365b644 [Debug] sync with deprecation in DebugClassLoader 2014-03-21 10:31:50 +01:00
Nicolas Grekas
3d839f44bd [WebProfilerBundle] enhance logs display 2014-03-18 10:04:14 +01:00
Nicolas Grekas
cff0a2e884 [Debug] add a screaming mode to ErrorHandler 2014-03-17 14:44:56 +00:00
Fabien Potencier
8b0839bb30 Merge branch '2.4'
* 2.4:
  [Form][2.3] Fixes empty file-inputs getting treated as extra field.
  changed some PHPUnit assertions to more specific ones
  fixed Kernel::stripComments() normalizing new-lines
  added a BC comment
  Update FileLoader to fix issue #10339
  bumped Symfony version to 2.3.12
  updated VERSION for 2.3.11
  update CONTRIBUTORS for 2.3.11
  updated CHANGELOG for 2.3.11
  Throw exception when unable to normalize embedded object
  Fixed evaluation of short circuit operators
  Follow-up to #10312: Fixed minor performance related issues in Yaml\Inline.
  [2.4][HttpKernel] Fix issue #10209 When the profiler has `only_exception` option activated and a subrequest throw an exception, the parent profile cannot be found.
2014-03-03 13:53:01 +01:00
Andrew Moore
d0dc588a46 Create ExceptionHandlerInterface to allow third party exception handlers' to handle fatal errors 2014-02-28 09:48:11 -05:00
Nicolas Grekas
34a8ad8509 [Debug] enhance non-PSR-0 compatibility for case mismatch test 2014-02-24 14:11:02 +01:00
Nicolas Grekas
57ad6f754e [Debug] error stacking+fatal screaming+case testing 2014-02-16 11:08:14 +01:00
Fabien Potencier
cb1965864a Merge branch '2.4'
* 2.4:
  fixed various inconsistencies
  reduced recursion when building DumperPrefixCollection
  renamed variables - making next change more readable
  removing dead code.
  [ExpressionLanguage] added some tests for the built-in constant() function
  [ExpressionLanguage] added some documentation about functions
  [DomCrawler] Fixed filterXPath() chaining
  [DomCrawler] Fixed incorrect handling of image inputs
2014-02-11 14:52:21 +01:00
Fabien Potencier
429b394c16 Merge branch '2.4'
* 2.4:
  added missing @deprecated tag
  Fixed recursion level incrementing in FlattenException::flattenArgs().
  [HttpKernel] fixed wrong reference in TraceableEventDispatcher

Conflicts:
	src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php
2014-02-04 12:21:37 +01:00
Fabien Potencier
a2c111a569 [Debug] fixed case differences between PHP and HHVM (classes are case-insensitive anyway in PHP) 2014-02-03 11:14:27 +01:00
Fabien Potencier
c3cfefd38b [Debug] made order of suggestions predictable in error messages 2014-02-03 11:14:18 +01:00
Fabien Potencier
ffad875704 Merge branch '2.4'
* 2.4:
  udpated LICENSE year
  update year on licenses
  rundown and typo fix
  [Process] Fix #9861 : Revert TTY mode
  [Form] Update minimal requirement in composer.json
  Fix Empty translations with Qt files
  [Console] Fixed command name guessing if an alternative is an alias.
  Update UPGRADE-2.3.md to account for #9388
  [WebProfilerBundle] Fixed profiler toolbar icons for XHTML.
  [BrowserKit] Throw exception on invalid cookie expiration timestamp
  [Propel1Bridge][ModelChoiceList] add exception message for invalid classes
2014-01-07 14:29:57 +01:00
Fabien Potencier
ac63c60169 Merge branch '2.4'
* 2.4:
  removed unneeded use statements
  [DoctrineBridge] Fixed an issue with DoctrineParserCache
  removed unneeded use statements
  Prepend Child Bundle paths before the parent
  [Routing] add unit tests for Symfony\Component\Routing\RequestContext class
2014-01-01 10:16:05 +01:00
Fabien Potencier
5d95110638 Merge branch '2.4'
* 2.4:
  fixed typos
  Fixed deprecated method calls
  Add testing against HHVM at Travis-CI
  Fixed typo
  Fixed typo
  Fixed typo
  fixed acronyms
  [Validator] Fixed IBAN validator with 0750447346 value

Conflicts:
	src/Symfony/Component/HttpKernel/Debug/TraceableEventDispatcher.php
2013-12-28 22:40:48 +01:00
Fabien Potencier
dca6697bea Merge branch '2.4'
* 2.4:
  fixed CS
  fixed a typo
  fixed CS for lambdas
  [Yaml] fixed some license headers
  Fixes message value for objects
  Check for hour, minute & second validity
  avoid tables to have apparently long blank line breaks and be too far appart for long nested array params
  fixed various typos
  [Filesystem] Fixed mirror for symlinks
  [Validator] Removed duplicated test for IBAN in data provider
2013-12-28 09:12:58 +01:00
Grégoire Pineau
a1dd7dc6d7 [Debug] Added UndefinedMethodFatalErrorHandler 2013-12-16 11:52:16 +01:00
Fabien Potencier
9566ace63d Merge branch '2.4'
* 2.4:
  fixed CS
  fixed CS
2013-12-12 17:10:54 +01:00
Fabien Potencier
0000f11398 Merge branch '2.4'
* 2.4:
  [Debug] fixed tests
  ErrorHandlerTest: restore_error_handler() on assertion failure
  Fixed typo
  [validator] throw an exception if isn't an instance of ConstraintValidatorInterface.
  Reset the box model to content-box in the web debug toolbar
  [FrameworkBundle] Allowed "0" as a checkbox value in the php template.
  raising delta on stopwatch as travis really breaks with less than 35
  Switched to correct gender of "Token"
  bumped Symfony version to 2.4.1
  updated VERSION for 2.4.0
  updated CHANGELOG for 2.4.0
  fixed typos in several translations
  [HttpKernel] use static late binding when dumping out container

Conflicts:
	src/Symfony/Component/HttpKernel/Kernel.php
2013-12-12 17:07:18 +01:00
Fabien Potencier
b7682104fa Merge branch '2.4'
* 2.4:
  updated Composer suggested packages
  updated VERSION for 2.2.11
  update CONTRIBUTORS for 2.2.11
  updated CHANGELOG for 2.2.11
  Fixed typo in phpdoc
  Default form.csrf_protection.enabled to csrf_protection.enabled
  Handled the scenario when no entity manager is passed with closure query builder.
  Enabled csrf_protection by default if form.csrf_protection is enabled
  [HttpKernel] made a small optimization to Bundle initialization
  minor optimalization at bundle initialization
  [EventDispatcher] tweaked README
  removed observer pattern, in favour of mediator
  [DoctrineBridge] normalized class names in the ORM type guesser
  Fix `extract` method to avoid recalculating count() for each iteration.
  [Debug] ensured that a fatal PHP error is actually fatal after being handled by our error handler
  use the correct class name to retrieve mapped class' metadata and repository
  [WebProfilerBundle] Fixed js escaping in time.html.twig
2013-12-03 15:52:29 +01:00
Fabien Potencier
9e718c0517 Merge branch '2.4'
* 2.4:
  [Debug] fixed unit tests
  Avoid notice from being *eaten* by fatal error.
  Teardown used wrong property
  Modified guessDefaultEscapingStrategy to not escape txt templates
  Fix DateType for 32bits computers.
  Fixed the registration of validation.xml file when the form is disabled
  fixed lexing expression ending with spaces
  Fixes #9633, Removed dependency to Symfony\Bundle\FrameworkBundle\Tests\TestCase
  [Validator] Replaced inexistent interface.
  [HttpKernel] Fix profiler event-listener usage outside request stack context
  When getting the session's id, check if the session is not closed
  Fix undefined offset when formatting namespace suggestions
  Adjusting CacheClear Warmup method to namespaced kernels
2013-11-28 11:27:35 +01:00
Fabien Potencier
598ba50d67 updated version to 2.5 2013-11-24 21:17:07 +01:00
18 changed files with 595 additions and 126 deletions

View File

@@ -1,6 +1,11 @@
CHANGELOG
=========
2.5.0
-----
* added UndefinedMethodFatalErrorHandler
2.4.0
-----

View File

@@ -14,46 +14,67 @@ namespace Symfony\Component\Debug;
/**
* Autoloader checking if the class is really defined in the file found.
*
* The ClassLoader will wrap all registered autoloaders providing a
* findFile method and will throw an exception if a file is found but does
* The ClassLoader will wrap all registered autoloaders
* and will throw an exception if a file is found but does
* not declare the class.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Christophe Coevoet <stof@notk.org>
* @author Nicolas Grekas <p@tchwork.com>
*
* @api
*/
class DebugClassLoader
{
private $classFinder;
private $classLoader;
private $isFinder;
private $wasFinder;
/**
* Constructor.
*
* @param object $classFinder
* @param callable|object $classLoader
*
* @api
* @deprecated since 2.5, passing an object is deprecated and support for it will be removed in 3.0
*/
public function __construct($classFinder)
public function __construct($classLoader)
{
$this->classFinder = $classFinder;
$this->wasFinder = is_object($classLoader) && method_exists($classLoader, 'findFile');
if ($this->wasFinder) {
$this->classLoader = array($classLoader, 'loadClass');
$this->isFinder = true;
} else {
$this->classLoader = $classLoader;
$this->isFinder = is_array($classLoader) && method_exists($classLoader[0], 'findFile');
}
}
/**
* Gets the wrapped class loader.
*
* @return object a class loader instance
* @return callable|object a class loader
*
* @deprecated since 2.5, returning an object is deprecated and support for it will be removed in 3.0
*/
public function getClassLoader()
{
return $this->classFinder;
if ($this->wasFinder) {
return $this->classLoader[0];
} else {
return $this->classLoader;
}
}
/**
* Replaces all autoloaders implementing a findFile method by a DebugClassLoader wrapper.
* Wraps all autoloaders
*/
public static function enable()
{
// Ensures we don't hit https://bugs.php.net/42098
class_exists(__NAMESPACE__.'\ErrorHandler', true);
if (!is_array($functions = spl_autoload_functions())) {
return;
}
@@ -63,8 +84,8 @@ class DebugClassLoader
}
foreach ($functions as $function) {
if (is_array($function) && !$function[0] instanceof self && method_exists($function[0], 'findFile')) {
$function = array(new static($function[0]), 'loadClass');
if (!is_array($function) || !$function[0] instanceof self) {
$function = array(new static($function), 'loadClass');
}
spl_autoload_register($function);
@@ -86,7 +107,7 @@ class DebugClassLoader
foreach ($functions as $function) {
if (is_array($function) && $function[0] instanceof self) {
$function[0] = $function[0]->getClassLoader();
$function = $function[0]->getClassLoader();
}
spl_autoload_register($function);
@@ -99,10 +120,14 @@ class DebugClassLoader
* @param string $class A class name to resolve to file
*
* @return string|null
*
* @deprecated Deprecated since 2.5, to be removed in 3.0.
*/
public function findFile($class)
{
return $this->classFinder->findFile($class);
if ($this->wasFinder) {
return $this->classLoader[0]->findFile($class);
}
}
/**
@@ -116,10 +141,67 @@ class DebugClassLoader
*/
public function loadClass($class)
{
if ($file = $this->classFinder->findFile($class)) {
require $file;
ErrorHandler::stackErrors();
if (!class_exists($class, false) && !interface_exists($class, false) && (!function_exists('trait_exists') || !trait_exists($class, false))) {
try {
if ($this->isFinder) {
if ($file = $this->classLoader[0]->findFile($class)) {
require $file;
}
} else {
call_user_func($this->classLoader, $class);
$file = false;
}
} catch (\Exception $e) {
ErrorHandler::unstackErrors();
throw $e;
}
ErrorHandler::unstackErrors();
$exists = class_exists($class, false) || interface_exists($class, false) || (function_exists('trait_exists') && trait_exists($class, false));
if ($exists) {
$name = new \ReflectionClass($class);
$name = $name->getName();
if ($name !== $class) {
throw new \RuntimeException(sprintf('Case mismatch between loaded and declared class names: %s vs %s', $class, $name));
}
}
if ($file) {
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
$i = 0;
$tail = DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php';
$len = strlen($tail);
do {
$tail = substr($tail, $i);
$len -= $i;
if (0 === substr_compare($file, $tail, -$len, $len, true)) {
if (0 !== substr_compare($file, $tail, -$len, $len, false)) {
if (method_exists($this->classLoader[0], 'getClassMap')) {
$map = $this->classLoader[0]->getClassMap();
} else {
$map = array();
}
if (! isset($map[$class])) {
throw new \RuntimeException(sprintf('Case mismatch between class and source file names: %s vs %s', $class, $file));
}
}
break;
}
} while (false !== $i = strpos($tail, DIRECTORY_SEPARATOR, 1));
if (! $exists) {
if (false !== strpos($class, '/')) {
throw new \RuntimeException(sprintf('Trying to autoload a class with an invalid name "%s". Be careful that the namespace separator is "\" in PHP, not "/".', $class));
}

View File

@@ -11,11 +11,13 @@
namespace Symfony\Component\Debug;
use Psr\Log\LogLevel;
use Psr\Log\LoggerInterface;
use Symfony\Component\Debug\Exception\ContextErrorException;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\Exception\DummyException;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler;
use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler;
use Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface;
@@ -24,6 +26,7 @@ use Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface;
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Konstantin Myakshin <koc-dp@yandex.ru>
* @author Nicolas Grekas <p@tchwork.com>
*/
class ErrorHandler
{
@@ -42,7 +45,7 @@ class ErrorHandler
E_ERROR => 'Error',
E_CORE_ERROR => 'Core Error',
E_COMPILE_ERROR => 'Compile Error',
E_PARSE => 'Parse',
E_PARSE => 'Parse Error',
);
private $level;
@@ -56,6 +59,10 @@ class ErrorHandler
*/
private static $loggers = array();
private static $stackedErrors = array();
private static $stackedErrorLevels = array();
/**
* Registers the error handler.
*
@@ -102,7 +109,7 @@ class ErrorHandler
* Sets a logger for the given channel.
*
* @param LoggerInterface $logger A logger interface
* @param string $channel The channel associated with the logger (deprecation or emergency)
* @param string $channel The channel associated with the logger (deprecation, emergency or scream)
*/
public static function setLogger(LoggerInterface $logger, $channel = 'deprecation')
{
@@ -114,51 +121,48 @@ class ErrorHandler
*/
public function handle($level, $message, $file = 'unknown', $line = 0, $context = array())
{
if (0 === $this->level) {
return false;
}
if ($level & (E_USER_DEPRECATED | E_DEPRECATED)) {
if (isset(self::$loggers['deprecation'])) {
if (version_compare(PHP_VERSION, '5.4', '<')) {
$stack = array_map(
function ($row) {
unset($row['args']);
return $row;
},
array_slice(debug_backtrace(false), 0, 10)
);
if (self::$stackedErrorLevels) {
self::$stackedErrors[] = func_get_args();
} else {
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
}
if (version_compare(PHP_VERSION, '5.4', '<')) {
$stack = array_map(
function ($row) {
unset($row['args']);
self::$loggers['deprecation']->warning($message, array('type' => self::TYPE_DEPRECATION, 'stack' => $stack));
return $row;
},
array_slice(debug_backtrace(false), 0, 10)
);
} else {
$stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10);
}
self::$loggers['deprecation']->warning($message, array('type' => self::TYPE_DEPRECATION, 'stack' => $stack));
}
}
return true;
}
if ($this->displayErrors && error_reporting() & $level && $this->level & $level) {
// make sure the ContextErrorException class is loaded (https://bugs.php.net/bug.php?id=65322)
if (!class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
require __DIR__.'/Exception/ContextErrorException.php';
}
$exception = new ContextErrorException(sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line), 0, $level, $file, $line, $context);
// Exceptions thrown from error handlers are sometimes not caught by the exception
// handler, so we invoke it directly (https://bugs.php.net/bug.php?id=54275)
$exceptionHandler = set_exception_handler(function () {});
$exceptionHandler = set_exception_handler('var_dump');
restore_exception_handler();
if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandler) {
$exceptionHandler[0]->handle($exception);
if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandlerInterface) {
if (self::$stackedErrorLevels) {
self::$stackedErrors[] = func_get_args();
if (!class_exists('Symfony\Component\Debug\Exception\DummyException')) {
require __DIR__.'/Exception/DummyException.php';
return true;
}
$exception = sprintf('%s: %s in %s line %d', isset($this->levels[$level]) ? $this->levels[$level] : $level, $message, $file, $line);
$exception = new ContextErrorException($exception, 0, $level, $file, $line, $context);
$exceptionHandler[0]->handle($exception);
// we must stop the PHP script execution, as the exception has
// already been dealt with, so, let's throw an exception that
// will be caught by a dummy exception handler
@@ -175,16 +179,93 @@ class ErrorHandler
}
}
if (isset(self::$loggers['scream']) && !(error_reporting() & $level)) {
if (self::$stackedErrorLevels) {
self::$stackedErrors[] = func_get_args();
} else {
switch ($level) {
case E_USER_ERROR:
case E_RECOVERABLE_ERROR:
$logLevel = LogLevel::ERROR;
break;
case E_WARNING:
case E_USER_WARNING:
$logLevel = LogLevel::WARNING;
break;
default:
$logLevel = LogLevel::NOTICE;
break;
}
self::$loggers['scream']->log($logLevel, $message, array(
'type' => $level,
'file' => $file,
'line' => $line,
'scream' => error_reporting(),
));
}
}
return false;
}
/**
* Configure the error handler for delayed handling.
* Ensures also that non-catchable fatal errors are never silenced.
*
* As shown by http://bugs.php.net/42098 and http://bugs.php.net/60724
* PHP has a compile stage where it behaves unusually. To workaround it,
* we plug an error handler that only stacks errors for later.
*
* The most important feature of this is to prevent
* autoloading until unstackErrors() is called.
*/
public static function stackErrors()
{
self::$stackedErrorLevels[] = error_reporting(error_reporting() | E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR);
}
/**
* Unstacks stacked errors and forwards to the regular handler
*/
public static function unstackErrors()
{
$level = array_pop(self::$stackedErrorLevels);
if (null !== $level) {
error_reporting($level);
}
if (empty(self::$stackedErrorLevels)) {
$errors = self::$stackedErrors;
self::$stackedErrors = array();
$errorHandler = set_error_handler('var_dump');
restore_error_handler();
if ($errorHandler) {
foreach ($errors as $e) {
call_user_func_array($errorHandler, $e);
}
}
}
}
public function handleFatal()
{
if (null === $error = error_get_last()) {
$this->reservedMemory = '';
$error = error_get_last();
while (self::$stackedErrorLevels) {
static::unstackErrors();
}
if (null === $error) {
return;
}
$this->reservedMemory = '';
$type = $error['type'];
if (0 === $this->level || !in_array($type, array(E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE))) {
return;
@@ -197,7 +278,7 @@ class ErrorHandler
'line' => $error['line'],
);
self::$loggers['emergency']->emerg($error['message'], $fatal);
self::$loggers['emergency']->emergency($error['message'], $fatal);
}
if (!$this->displayErrors) {
@@ -205,10 +286,10 @@ class ErrorHandler
}
// get current exception handler
$exceptionHandler = set_exception_handler(function () {});
$exceptionHandler = set_exception_handler('var_dump');
restore_exception_handler();
if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandler) {
if (is_array($exceptionHandler) && $exceptionHandler[0] instanceof ExceptionHandlerInterface) {
$this->handleFatalError($exceptionHandler[0], $error);
}
}
@@ -224,11 +305,12 @@ class ErrorHandler
{
return array(
new UndefinedFunctionFatalErrorHandler(),
new UndefinedMethodFatalErrorHandler(),
new ClassNotFoundFatalErrorHandler(),
);
}
private function handleFatalError(ExceptionHandler $exceptionHandler, array $error)
private function handleFatalError(ExceptionHandlerInterface $exceptionHandler, array $error)
{
$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']);

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 Method Exception.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class UndefinedMethodException extends FatalErrorException
{
public function __construct($message, \ErrorException $previous)
{
parent::__construct($message, $previous->getCode(), $previous->getSeverity(), $previous->getFile(), $previous->getLine(), $previous->getPrevious());
}
}

View File

@@ -29,7 +29,7 @@ if (!defined('ENT_SUBSTITUTE')) {
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ExceptionHandler
class ExceptionHandler implements ExceptionHandlerInterface
{
private $debug;
private $charset;
@@ -57,14 +57,14 @@ class ExceptionHandler
}
/**
* {@inheritDoc}
*
* Sends a response for the given Exception.
*
* If you have the Symfony HttpFoundation component installed,
* this method will use it to create and send the response. If not,
* it will fallback to plain PHP functions.
*
* @param \Exception $exception An \Exception instance
*
* @see sendPhpResponse
* @see createResponse
*/

View File

@@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug;
/**
* An ExceptionHandler does something useful with an exception.
*
* @author Andrew Moore <me@andrewmoore.ca>
*/
interface ExceptionHandlerInterface
{
/**
* Handles an exception.
*
* @param \Exception $exception An \Exception instance
*/
public function handle(\Exception $exception);
}

View File

@@ -103,8 +103,17 @@ class ClassNotFoundFatalErrorHandler implements FatalErrorHandlerInterface
}
// get class loaders wrapped by DebugClassLoader
if ($function[0] instanceof DebugClassLoader && method_exists($function[0], 'getClassLoader')) {
$function[0] = $function[0]->getClassLoader();
if ($function[0] instanceof DebugClassLoader) {
$function = $function[0]->getClassLoader();
// Since 2.5, returning an object from DebugClassLoader::getClassLoader() is @deprecated
if (is_object($function)) {
$function = array($function);
}
if (!is_array($function)) {
continue;
}
}
if ($function[0] instanceof ComposerClassLoader || $function[0] instanceof SymfonyClassLoader) {

View File

@@ -80,6 +80,7 @@ class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface
}
if ($candidates) {
sort($candidates);
$message .= ' Did you mean to call: '.implode(', ', array_map(function ($val) {
return '"'.$val.'"';
}, $candidates)).'?';

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\FatalErrorHandler;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\Exception\UndefinedMethodException;
/**
* ErrorHandler for undefined methods.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface
{
/**
* {@inheritdoc}
*/
public function handleError(array $error, FatalErrorException $exception)
{
preg_match('/^Call to undefined method (.*)::(.*)\(\)$/', $error['message'], $matches);
if (!$matches) {
return;
}
$className = $matches[1];
$methodName = $matches[2];
$message = sprintf('Attempted to call method "%s" on class "%s" in %s line %d.', $methodName, $className, $error['file'], $error['line']);
$candidates = array();
foreach (get_class_methods($className) as $definedMethodName) {
$lev = levenshtein($methodName, $definedMethodName);
if ($lev <= strlen($methodName) / 3 || false !== strpos($definedMethodName, $methodName)) {
$candidates[] = $definedMethodName;
}
}
if ($candidates) {
sort($candidates);
$message .= sprintf(' Did you mean to call: "%s"?', implode('", "', $candidates));
}
return new UndefinedMethodException($message, $exception);
}
}

View File

@@ -12,46 +12,152 @@
namespace Symfony\Component\Debug\Tests;
use Symfony\Component\Debug\DebugClassLoader;
use Symfony\Component\Debug\ErrorHandler;
class DebugClassLoaderTest extends \PHPUnit_Framework_TestCase
{
/**
* @var int Error reporting level before running tests.
*/
private $errorReporting;
private $loader;
protected function setUp()
{
$this->errorReporting = error_reporting(E_ALL | E_STRICT);
$this->loader = new ClassLoader();
spl_autoload_register(array($this->loader, 'loadClass'));
DebugClassLoader::enable();
}
protected function tearDown()
{
DebugClassLoader::disable();
spl_autoload_unregister(array($this->loader, 'loadClass'));
error_reporting($this->errorReporting);
}
public function testIdempotence()
{
DebugClassLoader::enable();
DebugClassLoader::enable();
$functions = spl_autoload_functions();
foreach ($functions as $function) {
if (is_array($function) && $function[0] instanceof DebugClassLoader) {
$reflClass = new \ReflectionClass($function[0]);
$reflProp = $reflClass->getProperty('classFinder');
$reflProp = $reflClass->getProperty('classLoader');
$reflProp->setAccessible(true);
$this->assertNotInstanceOf('Symfony\Component\Debug\DebugClassLoader', $reflProp->getValue($function[0]));
DebugClassLoader::disable();
return;
}
}
DebugClassLoader::disable();
$this->fail('DebugClassLoader did not register');
}
public function testUnsilencing()
{
ob_start();
$bak = array(
ini_set('log_errors', 0),
ini_set('display_errors', 1),
);
// See below: this will fail with parse error
// but this should not be @-silenced.
@ class_exists(__NAMESPACE__.'\TestingUnsilencing', true);
ini_set('log_errors', $bak[0]);
ini_set('display_errors', $bak[1]);
$output = ob_get_clean();
$this->assertStringMatchesFormat('%aParse error%a', $output);
}
/**
* @expectedException \Symfony\Component\Debug\Exception\DummyException
*/
public function testStacking()
{
// the ContextErrorException must not be loaded to test the workaround
// for https://bugs.php.net/65322.
if (class_exists('Symfony\Component\Debug\Exception\ContextErrorException', false)) {
$this->markTestSkipped('The ContextErrorException class is already loaded.');
}
$exceptionHandler = $this->getMock('Symfony\Component\Debug\ExceptionHandler', array('handle'));
set_exception_handler(array($exceptionHandler, 'handle'));
$that = $this;
$exceptionCheck = function ($exception) use ($that) {
$that->assertInstanceOf('Symfony\Component\Debug\Exception\ContextErrorException', $exception);
$that->assertEquals(E_STRICT, $exception->getSeverity());
$that->assertStringStartsWith(__FILE__, $exception->getFile());
$that->assertRegexp('/^Runtime Notice: Declaration/', $exception->getMessage());
};
$exceptionHandler->expects($this->once())
->method('handle')
->will($this->returnCallback($exceptionCheck));
ErrorHandler::register();
try {
// Trigger autoloading + E_STRICT at compile time
// which in turn triggers $errorHandler->handle()
// that again triggers autoloading for ContextErrorException.
// Error stacking works around the bug above and everything is fine.
eval('
namespace '.__NAMESPACE__.';
class ChildTestingStacking extends TestingStacking { function foo($bar) {} }
');
} catch (\Exception $e) {
restore_error_handler();
restore_exception_handler();
throw $e;
}
restore_error_handler();
restore_exception_handler();
}
/**
* @expectedException \RuntimeException
*/
public function testNameCaseMismatch()
{
class_exists(__NAMESPACE__.'\TestingCaseMismatch', true);
}
/**
* @expectedException \RuntimeException
*/
public function testFileCaseMismatch()
{
class_exists(__NAMESPACE__.'\Fixtures\CaseMismatch', true);
}
/**
* @expectedException \RuntimeException
*/
public function testPsr4CaseMismatch()
{
class_exists(__NAMESPACE__.'\Fixtures\Psr4CaseMismatch', true);
}
public function testNotPsr0()
{
$this->assertTrue(class_exists(__NAMESPACE__.'\Fixtures\NotPSR0', true));
}
public function testNotPsr0Bis()
{
$this->assertTrue(class_exists(__NAMESPACE__.'\Fixtures\NotPSR0bis', true));
}
}
class ClassLoader
@@ -60,7 +166,27 @@ class ClassLoader
{
}
public function getClassMap()
{
return array(__NAMESPACE__.'\Fixtures\NotPSR0bis' => __DIR__ . '/Fixtures/notPsr0Bis.php');
}
public function findFile($class)
{
if (__NAMESPACE__.'\TestingUnsilencing' === $class) {
eval('-- parse error --');
} elseif (__NAMESPACE__.'\TestingStacking' === $class) {
eval('namespace '.__NAMESPACE__.'; class TestingStacking { function foo() {} }');
} elseif (__NAMESPACE__.'\TestingCaseMismatch' === $class) {
eval('namespace '.__NAMESPACE__.'; class TestingCaseMisMatch {}');
} elseif (__NAMESPACE__.'\Fixtures\CaseMismatch' === $class) {
return __DIR__ . '/Fixtures/casemismatch.php';
} elseif (__NAMESPACE__.'\Fixtures\Psr4CaseMismatch' === $class) {
return __DIR__ . '/Fixtures/psr4/Psr4CaseMismatch.php';
} elseif (__NAMESPACE__.'\Fixtures\NotPSR0' === $class) {
return __DIR__ . '/Fixtures/reallyNotPsr0.php';
} elseif (__NAMESPACE__.'\Fixtures\NotPSR0bis' === $class) {
return __DIR__ . '/Fixtures/notPsr0Bis.php';
}
}
}

View File

@@ -44,65 +44,6 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase
error_reporting($this->errorReporting);
}
public function testCompileTimeError()
{
// the ContextErrorException must not be loaded to test the workaround
// for https://bugs.php.net/bug.php?id=65322.
if (class_exists('Symfony\Component\Debug\Exception\ContextErrorException', false)) {
$this->markTestSkipped('The ContextErrorException class is already loaded.');
}
$exceptionHandler = $this->getMock('Symfony\Component\Debug\ExceptionHandler', array('handle'));
// the following code forces some PHPUnit classes to be loaded
// so that they will be available in the exception handler
// as they won't be autoloaded by PHP
class_exists('PHPUnit_Framework_MockObject_Invocation_Object');
$this->assertInstanceOf('stdClass', new \stdClass());
$this->assertEquals(1, 1);
$this->assertStringStartsWith('foo', 'foobar');
$this->assertArrayHasKey('bar', array('bar' => 'foo'));
$that = $this;
$exceptionCheck = function ($exception) use ($that) {
$that->assertInstanceOf('Symfony\Component\Debug\Exception\ContextErrorException', $exception);
$that->assertEquals(E_STRICT, $exception->getSeverity());
$that->assertEquals(2, $exception->getLine());
$that->assertStringStartsWith('Runtime Notice: Declaration of _CompileTimeError::foo() should be compatible with', $exception->getMessage());
$that->assertArrayHasKey('bar', $exception->getContext());
};
$exceptionHandler->expects($this->once())
->method('handle')
->will($this->returnCallback($exceptionCheck))
;
ErrorHandler::register();
set_exception_handler(array($exceptionHandler, 'handle'));
// dummy variable to check for in error handler.
$bar = 123;
// trigger compile time error
try {
eval(<<<'PHP'
class _BaseCompileTimeError { function foo() {} }
class _CompileTimeError extends _BaseCompileTimeError { function foo($invalid) {} }
PHP
);
} catch (DummyException $e) {
// if an exception is thrown, the test passed
} catch (\Exception $e) {
restore_error_handler();
restore_exception_handler();
throw $e;
}
restore_error_handler();
restore_exception_handler();
}
public function testNotice()
{
$exceptionHandler = $this->getMock('Symfony\Component\Debug\ExceptionHandler', array('handle'));
@@ -112,7 +53,6 @@ PHP
$exceptionCheck = function ($exception) use ($that) {
$that->assertInstanceOf('Symfony\Component\Debug\Exception\ContextErrorException', $exception);
$that->assertEquals(E_NOTICE, $exception->getSeverity());
$that->assertEquals(__LINE__ + 44, $exception->getLine());
$that->assertEquals(__FILE__, $exception->getFile());
$that->assertRegexp('/^Notice: Undefined variable: (foo|bar)/', $exception->getMessage());
$that->assertArrayHasKey('foobar', $exception->getContext());
@@ -234,6 +174,28 @@ PHP
$handler->handle(E_USER_DEPRECATED, 'foo', 'foo.php', 12, 'foo');
restore_error_handler();
$logger = $this->getMock('Psr\Log\LoggerInterface');
$that = $this;
$logArgCheck = function ($level, $message, $context) use ($that) {
$that->assertEquals('Undefined variable: undefVar', $message);
$that->assertArrayHasKey('type', $context);
$that->assertEquals($context['type'], E_NOTICE);
};
$logger
->expects($this->once())
->method('log')
->will($this->returnCallback($logArgCheck))
;
$handler = ErrorHandler::register(E_NOTICE);
$handler->setLogger($logger, 'scream');
unset($undefVar);
@$undefVar++;
restore_error_handler();
} catch (\Exception $e) {
restore_error_handler();
@@ -254,7 +216,8 @@ PHP
$m->invoke($handler, $exceptionHandler, $error);
$this->assertInstanceof($class, $exceptionHandler->e);
$this->assertSame($translatedMessage, $exceptionHandler->e->getMessage());
// class names are case insensitive and PHP/HHVM do not return the same
$this->assertSame(strtolower($translatedMessage), strtolower($exceptionHandler->e->getMessage()));
$this->assertSame($error['type'], $exceptionHandler->e->getSeverity());
$this->assertSame($error['file'], $exceptionHandler->e->getFile());
$this->assertSame($error['line'], $exceptionHandler->e->getLine());

View File

@@ -25,7 +25,8 @@ class UndefinedFunctionFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase
$exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line']));
$this->assertInstanceof('Symfony\Component\Debug\Exception\UndefinedFunctionException', $exception);
$this->assertSame($translatedMessage, $exception->getMessage());
// class names are case insensitive and PHP/HHVM do not return the same
$this->assertSame(strtolower($translatedMessage), strtolower($exception->getMessage()));
$this->assertSame($error['type'], $exception->getSeverity());
$this->assertSame($error['file'], $exception->getFile());
$this->assertSame($error['line'], $exception->getLine());

View File

@@ -0,0 +1,66 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Tests\FatalErrorHandler;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler;
class UndefinedMethodFatalErrorHandlerTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider provideUndefinedMethodData
*/
public function testUndefinedMethod($error, $translatedMessage)
{
$handler = new UndefinedMethodFatalErrorHandler();
$exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line']));
$this->assertInstanceof('Symfony\Component\Debug\Exception\UndefinedMethodException', $exception);
$this->assertSame($translatedMessage, $exception->getMessage());
$this->assertSame($error['type'], $exception->getSeverity());
$this->assertSame($error['file'], $exception->getFile());
$this->assertSame($error['line'], $exception->getLine());
}
public function provideUndefinedMethodData()
{
return array(
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => 'Call to undefined method SplObjectStorage::what()',
),
'Attempted to call method "what" on class "SplObjectStorage" in foo.php line 12.',
),
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => 'Call to undefined method SplObjectStorage::walid()',
),
'Attempted to call method "walid" on class "SplObjectStorage" in foo.php line 12. Did you mean to call: "valid"?',
),
array(
array(
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => 'Call to undefined method SplObjectStorage::offsetFet()',
),
'Attempted to call method "offsetFet" on class "SplObjectStorage" in foo.php line 12. Did you mean to call: "offsetGet", "offsetSet", "offsetUnset"?',
),
);
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
class CaseMismatch
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
class NotPSR0bis
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
class PSR4CaseMismatch
{
}

View File

@@ -0,0 +1,7 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
class NotPSR0
{
}

View File

@@ -33,7 +33,7 @@
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.4-dev"
"dev-master": "2.5-dev"
}
}
}