feature #31065 Add ErrorHandler component (yceruto)

This PR was merged into the 4.4 branch.

Discussion
----------

Add ErrorHandler component

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | no
| Fixed tickets | #25905, #26448
| License       | MIT
| Doc PR        | TODO

Mainly for API-based apps that don't require TwigBundle to get the correct exception response according to the request format (aka `_format` attribute).

![exception_response](https://user-images.githubusercontent.com/2028198/55509651-713dc700-562a-11e9-8b98-bef3b0229397.gif)

✔️ [RFC7807](https://tools.ietf.org/html/rfc7807) compliant for JSON and XML formats.

---

This introduce a new `ErrorRenderer` service that render a `FlattenException` into a given format:
```php
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorHandler\ErrorRenderer\JsonErrorRenderer;

$renderers = [
    new HtmlErrorRenderer(),
    new JsonErrorRenderer(),
    // ...
];
$errorRenderer = new ErrorRenderer($renderers);

return new Response(
    $errorRenderer->render($exception, $request->getRequestFormat()),
    $exception->getStatusCode(),
    $exception->getHeaders()
);
```

The built-in error renderers are:

| Format | Class |
| --- | --- |
| html | HtmlErrorRenderer |
| json | JsonErrorRenderer |
| xml, atom | XmlErrorRenderer |
| txt | TxtErrorRenderer |

And you can add your own error renderer by implementing the `ErrorRendererInterface` and tagging it with `error_handler.renderer` in your service definition.

Creating your own error renderer for a built-in format will end up replacing the related built-in error renderer.

Demo: https://github.com/yceruto/error-handler-app ([add custom error renderer](06fc647841))

Commits
-------

7057244890 Added ErrorHandler component
This commit is contained in:
Fabien Potencier 2019-06-27 14:40:37 +02:00
commit 13a5e2dd1f
105 changed files with 3547 additions and 2152 deletions

View File

@ -19,8 +19,8 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\ErrorHandler\Exception\FatalThrowableError;
use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\HttpKernel\KernelInterface;

View File

@ -152,6 +152,7 @@ class FrameworkExtension extends Extension
$loader->load('web.xml');
$loader->load('services.xml');
$loader->load('fragment_renderer.xml');
$loader->load('error_renderer.xml');
$container->registerAliasForArgument('parameter_bag', PsrContainerInterface::class);

View File

@ -29,10 +29,11 @@ use Symfony\Component\Cache\DependencyInjection\CachePoolPass;
use Symfony\Component\Cache\DependencyInjection\CachePoolPrunerPass;
use Symfony\Component\Config\Resource\ClassExistenceResource;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\ErrorHandler\DependencyInjection\ErrorHandlerPass;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\Form\DependencyInjection\FormPass;
use Symfony\Component\HttpFoundation\Request;
@ -90,6 +91,7 @@ class FrameworkBundle extends Bundle
KernelEvents::FINISH_REQUEST,
];
$this->addCompilerPassIfExists($container, ErrorHandlerPass::class);
$container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
$container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING);

View File

@ -19,9 +19,10 @@
<argument>null</argument><!-- Log levels map for enabled error levels -->
<argument>%debug.error_handler.throw_at%</argument>
<argument>%kernel.debug%</argument>
<argument type="service" id="debug.file_link_formatter"></argument>
<argument type="service" id="debug.file_link_formatter" />
<argument>%kernel.debug%</argument>
<argument>%kernel.charset%</argument>
<argument type="service" id="error_handler.error_renderer" on-invalid="null" />
</service>
<service id="debug.file_link_formatter" class="Symfony\Component\HttpKernel\Debug\FileLinkFormatter">

View File

@ -0,0 +1,39 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults public="false" />
<service id="error_handler.error_renderer" class="Symfony\Component\ErrorHandler\DependencyInjection\ErrorRenderer">
<argument /> <!-- error renderer locator -->
</service>
<service id="error_handler.renderer.html" class="Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer">
<tag name="error_handler.renderer" />
<argument>%kernel.debug%</argument>
<argument>%kernel.charset%</argument>
<argument>%debug.file_link_format%</argument>
</service>
<service id="error_handler.renderer.json" class="Symfony\Component\ErrorHandler\ErrorRenderer\JsonErrorRenderer">
<tag name="error_handler.renderer" />
<argument>%kernel.debug%</argument>
</service>
<service id="error_handler.renderer.xml" class="Symfony\Component\ErrorHandler\ErrorRenderer\XmlErrorRenderer">
<tag name="error_handler.renderer" format="atom" />
<tag name="error_handler.renderer" />
<argument>%kernel.debug%</argument>
<argument>%kernel.charset%</argument>
</service>
<service id="error_handler.renderer.txt" class="Symfony\Component\ErrorHandler\ErrorRenderer\TxtErrorRenderer">
<tag name="error_handler.renderer" />
<argument>%kernel.debug%</argument>
<argument>%kernel.charset%</argument>
</service>
</services>
</container>

View File

@ -22,7 +22,7 @@
"symfony/config": "^4.2|^5.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/http-foundation": "^4.3|^5.0",
"symfony/http-kernel": "^4.3|^5.0",
"symfony/http-kernel": "^4.4|^5.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/filesystem": "^3.4|^4.0|^5.0",
"symfony/finder": "^3.4|^4.0|^5.0",
@ -69,6 +69,7 @@
"symfony/asset": "<3.4",
"symfony/browser-kit": "<4.3",
"symfony/console": "<4.3",
"symfony/debug": "<4.4",
"symfony/dotenv": "<4.2",
"symfony/dom-crawler": "<4.3",
"symfony/form": "<4.3",

View File

@ -11,7 +11,7 @@
namespace Symfony\Bundle\TwigBundle\Controller;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;

View File

@ -11,7 +11,7 @@
namespace Symfony\Bundle\TwigBundle\Controller;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

View File

@ -28,7 +28,7 @@ class ExceptionListenerPass implements CompilerPassInterface
}
// register the exception controller only if Twig is enabled and required dependencies do exist
if (!class_exists('Symfony\Component\Debug\Exception\FlattenException') || !interface_exists('Symfony\Component\EventDispatcher\EventSubscriberInterface')) {
if (!class_exists('Symfony\Component\ErrorHandler\Exception\FlattenException') || !interface_exists('Symfony\Component\EventDispatcher\EventSubscriberInterface')) {
$container->removeDefinition('twig.exception_listener');
} elseif ($container->hasParameter('templating.engines')) {
$engines = $container->getParameter('templating.engines');

View File

@ -13,7 +13,7 @@ namespace Symfony\Bundle\TwigBundle\Tests\Controller;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Bundle\TwigBundle\Tests\TestCase;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Twig\Environment;
use Twig\Loader\ArrayLoader;

View File

@ -13,7 +13,7 @@ namespace Symfony\Bundle\TwigBundle\Tests\Controller;
use Symfony\Bundle\TwigBundle\Controller\PreviewErrorController;
use Symfony\Bundle\TwigBundle\Tests\TestCase;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;

View File

@ -20,7 +20,7 @@
"symfony/config": "^4.2|^5.0",
"symfony/twig-bridge": "^4.4|^5.0",
"symfony/http-foundation": "^4.3|^5.0",
"symfony/http-kernel": "^4.1|^5.0",
"symfony/http-kernel": "^4.4|^5.0",
"symfony/polyfill-ctype": "~1.8",
"twig/twig": "~1.41|~2.10"
},

View File

@ -11,7 +11,7 @@
namespace Symfony\Bundle\WebProfilerBundle\Controller;
use Symfony\Component\Debug\ExceptionHandler;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -30,14 +30,18 @@ class ExceptionController
protected $twig;
protected $debug;
protected $profiler;
private $fileLinkFormat;
private $errorRenderer;
public function __construct(Profiler $profiler = null, Environment $twig, bool $debug, FileLinkFormatter $fileLinkFormat = null)
public function __construct(Profiler $profiler = null, Environment $twig, bool $debug, FileLinkFormatter $fileLinkFormat = null, HtmlErrorRenderer $errorRenderer = null)
{
$this->profiler = $profiler;
$this->twig = $twig;
$this->debug = $debug;
$this->fileLinkFormat = $fileLinkFormat;
$this->errorRenderer = $errorRenderer;
if (null === $errorRenderer) {
$this->errorRenderer = new HtmlErrorRenderer($debug, $this->twig->getCharset(), $fileLinkFormat);
}
}
/**
@ -61,9 +65,7 @@ class ExceptionController
$template = $this->getTemplate();
if (!$this->twig->getLoader()->exists($template)) {
$handler = new ExceptionHandler($this->debug, $this->twig->getCharset(), $this->fileLinkFormat);
return new Response($handler->getContent($exception), 200, ['Content-Type' => 'text/html']);
return new Response($this->errorRenderer->getBody($exception), 200, ['Content-Type' => 'text/html']);
}
$code = $exception->getStatusCode();
@ -97,13 +99,10 @@ class ExceptionController
$this->profiler->disable();
$exception = $this->profiler->loadProfile($token)->getCollector('exception')->getException();
$template = $this->getTemplate();
if (!$this->templateExists($template)) {
$handler = new ExceptionHandler($this->debug, $this->twig->getCharset(), $this->fileLinkFormat);
return new Response($handler->getStylesheet($exception), 200, ['Content-Type' => 'text/css']);
return new Response($this->errorRenderer->getStylesheet(), 200, ['Content-Type' => 'text/css']);
}
return new Response($this->twig->render('@WebProfiler/Collector/exception.css.twig'), 200, ['Content-Type' => 'text/css']);

View File

@ -27,6 +27,7 @@
<argument type="service" id="twig" />
<argument>%kernel.debug%</argument>
<argument type="service" id="debug.file_link_formatter" />
<argument type="service" id="error_handler.renderer.html" on-invalid="null" />
</service>
<service id="web_profiler.csp.handler" class="Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler">

View File

@ -17,6 +17,7 @@ use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\EventDispatcher\EventDispatcher;
@ -53,6 +54,7 @@ class WebProfilerExtensionTest extends TestCase
$this->kernel = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\KernelInterface')->getMock();
$this->container = new ContainerBuilder();
$this->container->register('error_handler.renderer.html', HtmlErrorRenderer::class);
$this->container->register('event_dispatcher', EventDispatcher::class)->setPublic(true);
$this->container->register('router', $this->getMockClass('Symfony\\Component\\Routing\\RouterInterface'))->setPublic(true);
$this->container->register('twig', 'Twig\Environment')->setPublic(true);

View File

@ -18,7 +18,7 @@
"require": {
"php": "^7.1.3",
"symfony/config": "^4.2|^5.0",
"symfony/http-kernel": "^4.3",
"symfony/http-kernel": "^4.4",
"symfony/routing": "^3.4|^4.0|^5.0",
"symfony/twig-bundle": "^4.2|^5.0",
"symfony/var-dumper": "^3.4|^4.0|^5.0",

View File

@ -41,8 +41,8 @@ use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\ErrorHandler\Exception\FatalThrowableError;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy;

View File

@ -11,27 +11,13 @@
namespace Symfony\Component\Debug;
use Psr\Log\AbstractLogger;
use Symfony\Component\ErrorHandler\BufferingLogger as BaseBufferingLogger;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', BufferingLogger::class, BaseBufferingLogger::class), E_USER_DEPRECATED);
/**
* A buffering logger that stacks logs for later.
*
* @author Nicolas Grekas <p@tchwork.com>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\BufferingLogger instead.
*/
class BufferingLogger extends AbstractLogger
class BufferingLogger extends BaseBufferingLogger
{
private $logs = [];
public function log($level, $message, array $context = [])
{
$this->logs[] = [$level, $message, $context];
}
public function cleanLogs()
{
$logs = $this->logs;
$this->logs = [];
return $logs;
}
}

View File

@ -1,6 +1,18 @@
CHANGELOG
=========
4.4.0
-----
* deprecated the `BufferingLogger`, `ErrorHandler` and `ExceptionHandler` classes,
they have been moved to the `ErrorHandler` component
* deprecated the `FatalErrorHandlerInterface`, `ClassNotFoundFatalErrorHandler`,
`UndefinedFunctionFatalErrorHandler` and `UndefinedMethodFatalErrorHandler` classes,
they have been moved to the `ErrorHandler` component
* deprecated the `ClassNotFoundException`, `FatalErrorException`, `FatalThrowableError`,
`FlattenException`, `OutOfMemoryException`, `SilencedErrorContext`, `UndefinedFunctionException`,
and `UndefinedMethodException`, they have been moved to the `ErrorHandler` component
4.3.0
-----

View File

@ -11,6 +11,10 @@
namespace Symfony\Component\Debug;
use Symfony\Component\ErrorHandler\BufferingLogger;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\ErrorHandler\ExceptionHandler;
/**
* Registers all the debug tools.
*

View File

@ -86,7 +86,7 @@ class DebugClassLoader
public static function enable()
{
// Ensures we don't hit https://bugs.php.net/42098
class_exists('Symfony\Component\Debug\ErrorHandler');
class_exists('Symfony\Component\ErrorHandler\ErrorHandler');
class_exists('Psr\Log\LogLevel');
if (!\is_array($functions = spl_autoload_functions())) {

View File

@ -11,700 +11,13 @@
namespace Symfony\Component\Debug;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\Debug\Exception\OutOfMemoryException;
use Symfony\Component\Debug\Exception\SilencedErrorContext;
use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler;
use Symfony\Component\Debug\FatalErrorHandler\FatalErrorHandlerInterface;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler;
use Symfony\Component\ErrorHandler\ErrorHandler as BaseErrorHandler;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ErrorHandler::class, BaseErrorHandler::class), E_USER_DEPRECATED);
/**
* A generic ErrorHandler for the PHP engine.
*
* Provides five bit fields that control how errors are handled:
* - thrownErrors: errors thrown as \ErrorException
* - loggedErrors: logged errors, when not @-silenced
* - scopedErrors: errors thrown or logged with their local context
* - tracedErrors: errors logged with their stack trace
* - screamedErrors: never @-silenced errors
*
* Each error level can be logged by a dedicated PSR-3 logger object.
* Screaming only applies to logging.
* Throwing takes precedence over logging.
* Uncaught exceptions are logged as E_ERROR.
* E_DEPRECATED and E_USER_DEPRECATED levels never throw.
* E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw.
* Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so.
* As errors have a performance cost, repeated errors are all logged, so that the developer
* can see them and weight them as more important to fix than others of the same level.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*
* @final since Symfony 4.3
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\ErrorHandler instead.
*/
class ErrorHandler
class ErrorHandler extends BaseErrorHandler
{
private $levels = [
E_DEPRECATED => 'Deprecated',
E_USER_DEPRECATED => 'User Deprecated',
E_NOTICE => 'Notice',
E_USER_NOTICE => 'User Notice',
E_STRICT => 'Runtime Notice',
E_WARNING => 'Warning',
E_USER_WARNING => 'User Warning',
E_COMPILE_WARNING => 'Compile Warning',
E_CORE_WARNING => 'Core Warning',
E_USER_ERROR => 'User Error',
E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
E_COMPILE_ERROR => 'Compile Error',
E_PARSE => 'Parse Error',
E_ERROR => 'Error',
E_CORE_ERROR => 'Core Error',
];
private $loggers = [
E_DEPRECATED => [null, LogLevel::INFO],
E_USER_DEPRECATED => [null, LogLevel::INFO],
E_NOTICE => [null, LogLevel::WARNING],
E_USER_NOTICE => [null, LogLevel::WARNING],
E_STRICT => [null, LogLevel::WARNING],
E_WARNING => [null, LogLevel::WARNING],
E_USER_WARNING => [null, LogLevel::WARNING],
E_COMPILE_WARNING => [null, LogLevel::WARNING],
E_CORE_WARNING => [null, LogLevel::WARNING],
E_USER_ERROR => [null, LogLevel::CRITICAL],
E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL],
E_COMPILE_ERROR => [null, LogLevel::CRITICAL],
E_PARSE => [null, LogLevel::CRITICAL],
E_ERROR => [null, LogLevel::CRITICAL],
E_CORE_ERROR => [null, LogLevel::CRITICAL],
];
private $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED
private $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED
private $tracedErrors = 0x77FB; // E_ALL - E_STRICT - E_PARSE
private $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE
private $loggedErrors = 0;
private $traceReflector;
private $isRecursive = 0;
private $isRoot = false;
private $exceptionHandler;
private $bootstrappingLogger;
private static $reservedMemory;
private static $toStringException = null;
private static $silencedErrorCache = [];
private static $silencedErrorCount = 0;
private static $exitCode = 0;
/**
* Registers the error handler.
*
* @param self|null $handler The handler to register
* @param bool $replace Whether to replace or not any existing handler
*
* @return self The registered error handler
*/
public static function register(self $handler = null, $replace = true)
{
if (null === self::$reservedMemory) {
self::$reservedMemory = str_repeat('x', 10240);
register_shutdown_function(__CLASS__.'::handleFatalError');
}
if ($handlerIsNew = null === $handler) {
$handler = new static();
}
if (null === $prev = set_error_handler([$handler, 'handleError'])) {
restore_error_handler();
// Specifying the error types earlier would expose us to https://bugs.php.net/63206
set_error_handler([$handler, 'handleError'], $handler->thrownErrors | $handler->loggedErrors);
$handler->isRoot = true;
}
if ($handlerIsNew && \is_array($prev) && $prev[0] instanceof self) {
$handler = $prev[0];
$replace = false;
}
if (!$replace && $prev) {
restore_error_handler();
$handlerIsRegistered = \is_array($prev) && $handler === $prev[0];
} else {
$handlerIsRegistered = true;
}
if (\is_array($prev = set_exception_handler([$handler, 'handleException'])) && $prev[0] instanceof self) {
restore_exception_handler();
if (!$handlerIsRegistered) {
$handler = $prev[0];
} elseif ($handler !== $prev[0] && $replace) {
set_exception_handler([$handler, 'handleException']);
$p = $prev[0]->setExceptionHandler(null);
$handler->setExceptionHandler($p);
$prev[0]->setExceptionHandler($p);
}
} else {
$handler->setExceptionHandler($prev);
}
$handler->throwAt(E_ALL & $handler->thrownErrors, true);
return $handler;
}
public function __construct(BufferingLogger $bootstrappingLogger = null)
{
if ($bootstrappingLogger) {
$this->bootstrappingLogger = $bootstrappingLogger;
$this->setDefaultLogger($bootstrappingLogger);
}
$this->traceReflector = new \ReflectionProperty('Exception', 'trace');
$this->traceReflector->setAccessible(true);
}
/**
* Sets a logger to non assigned errors levels.
*
* @param LoggerInterface $logger A PSR-3 logger to put as default for the given levels
* @param array|int $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants
* @param bool $replace Whether to replace or not any existing logger
*/
public function setDefaultLogger(LoggerInterface $logger, $levels = E_ALL, $replace = false)
{
$loggers = [];
if (\is_array($levels)) {
foreach ($levels as $type => $logLevel) {
if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) {
$loggers[$type] = [$logger, $logLevel];
}
}
} else {
if (null === $levels) {
$levels = E_ALL;
}
foreach ($this->loggers as $type => $log) {
if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) {
$log[0] = $logger;
$loggers[$type] = $log;
}
}
}
$this->setLoggers($loggers);
}
/**
* Sets a logger for each error level.
*
* @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map
*
* @return array The previous map
*
* @throws \InvalidArgumentException
*/
public function setLoggers(array $loggers)
{
$prevLogged = $this->loggedErrors;
$prev = $this->loggers;
$flush = [];
foreach ($loggers as $type => $log) {
if (!isset($prev[$type])) {
throw new \InvalidArgumentException('Unknown error type: '.$type);
}
if (!\is_array($log)) {
$log = [$log];
} elseif (!\array_key_exists(0, $log)) {
throw new \InvalidArgumentException('No logger provided');
}
if (null === $log[0]) {
$this->loggedErrors &= ~$type;
} elseif ($log[0] instanceof LoggerInterface) {
$this->loggedErrors |= $type;
} else {
throw new \InvalidArgumentException('Invalid logger provided');
}
$this->loggers[$type] = $log + $prev[$type];
if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) {
$flush[$type] = $type;
}
}
$this->reRegister($prevLogged | $this->thrownErrors);
if ($flush) {
foreach ($this->bootstrappingLogger->cleanLogs() as $log) {
$type = $log[2]['exception'] instanceof \ErrorException ? $log[2]['exception']->getSeverity() : E_ERROR;
if (!isset($flush[$type])) {
$this->bootstrappingLogger->log($log[0], $log[1], $log[2]);
} elseif ($this->loggers[$type][0]) {
$this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]);
}
}
}
return $prev;
}
/**
* Sets a user exception handler.
*
* @param callable $handler A handler that will be called on Exception
*
* @return callable|null The previous exception handler
*/
public function setExceptionHandler(callable $handler = null)
{
$prev = $this->exceptionHandler;
$this->exceptionHandler = $handler;
return $prev;
}
/**
* Sets the PHP error levels that throw an exception when a PHP error occurs.
*
* @param int $levels A bit field of E_* constants for thrown errors
* @param bool $replace Replace or amend the previous value
*
* @return int The previous value
*/
public function throwAt($levels, $replace = false)
{
$prev = $this->thrownErrors;
$this->thrownErrors = ($levels | E_RECOVERABLE_ERROR | E_USER_ERROR) & ~E_USER_DEPRECATED & ~E_DEPRECATED;
if (!$replace) {
$this->thrownErrors |= $prev;
}
$this->reRegister($prev | $this->loggedErrors);
return $prev;
}
/**
* Sets the PHP error levels for which local variables are preserved.
*
* @param int $levels A bit field of E_* constants for scoped errors
* @param bool $replace Replace or amend the previous value
*
* @return int The previous value
*/
public function scopeAt($levels, $replace = false)
{
$prev = $this->scopedErrors;
$this->scopedErrors = (int) $levels;
if (!$replace) {
$this->scopedErrors |= $prev;
}
return $prev;
}
/**
* Sets the PHP error levels for which the stack trace is preserved.
*
* @param int $levels A bit field of E_* constants for traced errors
* @param bool $replace Replace or amend the previous value
*
* @return int The previous value
*/
public function traceAt($levels, $replace = false)
{
$prev = $this->tracedErrors;
$this->tracedErrors = (int) $levels;
if (!$replace) {
$this->tracedErrors |= $prev;
}
return $prev;
}
/**
* Sets the error levels where the @-operator is ignored.
*
* @param int $levels A bit field of E_* constants for screamed errors
* @param bool $replace Replace or amend the previous value
*
* @return int The previous value
*/
public function screamAt($levels, $replace = false)
{
$prev = $this->screamedErrors;
$this->screamedErrors = (int) $levels;
if (!$replace) {
$this->screamedErrors |= $prev;
}
return $prev;
}
/**
* Re-registers as a PHP error handler if levels changed.
*/
private function reRegister($prev)
{
if ($prev !== $this->thrownErrors | $this->loggedErrors) {
$handler = set_error_handler('var_dump');
$handler = \is_array($handler) ? $handler[0] : null;
restore_error_handler();
if ($handler === $this) {
restore_error_handler();
if ($this->isRoot) {
set_error_handler([$this, 'handleError'], $this->thrownErrors | $this->loggedErrors);
} else {
set_error_handler([$this, 'handleError']);
}
}
}
}
/**
* Handles errors by filtering then logging them according to the configured bit fields.
*
* @return bool Returns false when no handling happens so that the PHP engine can handle the error itself
*
* @throws \ErrorException When $this->thrownErrors requests so
*
* @internal
*/
public function handleError(int $type, string $message, string $file, int $line): bool
{
// @deprecated to be removed in Symfony 5.0
if (\PHP_VERSION_ID >= 70300 && $message && '"' === $message[0] && 0 === strpos($message, '"continue') && preg_match('/^"continue(?: \d++)?" targeting switch is equivalent to "break(?: \d++)?"\. Did you mean to use "continue(?: \d++)?"\?$/', $message)) {
$type = E_DEPRECATED;
}
// Level is the current error reporting level to manage silent error.
$level = error_reporting();
$silenced = 0 === ($level & $type);
// Strong errors are not authorized to be silenced.
$level |= E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED;
$log = $this->loggedErrors & $type;
$throw = $this->thrownErrors & $type & $level;
$type &= $level | $this->screamedErrors;
if (!$type || (!$log && !$throw)) {
return !$silenced && $type && $log;
}
$scope = $this->scopedErrors & $type;
if (4 < $numArgs = \func_num_args()) {
$context = $scope ? (func_get_arg(4) ?: []) : [];
} else {
$context = [];
}
if (isset($context['GLOBALS']) && $scope) {
$e = $context; // Whatever the signature of the method,
unset($e['GLOBALS'], $context); // $context is always a reference in 5.3
$context = $e;
}
if (false !== strpos($message, "class@anonymous\0")) {
$logMessage = $this->levels[$type].': '.(new FlattenException())->setMessage($message)->getMessage();
} else {
$logMessage = $this->levels[$type].': '.$message;
}
if (null !== self::$toStringException) {
$errorAsException = self::$toStringException;
self::$toStringException = null;
} elseif (!$throw && !($type & $level)) {
if (!isset(self::$silencedErrorCache[$id = $file.':'.$line])) {
$lightTrace = $this->tracedErrors & $type ? $this->cleanTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5), $type, $file, $line, false) : [];
$errorAsException = new SilencedErrorContext($type, $file, $line, isset($lightTrace[1]) ? [$lightTrace[0]] : $lightTrace);
} elseif (isset(self::$silencedErrorCache[$id][$message])) {
$lightTrace = null;
$errorAsException = self::$silencedErrorCache[$id][$message];
++$errorAsException->count;
} else {
$lightTrace = [];
$errorAsException = null;
}
if (100 < ++self::$silencedErrorCount) {
self::$silencedErrorCache = $lightTrace = [];
self::$silencedErrorCount = 1;
}
if ($errorAsException) {
self::$silencedErrorCache[$id][$message] = $errorAsException;
}
if (null === $lightTrace) {
return true;
}
} else {
$errorAsException = new \ErrorException($logMessage, 0, $type, $file, $line);
if ($throw || $this->tracedErrors & $type) {
$backtrace = $errorAsException->getTrace();
$lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw);
$this->traceReflector->setValue($errorAsException, $lightTrace);
} else {
$this->traceReflector->setValue($errorAsException, []);
$backtrace = [];
}
}
if ($throw) {
if (E_USER_ERROR & $type) {
for ($i = 1; isset($backtrace[$i]); ++$i) {
if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function'])
&& '__toString' === $backtrace[$i]['function']
&& '->' === $backtrace[$i]['type']
&& !isset($backtrace[$i - 1]['class'])
&& ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function'])
) {
// Here, we know trigger_error() has been called from __toString().
// PHP triggers a fatal error when throwing from __toString().
// A small convention allows working around the limitation:
// given a caught $e exception in __toString(), quitting the method with
// `return trigger_error($e, E_USER_ERROR);` allows this error handler
// to make $e get through the __toString() barrier.
foreach ($context as $e) {
if ($e instanceof \Throwable && $e->__toString() === $message) {
self::$toStringException = $e;
return true;
}
}
// Display the original error message instead of the default one.
$this->handleException($errorAsException);
// Stop the process by giving back the error to the native handler.
return false;
}
}
}
throw $errorAsException;
}
if ($this->isRecursive) {
$log = 0;
} else {
if (!\defined('HHVM_VERSION')) {
$currentErrorHandler = set_error_handler('var_dump');
restore_error_handler();
}
try {
$this->isRecursive = true;
$level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG;
$this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? ['exception' => $errorAsException] : []);
} finally {
$this->isRecursive = false;
if (!\defined('HHVM_VERSION')) {
set_error_handler($currentErrorHandler);
}
}
}
return !$silenced && $type && $log;
}
/**
* Handles an exception by logging then forwarding it to another handler.
*
* @param \Exception|\Throwable $exception An exception to handle
* @param array $error An array as returned by error_get_last()
*
* @internal
*/
public function handleException($exception, array $error = null)
{
if (null === $error) {
self::$exitCode = 255;
}
if (!$exception instanceof \Exception) {
$exception = new FatalThrowableError($exception);
}
$type = $exception instanceof FatalErrorException ? $exception->getSeverity() : E_ERROR;
$handlerException = null;
if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) {
if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) {
$message = (new FlattenException())->setMessage($message)->getMessage();
}
if ($exception instanceof FatalErrorException) {
if ($exception instanceof FatalThrowableError) {
$error = [
'type' => $type,
'message' => $message,
'file' => $exception->getFile(),
'line' => $exception->getLine(),
];
} else {
$message = 'Fatal '.$message;
}
} elseif ($exception instanceof \ErrorException) {
$message = 'Uncaught '.$message;
} else {
$message = 'Uncaught Exception: '.$message;
}
}
if ($this->loggedErrors & $type) {
try {
$this->loggers[$type][0]->log($this->loggers[$type][1], $message, ['exception' => $exception]);
} catch (\Throwable $handlerException) {
}
}
if ($exception instanceof FatalErrorException && !$exception instanceof OutOfMemoryException && $error) {
foreach ($this->getFatalErrorHandlers() as $handler) {
if ($e = $handler->handleError($error, $exception)) {
$exception = $e;
break;
}
}
}
$exceptionHandler = $this->exceptionHandler;
$this->exceptionHandler = null;
try {
if (null !== $exceptionHandler) {
return $exceptionHandler($exception);
}
$handlerException = $handlerException ?: $exception;
} catch (\Throwable $handlerException) {
}
if ($exception === $handlerException) {
self::$reservedMemory = null; // Disable the fatal error handler
throw $exception; // Give back $exception to the native handler
}
$this->handleException($handlerException);
}
/**
* Shutdown registered function for handling PHP fatal errors.
*
* @param array $error An array as returned by error_get_last()
*
* @internal
*/
public static function handleFatalError(array $error = null)
{
if (null === self::$reservedMemory) {
return;
}
$handler = self::$reservedMemory = null;
$handlers = [];
$previousHandler = null;
$sameHandlerLimit = 10;
while (!\is_array($handler) || !$handler[0] instanceof self) {
$handler = set_exception_handler('var_dump');
restore_exception_handler();
if (!$handler) {
break;
}
restore_exception_handler();
if ($handler !== $previousHandler) {
array_unshift($handlers, $handler);
$previousHandler = $handler;
} elseif (0 === --$sameHandlerLimit) {
$handler = null;
break;
}
}
foreach ($handlers as $h) {
set_exception_handler($h);
}
if (!$handler) {
return;
}
if ($handler !== $h) {
$handler[0]->setExceptionHandler($h);
}
$handler = $handler[0];
$handlers = [];
if ($exit = null === $error) {
$error = error_get_last();
}
if ($error && $error['type'] &= E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR) {
// Let's not throw anymore but keep logging
$handler->throwAt(0, true);
$trace = isset($error['backtrace']) ? $error['backtrace'] : null;
if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) {
$exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false, $trace);
} else {
$exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace);
}
} else {
$exception = null;
}
try {
if (null !== $exception) {
self::$exitCode = 255;
$handler->handleException($exception, $error);
}
} catch (FatalErrorException $e) {
// Ignore this re-throw
}
if ($exit && self::$exitCode) {
$exitCode = self::$exitCode;
register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); });
}
}
/**
* Gets the fatal error handlers.
*
* Override this method if you want to define more fatal error handlers.
*
* @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface
*/
protected function getFatalErrorHandlers()
{
return [
new UndefinedFunctionFatalErrorHandler(),
new UndefinedMethodFatalErrorHandler(),
new ClassNotFoundFatalErrorHandler(),
];
}
/**
* Cleans the trace by removing function arguments and the frames added by the error handler and DebugClassLoader.
*/
private function cleanTrace($backtrace, $type, $file, $line, $throw)
{
$lightTrace = $backtrace;
for ($i = 0; isset($backtrace[$i]); ++$i) {
if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) {
$lightTrace = \array_slice($lightTrace, 1 + $i);
break;
}
}
if (class_exists(DebugClassLoader::class, false)) {
for ($i = \count($lightTrace) - 2; 0 < $i; --$i) {
if (DebugClassLoader::class === ($lightTrace[$i]['class'] ?? null)) {
array_splice($lightTrace, --$i, 2);
}
}
}
if (!($throw || $this->scopedErrors & $type)) {
for ($i = 0; isset($lightTrace[$i]); ++$i) {
unset($lightTrace[$i]['args'], $lightTrace[$i]['object']);
}
}
return $lightTrace;
}
}

View File

@ -11,26 +11,13 @@
namespace Symfony\Component\Debug\Exception;
use Symfony\Component\ErrorHandler\Exception\ClassNotFoundException as BaseClassNotFoundException;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ClassNotFoundException::class, BaseClassNotFoundException::class), E_USER_DEPRECATED);
/**
* Class (or Trait or Interface) Not Found Exception.
*
* @author Konstanton Myakshin <koc-dp@yandex.ru>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\ClassNotFoundException instead.
*/
class ClassNotFoundException extends FatalErrorException
class ClassNotFoundException extends BaseClassNotFoundException
{
public function __construct(string $message, \ErrorException $previous)
{
parent::__construct(
$message,
$previous->getCode(),
$previous->getSeverity(),
$previous->getFile(),
$previous->getLine(),
null,
true,
null,
$previous->getPrevious()
);
$this->setTrace($previous->getTrace());
}
}

View File

@ -11,67 +11,13 @@
namespace Symfony\Component\Debug\Exception;
use Symfony\Component\ErrorHandler\Exception\FatalErrorException as BaseFatalErrorException;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalErrorException::class, BaseFatalErrorException::class), E_USER_DEPRECATED);
/**
* Fatal Error Exception.
*
* @author Konstanton Myakshin <koc-dp@yandex.ru>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\FatalErrorException instead.
*/
class FatalErrorException extends \ErrorException
class FatalErrorException extends BaseFatalErrorException
{
public function __construct(string $message, int $code, int $severity, string $filename, int $lineno, int $traceOffset = null, bool $traceArgs = true, array $trace = null, \Throwable $previous = null)
{
parent::__construct($message, $code, $severity, $filename, $lineno, $previous);
if (null !== $trace) {
if (!$traceArgs) {
foreach ($trace as &$frame) {
unset($frame['args'], $frame['this'], $frame);
}
}
$this->setTrace($trace);
} elseif (null !== $traceOffset) {
if (\function_exists('xdebug_get_function_stack')) {
$trace = xdebug_get_function_stack();
if (0 < $traceOffset) {
array_splice($trace, -$traceOffset);
}
foreach ($trace as &$frame) {
if (!isset($frame['type'])) {
// XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695
if (isset($frame['class'])) {
$frame['type'] = '::';
}
} elseif ('dynamic' === $frame['type']) {
$frame['type'] = '->';
} elseif ('static' === $frame['type']) {
$frame['type'] = '::';
}
// XDebug also has a different name for the parameters array
if (!$traceArgs) {
unset($frame['params'], $frame['args']);
} elseif (isset($frame['params']) && !isset($frame['args'])) {
$frame['args'] = $frame['params'];
unset($frame['params']);
}
}
unset($frame);
$trace = array_reverse($trace);
} else {
$trace = [];
}
$this->setTrace($trace);
}
}
protected function setTrace($trace)
{
$traceReflector = new \ReflectionProperty('Exception', 'trace');
$traceReflector->setAccessible(true);
$traceReflector->setValue($this, $trace);
}
}

View File

@ -11,41 +11,13 @@
namespace Symfony\Component\Debug\Exception;
use Symfony\Component\ErrorHandler\Exception\FatalThrowableError as BaseFatalThrowableError;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FatalThrowableError::class, BaseFatalThrowableError::class), E_USER_DEPRECATED);
/**
* Fatal Throwable Error.
*
* @author Nicolas Grekas <p@tchwork.com>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\FatalThrowableError instead.
*/
class FatalThrowableError extends FatalErrorException
class FatalThrowableError extends BaseFatalThrowableError
{
private $originalClassName;
public function __construct(\Throwable $e)
{
$this->originalClassName = \get_class($e);
if ($e instanceof \ParseError) {
$severity = E_PARSE;
} elseif ($e instanceof \TypeError) {
$severity = E_RECOVERABLE_ERROR;
} else {
$severity = E_ERROR;
}
\ErrorException::__construct(
$e->getMessage(),
$e->getCode(),
$severity,
$e->getFile(),
$e->getLine(),
$e->getPrevious()
);
$this->setTrace($e->getTrace());
}
public function getOriginalClassName(): string
{
return $this->originalClassName;
}
}

View File

@ -11,349 +11,13 @@
namespace Symfony\Component\Debug\Exception;
use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException as BaseFlattenException;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', FlattenException::class, BaseFlattenException::class), E_USER_DEPRECATED);
/**
* FlattenException wraps a PHP Error or Exception to be able to serialize it.
*
* Basically, this class removes all objects from the trace.
*
* @author Fabien Potencier <fabien@symfony.com>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\FlattenException instead.
*/
class FlattenException
class FlattenException extends BaseFlattenException
{
private $message;
private $code;
private $previous;
private $trace;
private $traceAsString;
private $class;
private $statusCode;
private $headers;
private $file;
private $line;
public static function create(\Exception $exception, $statusCode = null, array $headers = [])
{
return static::createFromThrowable($exception, $statusCode, $headers);
}
public static function createFromThrowable(\Throwable $exception, ?int $statusCode = null, array $headers = []): self
{
$e = new static();
$e->setMessage($exception->getMessage());
$e->setCode($exception->getCode());
if ($exception instanceof HttpExceptionInterface) {
$statusCode = $exception->getStatusCode();
$headers = array_merge($headers, $exception->getHeaders());
} elseif ($exception instanceof RequestExceptionInterface) {
$statusCode = 400;
}
if (null === $statusCode) {
$statusCode = 500;
}
$e->setStatusCode($statusCode);
$e->setHeaders($headers);
$e->setTraceFromThrowable($exception);
$e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : \get_class($exception));
$e->setFile($exception->getFile());
$e->setLine($exception->getLine());
$previous = $exception->getPrevious();
if ($previous instanceof \Throwable) {
$e->setPrevious(static::createFromThrowable($previous));
}
return $e;
}
public function toArray()
{
$exceptions = [];
foreach (array_merge([$this], $this->getAllPrevious()) as $exception) {
$exceptions[] = [
'message' => $exception->getMessage(),
'class' => $exception->getClass(),
'trace' => $exception->getTrace(),
];
}
return $exceptions;
}
public function getStatusCode()
{
return $this->statusCode;
}
/**
* @return $this
*/
public function setStatusCode($code)
{
$this->statusCode = $code;
return $this;
}
public function getHeaders()
{
return $this->headers;
}
/**
* @return $this
*/
public function setHeaders(array $headers)
{
$this->headers = $headers;
return $this;
}
public function getClass()
{
return $this->class;
}
/**
* @return $this
*/
public function setClass($class)
{
$this->class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
return $this;
}
public function getFile()
{
return $this->file;
}
/**
* @return $this
*/
public function setFile($file)
{
$this->file = $file;
return $this;
}
public function getLine()
{
return $this->line;
}
/**
* @return $this
*/
public function setLine($line)
{
$this->line = $line;
return $this;
}
public function getMessage()
{
return $this->message;
}
/**
* @return $this
*/
public function setMessage($message)
{
if (false !== strpos($message, "class@anonymous\0")) {
$message = preg_replace_callback('/class@anonymous\x00.*?\.php0x?[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
}, $message);
}
$this->message = $message;
return $this;
}
public function getCode()
{
return $this->code;
}
/**
* @return $this
*/
public function setCode($code)
{
$this->code = $code;
return $this;
}
public function getPrevious()
{
return $this->previous;
}
/**
* @return $this
*/
public function setPrevious(self $previous)
{
$this->previous = $previous;
return $this;
}
public function getAllPrevious()
{
$exceptions = [];
$e = $this;
while ($e = $e->getPrevious()) {
$exceptions[] = $e;
}
return $exceptions;
}
public function getTrace()
{
return $this->trace;
}
/**
* @deprecated since 4.1, use {@see setTraceFromThrowable()} instead.
*/
public function setTraceFromException(\Exception $exception)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use "setTraceFromThrowable()" instead.', __METHOD__), E_USER_DEPRECATED);
$this->setTraceFromThrowable($exception);
}
public function setTraceFromThrowable(\Throwable $throwable)
{
$this->traceAsString = $throwable->getTraceAsString();
return $this->setTrace($throwable->getTrace(), $throwable->getFile(), $throwable->getLine());
}
/**
* @return $this
*/
public function setTrace($trace, $file, $line)
{
$this->trace = [];
$this->trace[] = [
'namespace' => '',
'short_class' => '',
'class' => '',
'type' => '',
'function' => '',
'file' => $file,
'line' => $line,
'args' => [],
];
foreach ($trace as $entry) {
$class = '';
$namespace = '';
if (isset($entry['class'])) {
$parts = explode('\\', $entry['class']);
$class = array_pop($parts);
$namespace = implode('\\', $parts);
}
$this->trace[] = [
'namespace' => $namespace,
'short_class' => $class,
'class' => isset($entry['class']) ? $entry['class'] : '',
'type' => isset($entry['type']) ? $entry['type'] : '',
'function' => isset($entry['function']) ? $entry['function'] : null,
'file' => isset($entry['file']) ? $entry['file'] : null,
'line' => isset($entry['line']) ? $entry['line'] : null,
'args' => isset($entry['args']) ? $this->flattenArgs($entry['args']) : [],
];
}
return $this;
}
private function flattenArgs($args, $level = 0, &$count = 0)
{
$result = [];
foreach ($args as $key => $value) {
if (++$count > 1e4) {
return ['array', '*SKIPPED over 10000 entries*'];
}
if ($value instanceof \__PHP_Incomplete_Class) {
// is_object() returns false on PHP<=7.1
$result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)];
} elseif (\is_object($value)) {
$result[$key] = ['object', \get_class($value)];
} elseif (\is_array($value)) {
if ($level > 10) {
$result[$key] = ['array', '*DEEP NESTED ARRAY*'];
} else {
$result[$key] = ['array', $this->flattenArgs($value, $level + 1, $count)];
}
} elseif (null === $value) {
$result[$key] = ['null', null];
} elseif (\is_bool($value)) {
$result[$key] = ['boolean', $value];
} elseif (\is_int($value)) {
$result[$key] = ['integer', $value];
} elseif (\is_float($value)) {
$result[$key] = ['float', $value];
} elseif (\is_resource($value)) {
$result[$key] = ['resource', get_resource_type($value)];
} else {
$result[$key] = ['string', (string) $value];
}
}
return $result;
}
private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value)
{
$array = new \ArrayObject($value);
return $array['__PHP_Incomplete_Class_Name'];
}
public function getTraceAsString()
{
return $this->traceAsString;
}
public function getAsString()
{
$message = '';
$next = false;
foreach (array_reverse(array_merge([$this], $this->getAllPrevious())) as $exception) {
if ($next) {
$message .= 'Next ';
} else {
$next = true;
}
$message .= $exception->getClass();
if ('' != $exception->getMessage()) {
$message .= ': '.$exception->getMessage();
}
$message .= ' in '.$exception->getFile().':'.$exception->getLine().
"\nStack trace:\n".$exception->getTraceAsString()."\n\n";
}
return rtrim($message);
}
}

View File

@ -11,11 +11,13 @@
namespace Symfony\Component\Debug\Exception;
use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException as BaseOutOfMemoryException;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', OutOfMemoryException::class, BaseOutOfMemoryException::class), E_USER_DEPRECATED);
/**
* Out of memory exception.
*
* @author Nicolas Grekas <p@tchwork.com>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException instead.
*/
class OutOfMemoryException extends FatalErrorException
class OutOfMemoryException extends BaseOutOfMemoryException
{
}

View File

@ -11,57 +11,13 @@
namespace Symfony\Component\Debug\Exception;
use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext as BaseSilencedErrorContext;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', SilencedErrorContext::class, BaseSilencedErrorContext::class), E_USER_DEPRECATED);
/**
* Data Object that represents a Silenced Error.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext instead.
*/
class SilencedErrorContext implements \JsonSerializable
class SilencedErrorContext extends BaseSilencedErrorContext
{
public $count = 1;
private $severity;
private $file;
private $line;
private $trace;
public function __construct(int $severity, string $file, int $line, array $trace = [], int $count = 1)
{
$this->severity = $severity;
$this->file = $file;
$this->line = $line;
$this->trace = $trace;
$this->count = $count;
}
public function getSeverity()
{
return $this->severity;
}
public function getFile()
{
return $this->file;
}
public function getLine()
{
return $this->line;
}
public function getTrace()
{
return $this->trace;
}
public function JsonSerialize()
{
return [
'severity' => $this->severity,
'file' => $this->file,
'line' => $this->line,
'trace' => $this->trace,
'count' => $this->count,
];
}
}

View File

@ -11,26 +11,13 @@
namespace Symfony\Component\Debug\Exception;
use Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException as BaseUndefinedFunctionException;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedFunctionException::class, BaseUndefinedFunctionException::class), E_USER_DEPRECATED);
/**
* Undefined Function Exception.
*
* @author Konstanton Myakshin <koc-dp@yandex.ru>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException instead.
*/
class UndefinedFunctionException extends FatalErrorException
class UndefinedFunctionException extends BaseUndefinedFunctionException
{
public function __construct(string $message, \ErrorException $previous)
{
parent::__construct(
$message,
$previous->getCode(),
$previous->getSeverity(),
$previous->getFile(),
$previous->getLine(),
null,
true,
null,
$previous->getPrevious()
);
$this->setTrace($previous->getTrace());
}
}

View File

@ -11,26 +11,13 @@
namespace Symfony\Component\Debug\Exception;
use Symfony\Component\ErrorHandler\Exception\UndefinedMethodException as BaseUndefinedMethodException;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedMethodException::class, BaseUndefinedMethodException::class), E_USER_DEPRECATED);
/**
* Undefined Method Exception.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\UndefinedMethodException instead.
*/
class UndefinedMethodException extends FatalErrorException
class UndefinedMethodException extends BaseUndefinedMethodException
{
public function __construct(string $message, \ErrorException $previous)
{
parent::__construct(
$message,
$previous->getCode(),
$previous->getSeverity(),
$previous->getFile(),
$previous->getLine(),
null,
true,
null,
$previous->getPrevious()
);
$this->setTrace($previous->getTrace());
}
}

File diff suppressed because one or more lines are too long

View File

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

View File

@ -11,22 +11,13 @@
namespace Symfony\Component\Debug\FatalErrorHandler;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\ErrorHandler\FatalErrorHandler\FatalErrorHandlerInterface as BaseFatalErrorHandlerInterface;
@trigger_error(sprintf('The "%s" interface is deprecated since Symfony 4.4, use "%s" instead.', FatalErrorHandlerInterface::class, BaseFatalErrorHandlerInterface::class), E_USER_DEPRECATED);
/**
* Attempts to convert fatal errors to exceptions.
*
* @author Fabien Potencier <fabien@symfony.com>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\FatalErrorHandlerInterface instead.
*/
interface FatalErrorHandlerInterface
interface FatalErrorHandlerInterface extends BaseFatalErrorHandlerInterface
{
/**
* Attempts to convert an error into an exception.
*
* @param array $error An array as returned by error_get_last()
* @param FatalErrorException $exception A FatalErrorException instance
*
* @return FatalErrorException|null A FatalErrorException instance if the class is able to convert the error, null otherwise
*/
public function handleError(array $error, FatalErrorException $exception);
}

View File

@ -11,74 +11,13 @@
namespace Symfony\Component\Debug\FatalErrorHandler;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\Exception\UndefinedFunctionException;
use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler as BaseUndefinedFunctionFatalErrorHandler;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedFunctionFatalErrorHandler::class, BaseUndefinedFunctionFatalErrorHandler::class), E_USER_DEPRECATED);
/**
* ErrorHandler for undefined functions.
*
* @author Fabien Potencier <fabien@symfony.com>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler instead.
*/
class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface
class UndefinedFunctionFatalErrorHandler extends BaseUndefinedFunctionFatalErrorHandler
{
/**
* {@inheritdoc}
*/
public function handleError(array $error, FatalErrorException $exception)
{
$messageLen = \strlen($error['message']);
$notFoundSuffix = '()';
$notFoundSuffixLen = \strlen($notFoundSuffix);
if ($notFoundSuffixLen > $messageLen) {
return;
}
if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) {
return;
}
$prefix = 'Call to undefined function ';
$prefixLen = \strlen($prefix);
if (0 !== strpos($error['message'], $prefix)) {
return;
}
$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".', $functionName, $namespacePrefix);
} else {
$functionName = $fullyQualifiedFunctionName;
$message = sprintf('Attempted to call function "%s" from the global namespace.', $functionName);
}
$candidates = [];
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) {
sort($candidates);
$last = array_pop($candidates).'"?';
if ($candidates) {
$candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last;
} else {
$candidates = '"'.$last;
}
$message .= "\nDid you mean to call ".$candidates;
}
return new UndefinedFunctionException($message, $exception);
}
}

View File

@ -11,56 +11,13 @@
namespace Symfony\Component\Debug\FatalErrorHandler;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\Exception\UndefinedMethodException;
use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler as BaseUndefinedMethodFatalErrorHandler;
@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', UndefinedMethodFatalErrorHandler::class, BaseUndefinedMethodFatalErrorHandler::class), E_USER_DEPRECATED);
/**
* ErrorHandler for undefined methods.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler instead.
*/
class UndefinedMethodFatalErrorHandler implements FatalErrorHandlerInterface
class UndefinedMethodFatalErrorHandler extends BaseUndefinedMethodFatalErrorHandler
{
/**
* {@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 an undefined method named "%s" of class "%s".', $methodName, $className);
if (!class_exists($className) || null === $methods = get_class_methods($className)) {
// failed to get the class or its methods on which an unknown method was called (for example on an anonymous class)
return new UndefinedMethodException($message, $exception);
}
$candidates = [];
foreach ($methods as $definedMethodName) {
$lev = levenshtein($methodName, $definedMethodName);
if ($lev <= \strlen($methodName) / 3 || false !== strpos($definedMethodName, $methodName)) {
$candidates[] = $definedMethodName;
}
}
if ($candidates) {
sort($candidates);
$last = array_pop($candidates).'"?';
if ($candidates) {
$candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last;
} else {
$candidates = '"'.$last;
}
$message .= "\nDid you mean to call ".$candidates;
}
return new UndefinedMethodException($message, $exception);
}
}

View File

@ -1,5 +0,0 @@
<?php
class Symfony_Component_Debug_Tests_Fixtures_PEARClass
{
}

View File

@ -17,7 +17,8 @@
],
"require": {
"php": "^7.1.3",
"psr/log": "~1.0"
"psr/log": "~1.0",
"symfony/error-handler": "^4.4"
},
"conflict": {
"symfony/http-kernel": "<3.4"

View File

@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@ -0,0 +1,37 @@
<?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\ErrorHandler;
use Psr\Log\AbstractLogger;
/**
* A buffering logger that stacks logs for later.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class BufferingLogger extends AbstractLogger
{
private $logs = [];
public function log($level, $message, array $context = [])
{
$this->logs[] = [$level, $message, $context];
}
public function cleanLogs()
{
$logs = $this->logs;
$this->logs = [];
return $logs;
}
}

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
4.4.0
-----
* added the component

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\ErrorHandler\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class ErrorHandlerPass implements CompilerPassInterface
{
private $rendererService;
private $rendererTag;
public function __construct(string $rendererService = 'error_handler.error_renderer', string $rendererTag = 'error_handler.renderer')
{
$this->rendererService = $rendererService;
$this->rendererTag = $rendererTag;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->rendererService)) {
return;
}
$renderers = $registered = [];
foreach ($container->findTaggedServiceIds($this->rendererTag, true) as $serviceId => $tags) {
/** @var ErrorRendererInterface $class */
$class = $container->getDefinition($serviceId)->getClass();
foreach ($tags as $tag) {
$format = $tag['format'] ?? $class::getFormat();
if (!isset($registered[$format])) {
$priority = $tag['priority'] ?? 0;
$renderers[$priority][$format] = new Reference($serviceId);
$registered[$format] = true;
}
}
}
if ($renderers) {
krsort($renderers);
$renderers = array_merge(...$renderers);
}
$definition = $container->getDefinition($this->rendererService);
$definition->replaceArgument(0, ServiceLocatorTagPass::register($container, $renderers));
}
}

View File

@ -0,0 +1,44 @@
<?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\ErrorHandler\DependencyInjection;
use Psr\Container\ContainerInterface;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRenderer as BaseErrorRenderer;
/**
* Lazily loads error renderers from the dependency injection container.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class ErrorRenderer extends BaseErrorRenderer
{
private $container;
private $initialized = [];
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* {@inheritdoc}
*/
public function render($exception, string $format = 'html'): string
{
if (!isset($this->initialized[$format]) && $this->container->has($format)) {
$this->addRenderer($this->container->get($format), $format);
$this->initialized[$format] = true;
}
return parent::render($exception, $format);
}
}

View File

@ -0,0 +1,711 @@
<?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\ErrorHandler;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\Debug\DebugClassLoader;
use Symfony\Component\ErrorHandler\Exception\FatalErrorException;
use Symfony\Component\ErrorHandler\Exception\FatalThrowableError;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException;
use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext;
use Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHandler;
use Symfony\Component\ErrorHandler\FatalErrorHandler\FatalErrorHandlerInterface;
use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler;
use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler;
/**
* A generic ErrorHandler for the PHP engine.
*
* Provides five bit fields that control how errors are handled:
* - thrownErrors: errors thrown as \ErrorException
* - loggedErrors: logged errors, when not @-silenced
* - scopedErrors: errors thrown or logged with their local context
* - tracedErrors: errors logged with their stack trace
* - screamedErrors: never @-silenced errors
*
* Each error level can be logged by a dedicated PSR-3 logger object.
* Screaming only applies to logging.
* Throwing takes precedence over logging.
* Uncaught exceptions are logged as E_ERROR.
* E_DEPRECATED and E_USER_DEPRECATED levels never throw.
* E_RECOVERABLE_ERROR and E_USER_ERROR levels always throw.
* Non catchable errors that can be detected at shutdown time are logged when the scream bit field allows so.
* As errors have a performance cost, repeated errors are all logged, so that the developer
* can see them and weight them as more important to fix than others of the same level.
*
* @author Nicolas Grekas <p@tchwork.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*
* @final
*/
class ErrorHandler
{
private $levels = [
E_COMPILE_ERROR => 'Compile Error',
E_COMPILE_WARNING => 'Compile Warning',
E_CORE_ERROR => 'Core Error',
E_CORE_WARNING => 'Core Warning',
E_DEPRECATED => 'Deprecated',
E_ERROR => 'Error',
E_NOTICE => 'Notice',
E_PARSE => 'Parse Error',
E_RECOVERABLE_ERROR => 'Catchable Fatal Error',
E_STRICT => 'Runtime Notice',
E_USER_DEPRECATED => 'User Deprecated',
E_USER_ERROR => 'User Error',
E_USER_NOTICE => 'User Notice',
E_USER_WARNING => 'User Warning',
E_WARNING => 'Warning',
];
private $loggers = [
E_COMPILE_ERROR => [null, LogLevel::CRITICAL],
E_COMPILE_WARNING => [null, LogLevel::WARNING],
E_CORE_ERROR => [null, LogLevel::CRITICAL],
E_CORE_WARNING => [null, LogLevel::WARNING],
E_DEPRECATED => [null, LogLevel::INFO],
E_ERROR => [null, LogLevel::CRITICAL],
E_NOTICE => [null, LogLevel::WARNING],
E_PARSE => [null, LogLevel::CRITICAL],
E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL],
E_STRICT => [null, LogLevel::WARNING],
E_USER_DEPRECATED => [null, LogLevel::INFO],
E_USER_ERROR => [null, LogLevel::CRITICAL],
E_USER_NOTICE => [null, LogLevel::WARNING],
E_USER_WARNING => [null, LogLevel::WARNING],
E_WARNING => [null, LogLevel::WARNING],
];
private $thrownErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED
private $scopedErrors = 0x1FFF; // E_ALL - E_DEPRECATED - E_USER_DEPRECATED
private $tracedErrors = 0x77FB; // E_ALL - E_STRICT - E_PARSE
private $screamedErrors = 0x55; // E_ERROR + E_CORE_ERROR + E_COMPILE_ERROR + E_PARSE
private $loggedErrors = 0;
private $traceReflector;
private $isRecursive = 0;
private $isRoot = false;
private $exceptionHandler;
private $bootstrappingLogger;
private static $reservedMemory;
private static $toStringException = null;
private static $silencedErrorCache = [];
private static $silencedErrorCount = 0;
private static $exitCode = 0;
/**
* Registers the error handler.
*
* @param self|null $handler The handler to register
* @param bool $replace Whether to replace or not any existing handler
*
* @return self The registered error handler
*/
public static function register(self $handler = null, $replace = true)
{
if (null === self::$reservedMemory) {
self::$reservedMemory = str_repeat('x', 10240);
register_shutdown_function(__CLASS__.'::handleFatalError');
}
if ($handlerIsNew = null === $handler) {
$handler = new static();
}
if (null === $prev = set_error_handler([$handler, 'handleError'])) {
restore_error_handler();
// Specifying the error types earlier would expose us to https://bugs.php.net/63206
set_error_handler([$handler, 'handleError'], $handler->thrownErrors | $handler->loggedErrors);
$handler->isRoot = true;
}
if ($handlerIsNew && \is_array($prev) && $prev[0] instanceof self) {
$handler = $prev[0];
$replace = false;
}
if (!$replace && $prev) {
restore_error_handler();
$handlerIsRegistered = \is_array($prev) && $handler === $prev[0];
} else {
$handlerIsRegistered = true;
}
if (\is_array($prev = set_exception_handler([$handler, 'handleException'])) && $prev[0] instanceof self) {
restore_exception_handler();
if (!$handlerIsRegistered) {
$handler = $prev[0];
} elseif ($handler !== $prev[0] && $replace) {
set_exception_handler([$handler, 'handleException']);
$p = $prev[0]->setExceptionHandler(null);
$handler->setExceptionHandler($p);
$prev[0]->setExceptionHandler($p);
}
} else {
$handler->setExceptionHandler($prev);
}
$handler->throwAt(E_ALL & $handler->thrownErrors, true);
return $handler;
}
public function __construct(BufferingLogger $bootstrappingLogger = null)
{
if ($bootstrappingLogger) {
$this->bootstrappingLogger = $bootstrappingLogger;
$this->setDefaultLogger($bootstrappingLogger);
}
$this->traceReflector = new \ReflectionProperty('Exception', 'trace');
$this->traceReflector->setAccessible(true);
}
/**
* Sets a logger to non assigned errors levels.
*
* @param LoggerInterface $logger A PSR-3 logger to put as default for the given levels
* @param array|int $levels An array map of E_* to LogLevel::* or an integer bit field of E_* constants
* @param bool $replace Whether to replace or not any existing logger
*/
public function setDefaultLogger(LoggerInterface $logger, $levels = E_ALL, $replace = false)
{
$loggers = [];
if (\is_array($levels)) {
foreach ($levels as $type => $logLevel) {
if (empty($this->loggers[$type][0]) || $replace || $this->loggers[$type][0] === $this->bootstrappingLogger) {
$loggers[$type] = [$logger, $logLevel];
}
}
} else {
if (null === $levels) {
$levels = E_ALL;
}
foreach ($this->loggers as $type => $log) {
if (($type & $levels) && (empty($log[0]) || $replace || $log[0] === $this->bootstrappingLogger)) {
$log[0] = $logger;
$loggers[$type] = $log;
}
}
}
$this->setLoggers($loggers);
}
/**
* Sets a logger for each error level.
*
* @param array $loggers Error levels to [LoggerInterface|null, LogLevel::*] map
*
* @return array The previous map
*
* @throws \InvalidArgumentException
*/
public function setLoggers(array $loggers)
{
$prevLogged = $this->loggedErrors;
$prev = $this->loggers;
$flush = [];
foreach ($loggers as $type => $log) {
if (!isset($prev[$type])) {
throw new \InvalidArgumentException('Unknown error type: '.$type);
}
if (!\is_array($log)) {
$log = [$log];
} elseif (!\array_key_exists(0, $log)) {
throw new \InvalidArgumentException('No logger provided');
}
if (null === $log[0]) {
$this->loggedErrors &= ~$type;
} elseif ($log[0] instanceof LoggerInterface) {
$this->loggedErrors |= $type;
} else {
throw new \InvalidArgumentException('Invalid logger provided');
}
$this->loggers[$type] = $log + $prev[$type];
if ($this->bootstrappingLogger && $prev[$type][0] === $this->bootstrappingLogger) {
$flush[$type] = $type;
}
}
$this->reRegister($prevLogged | $this->thrownErrors);
if ($flush) {
foreach ($this->bootstrappingLogger->cleanLogs() as $log) {
$type = $log[2]['exception'] instanceof \ErrorException ? $log[2]['exception']->getSeverity() : E_ERROR;
if (!isset($flush[$type])) {
$this->bootstrappingLogger->log($log[0], $log[1], $log[2]);
} elseif ($this->loggers[$type][0]) {
$this->loggers[$type][0]->log($this->loggers[$type][1], $log[1], $log[2]);
}
}
}
return $prev;
}
/**
* Sets a user exception handler.
*
* @param callable $handler A handler that will be called on Exception
*
* @return callable|null The previous exception handler
*/
public function setExceptionHandler(callable $handler = null)
{
$prev = $this->exceptionHandler;
$this->exceptionHandler = $handler;
return $prev;
}
/**
* Sets the PHP error levels that throw an exception when a PHP error occurs.
*
* @param int $levels A bit field of E_* constants for thrown errors
* @param bool $replace Replace or amend the previous value
*
* @return int The previous value
*/
public function throwAt($levels, $replace = false)
{
$prev = $this->thrownErrors;
$this->thrownErrors = ($levels | E_RECOVERABLE_ERROR | E_USER_ERROR) & ~E_USER_DEPRECATED & ~E_DEPRECATED;
if (!$replace) {
$this->thrownErrors |= $prev;
}
$this->reRegister($prev | $this->loggedErrors);
return $prev;
}
/**
* Sets the PHP error levels for which local variables are preserved.
*
* @param int $levels A bit field of E_* constants for scoped errors
* @param bool $replace Replace or amend the previous value
*
* @return int The previous value
*/
public function scopeAt($levels, $replace = false)
{
$prev = $this->scopedErrors;
$this->scopedErrors = (int) $levels;
if (!$replace) {
$this->scopedErrors |= $prev;
}
return $prev;
}
/**
* Sets the PHP error levels for which the stack trace is preserved.
*
* @param int $levels A bit field of E_* constants for traced errors
* @param bool $replace Replace or amend the previous value
*
* @return int The previous value
*/
public function traceAt($levels, $replace = false)
{
$prev = $this->tracedErrors;
$this->tracedErrors = (int) $levels;
if (!$replace) {
$this->tracedErrors |= $prev;
}
return $prev;
}
/**
* Sets the error levels where the @-operator is ignored.
*
* @param int $levels A bit field of E_* constants for screamed errors
* @param bool $replace Replace or amend the previous value
*
* @return int The previous value
*/
public function screamAt($levels, $replace = false)
{
$prev = $this->screamedErrors;
$this->screamedErrors = (int) $levels;
if (!$replace) {
$this->screamedErrors |= $prev;
}
return $prev;
}
/**
* Re-registers as a PHP error handler if levels changed.
*/
private function reRegister($prev)
{
if ($prev !== $this->thrownErrors | $this->loggedErrors) {
$handler = set_error_handler('var_dump');
$handler = \is_array($handler) ? $handler[0] : null;
restore_error_handler();
if ($handler === $this) {
restore_error_handler();
if ($this->isRoot) {
set_error_handler([$this, 'handleError'], $this->thrownErrors | $this->loggedErrors);
} else {
set_error_handler([$this, 'handleError']);
}
}
}
}
/**
* Handles errors by filtering then logging them according to the configured bit fields.
*
* @return bool Returns false when no handling happens so that the PHP engine can handle the error itself
*
* @throws \ErrorException When $this->thrownErrors requests so
*
* @internal
*/
public function handleError(int $type, string $message, string $file, int $line): bool
{
// @deprecated to be removed in Symfony 5.0
if (\PHP_VERSION_ID >= 70300 && $message && '"' === $message[0] && 0 === strpos($message, '"continue') && preg_match('/^"continue(?: \d++)?" targeting switch is equivalent to "break(?: \d++)?"\. Did you mean to use "continue(?: \d++)?"\?$/', $message)) {
$type = E_DEPRECATED;
}
// Level is the current error reporting level to manage silent error.
$level = error_reporting();
$silenced = 0 === ($level & $type);
// Strong errors are not authorized to be silenced.
$level |= E_RECOVERABLE_ERROR | E_USER_ERROR | E_DEPRECATED | E_USER_DEPRECATED;
$log = $this->loggedErrors & $type;
$throw = $this->thrownErrors & $type & $level;
$type &= $level | $this->screamedErrors;
if (!$type || (!$log && !$throw)) {
return !$silenced && $type && $log;
}
$scope = $this->scopedErrors & $type;
if (4 < $numArgs = \func_num_args()) {
$context = $scope ? (func_get_arg(4) ?: []) : [];
} else {
$context = [];
}
if (isset($context['GLOBALS']) && $scope) {
$e = $context; // Whatever the signature of the method,
unset($e['GLOBALS'], $context);
$context = $e;
}
if (false !== strpos($message, "class@anonymous\0")) {
$logMessage = $this->levels[$type].': '.(new FlattenException())->setMessage($message)->getMessage();
} else {
$logMessage = $this->levels[$type].': '.$message;
}
if (null !== self::$toStringException) {
$errorAsException = self::$toStringException;
self::$toStringException = null;
} elseif (!$throw && !($type & $level)) {
if (!isset(self::$silencedErrorCache[$id = $file.':'.$line])) {
$lightTrace = $this->tracedErrors & $type ? $this->cleanTrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 5), $type, $file, $line, false) : [];
$errorAsException = new SilencedErrorContext($type, $file, $line, isset($lightTrace[1]) ? [$lightTrace[0]] : $lightTrace);
} elseif (isset(self::$silencedErrorCache[$id][$message])) {
$lightTrace = null;
$errorAsException = self::$silencedErrorCache[$id][$message];
++$errorAsException->count;
} else {
$lightTrace = [];
$errorAsException = null;
}
if (100 < ++self::$silencedErrorCount) {
self::$silencedErrorCache = $lightTrace = [];
self::$silencedErrorCount = 1;
}
if ($errorAsException) {
self::$silencedErrorCache[$id][$message] = $errorAsException;
}
if (null === $lightTrace) {
return true;
}
} else {
$errorAsException = new \ErrorException($logMessage, 0, $type, $file, $line);
if ($throw || $this->tracedErrors & $type) {
$backtrace = $errorAsException->getTrace();
$lightTrace = $this->cleanTrace($backtrace, $type, $file, $line, $throw);
$this->traceReflector->setValue($errorAsException, $lightTrace);
} else {
$this->traceReflector->setValue($errorAsException, []);
$backtrace = [];
}
}
if ($throw) {
if (E_USER_ERROR & $type) {
for ($i = 1; isset($backtrace[$i]); ++$i) {
if (isset($backtrace[$i]['function'], $backtrace[$i]['type'], $backtrace[$i - 1]['function'])
&& '__toString' === $backtrace[$i]['function']
&& '->' === $backtrace[$i]['type']
&& !isset($backtrace[$i - 1]['class'])
&& ('trigger_error' === $backtrace[$i - 1]['function'] || 'user_error' === $backtrace[$i - 1]['function'])
) {
// Here, we know trigger_error() has been called from __toString().
// PHP triggers a fatal error when throwing from __toString().
// A small convention allows working around the limitation:
// given a caught $e exception in __toString(), quitting the method with
// `return trigger_error($e, E_USER_ERROR);` allows this error handler
// to make $e get through the __toString() barrier.
foreach ($context as $e) {
if ($e instanceof \Throwable && $e->__toString() === $message) {
self::$toStringException = $e;
return true;
}
}
// Display the original error message instead of the default one.
$this->handleException($errorAsException);
// Stop the process by giving back the error to the native handler.
return false;
}
}
}
throw $errorAsException;
}
if ($this->isRecursive) {
$log = 0;
} else {
if (!\defined('HHVM_VERSION')) {
$currentErrorHandler = set_error_handler('var_dump');
restore_error_handler();
}
try {
$this->isRecursive = true;
$level = ($type & $level) ? $this->loggers[$type][1] : LogLevel::DEBUG;
$this->loggers[$type][0]->log($level, $logMessage, $errorAsException ? ['exception' => $errorAsException] : []);
} finally {
$this->isRecursive = false;
if (!\defined('HHVM_VERSION')) {
set_error_handler($currentErrorHandler);
}
}
}
return !$silenced && $type && $log;
}
/**
* Handles an exception by logging then forwarding it to another handler.
*
* @param \Exception|\Throwable $exception An exception to handle
* @param array $error An array as returned by error_get_last()
*
* @internal
*/
public function handleException($exception, array $error = null)
{
if (null === $error) {
self::$exitCode = 255;
}
if (!$exception instanceof \Exception) {
$exception = new FatalThrowableError($exception);
}
$type = $exception instanceof FatalErrorException ? $exception->getSeverity() : E_ERROR;
$handlerException = null;
if (($this->loggedErrors & $type) || $exception instanceof FatalThrowableError) {
if (false !== strpos($message = $exception->getMessage(), "class@anonymous\0")) {
$message = (new FlattenException())->setMessage($message)->getMessage();
}
if ($exception instanceof FatalErrorException) {
if ($exception instanceof FatalThrowableError) {
$error = [
'type' => $type,
'message' => $message,
'file' => $exception->getFile(),
'line' => $exception->getLine(),
];
} else {
$message = 'Fatal '.$message;
}
} elseif ($exception instanceof \ErrorException) {
$message = 'Uncaught '.$message;
} else {
$message = 'Uncaught Exception: '.$message;
}
}
if ($this->loggedErrors & $type) {
try {
$this->loggers[$type][0]->log($this->loggers[$type][1], $message, ['exception' => $exception]);
} catch (\Throwable $handlerException) {
}
}
if ($exception instanceof FatalErrorException && !$exception instanceof OutOfMemoryException && $error) {
foreach ($this->getFatalErrorHandlers() as $handler) {
if ($e = $handler->handleError($error, $exception)) {
$exception = $e;
break;
}
}
}
$exceptionHandler = $this->exceptionHandler;
$this->exceptionHandler = null;
try {
if (null !== $exceptionHandler) {
return $exceptionHandler($exception);
}
$handlerException = $handlerException ?: $exception;
} catch (\Throwable $handlerException) {
}
if ($exception === $handlerException) {
self::$reservedMemory = null; // Disable the fatal error handler
throw $exception; // Give back $exception to the native handler
}
$this->handleException($handlerException);
}
/**
* Shutdown registered function for handling PHP fatal errors.
*
* @param array $error An array as returned by error_get_last()
*
* @internal
*/
public static function handleFatalError(array $error = null)
{
if (null === self::$reservedMemory) {
return;
}
$handler = self::$reservedMemory = null;
$handlers = [];
$previousHandler = null;
$sameHandlerLimit = 10;
while (!\is_array($handler) || !$handler[0] instanceof self) {
$handler = set_exception_handler('var_dump');
restore_exception_handler();
if (!$handler) {
break;
}
restore_exception_handler();
if ($handler !== $previousHandler) {
array_unshift($handlers, $handler);
$previousHandler = $handler;
} elseif (0 === --$sameHandlerLimit) {
$handler = null;
break;
}
}
foreach ($handlers as $h) {
set_exception_handler($h);
}
if (!$handler) {
return;
}
if ($handler !== $h) {
$handler[0]->setExceptionHandler($h);
}
$handler = $handler[0];
$handlers = [];
if ($exit = null === $error) {
$error = error_get_last();
}
if ($error && $error['type'] &= E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR) {
// Let's not throw anymore but keep logging
$handler->throwAt(0, true);
$trace = isset($error['backtrace']) ? $error['backtrace'] : null;
if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) {
$exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false, $trace);
} else {
$exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true, $trace);
}
} else {
$exception = null;
}
try {
if (null !== $exception) {
self::$exitCode = 255;
$handler->handleException($exception, $error);
}
} catch (FatalErrorException $e) {
// Ignore this re-throw
}
if ($exit && self::$exitCode) {
$exitCode = self::$exitCode;
register_shutdown_function('register_shutdown_function', function () use ($exitCode) { exit($exitCode); });
}
}
/**
* Gets the fatal error handlers.
*
* Override this method if you want to define more fatal error handlers.
*
* @return FatalErrorHandlerInterface[] An array of FatalErrorHandlerInterface
*/
protected function getFatalErrorHandlers()
{
return [
new UndefinedFunctionFatalErrorHandler(),
new UndefinedMethodFatalErrorHandler(),
new ClassNotFoundFatalErrorHandler(),
];
}
/**
* Cleans the trace by removing function arguments and the frames added by the error handler and DebugClassLoader.
*/
private function cleanTrace($backtrace, $type, $file, $line, $throw)
{
$lightTrace = $backtrace;
for ($i = 0; isset($backtrace[$i]); ++$i) {
if (isset($backtrace[$i]['file'], $backtrace[$i]['line']) && $backtrace[$i]['line'] === $line && $backtrace[$i]['file'] === $file) {
$lightTrace = \array_slice($lightTrace, 1 + $i);
break;
}
}
if (class_exists(DebugClassLoader::class, false)) {
for ($i = \count($lightTrace) - 2; 0 < $i; --$i) {
if (DebugClassLoader::class === ($lightTrace[$i]['class'] ?? null)) {
array_splice($lightTrace, --$i, 2);
}
}
}
if (!($throw || $this->scopedErrors & $type)) {
for ($i = 0; isset($lightTrace[$i]); ++$i) {
unset($lightTrace[$i]['args'], $lightTrace[$i]['object']);
}
}
return $lightTrace;
}
}

View File

@ -0,0 +1,71 @@
<?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\ErrorHandler\ErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\ErrorRendererNotFoundException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
/**
* Renders an Exception that represents a Response content.
*
* @see ErrorRendererInterface
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class ErrorRenderer
{
private $renderers = [];
/**
* @param ErrorRendererInterface[] $renderers
*/
public function __construct(array $renderers)
{
foreach ($renderers as $renderer) {
if (!$renderer instanceof ErrorRendererInterface) {
throw new \InvalidArgumentException(sprintf('Error renderer "%s" must implement "%s".', \get_class($renderer), ErrorRendererInterface::class));
}
$this->addRenderer($renderer, $renderer::getFormat());
}
}
public function addRenderer(ErrorRendererInterface $renderer, string $format): self
{
$this->renderers[$format] = $renderer;
return $this;
}
/**
* Renders an Exception and returns the Response content.
*
* @param \Exception|FlattenException $exception An \Exception or FlattenException instance
* @param string $format The request format (html, json, xml, etc.)
*
* @return string The Response content as a string
*
* @throws ErrorRendererNotFoundException if no renderer is found
*/
public function render($exception, string $format = 'html'): string
{
if (!isset($this->renderers[$format])) {
throw new ErrorRendererNotFoundException(sprintf('No error renderer found for format "%s".', $format));
}
if (!$exception instanceof FlattenException) {
$exception = FlattenException::create($exception);
}
return $this->renderers[$format]->render($exception);
}
}

View File

@ -0,0 +1,36 @@
<?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\ErrorHandler\ErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
/**
* Interface implemented by all error renderers.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
interface ErrorRendererInterface
{
/**
* Gets the format of the content.
*
* @return string The content format
*/
public static function getFormat(): string;
/**
* Renders an Exception and returns the Response content.
*
* @return string The Response content as a string
*/
public function render(FlattenException $exception): string;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,52 @@
<?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\ErrorHandler\ErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class JsonErrorRenderer implements ErrorRendererInterface
{
private $debug;
public function __construct(bool $debug = true)
{
$this->debug = $debug;
}
/**
* {@inheritdoc}
*/
public static function getFormat(): string
{
return 'json';
}
/**
* {@inheritdoc}
*/
public function render(FlattenException $exception): string
{
$content = [
'title' => $exception->getTitle(),
'status' => $exception->getStatusCode(),
'detail' => $exception->getMessage(),
];
if ($this->debug) {
$content['exceptions'] = $exception->toArray();
}
return (string) json_encode($content);
}
}

View File

@ -0,0 +1,98 @@
<?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\ErrorHandler\ErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class TxtErrorRenderer implements ErrorRendererInterface
{
private $debug;
private $charset;
public function __construct(bool $debug = true, string $charset = null)
{
$this->debug = $debug;
$this->charset = $charset ?: (ini_get('default_charset') ?: 'UTF-8');
}
/**
* {@inheritdoc}
*/
public static function getFormat(): string
{
return 'txt';
}
/**
* {@inheritdoc}
*/
public function render(FlattenException $exception): string
{
$content = sprintf("[title] %s\n", $exception->getTitle());
$content .= sprintf("[status] %s\n", $exception->getStatusCode());
$content .= sprintf("[detail] %s\n", $exception->getMessage());
if ($this->debug) {
foreach ($exception->toArray() as $i => $e) {
$content .= sprintf("[%d] %s: %s\n", $i + 1, $e['class'], $e['message']);
foreach ($e['trace'] as $trace) {
if ($trace['function']) {
$content .= sprintf('at %s%s%s(%s) ', $trace['class'], $trace['type'], $trace['function'], $this->formatArgs($trace['args']));
}
if (isset($trace['file'], $trace['line'])) {
$content .= $this->formatPath($trace['file'], $trace['line']);
}
$content .= "\n";
}
}
}
return $content;
}
private function formatPath(string $path, int $line): string
{
$file = preg_match('#[^/\\\\]*+$#', $path, $file) ? $file[0] : $path;
return sprintf('in %s %s', $path, 0 < $line ? ' line '.$line : '');
}
/**
* Formats an array as a string.
*/
private function formatArgs(array $args): string
{
$result = [];
foreach ($args as $key => $item) {
if ('object' === $item[0]) {
$formattedValue = sprintf('object(%s)', $item[1]);
} elseif ('array' === $item[0]) {
$formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]);
} elseif ('null' === $item[0]) {
$formattedValue = 'null';
} elseif ('boolean' === $item[0]) {
$formattedValue = strtolower(var_export($item[1], true));
} elseif ('resource' === $item[0]) {
$formattedValue = 'resource';
} else {
$formattedValue = str_replace("\n", '', var_export($item[1], true));
}
$result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue);
}
return implode(', ', $result);
}
}

View File

@ -0,0 +1,117 @@
<?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\ErrorHandler\ErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class XmlErrorRenderer implements ErrorRendererInterface
{
private $debug;
private $charset;
public function __construct(bool $debug = true, string $charset = null)
{
$this->debug = $debug;
$this->charset = $charset ?: (ini_get('default_charset') ?: 'UTF-8');
}
/**
* {@inheritdoc}
*/
public static function getFormat(): string
{
return 'xml';
}
/**
* {@inheritdoc}
*/
public function render(FlattenException $exception): string
{
$message = $this->escapeXml($exception->getMessage());
$exceptions = '';
if ($this->debug) {
$exceptions .= '<exceptions>';
foreach ($exception->toArray() as $e) {
$exceptions .= sprintf('<exception class="%s" message="%s"><traces>', $e['class'], $this->escapeXml($e['message']));
foreach ($e['trace'] as $trace) {
$exceptions .= '<trace>';
if ($trace['function']) {
$exceptions .= sprintf('at %s%s%s(%s) ', $trace['class'], $trace['type'], $trace['function'], $this->formatArgs($trace['args']));
}
if (isset($trace['file'], $trace['line'])) {
$exceptions .= $this->formatPath($trace['file'], $trace['line']);
}
$exceptions .= '</trace>';
}
$exceptions .= '</traces></exception>';
}
$exceptions .= '</exceptions>';
}
return <<<EOF
<?xml version="1.0" encoding="{$this->charset}" ?>
<problem xmlns="urn:ietf:rfc:7807">
<title>{$exception->getTitle()}</title>
<status>{$exception->getStatusCode()}</status>
<detail>{$message}</detail>
{$exceptions}
</problem>
EOF;
}
/**
* XML-encodes a string.
*/
private function escapeXml(string $str): string
{
return htmlspecialchars($str, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset);
}
private function formatPath(string $path, int $line): string
{
$file = $this->escapeXml(preg_match('#[^/\\\\]*+$#', $path, $file) ? $file[0] : $path);
return sprintf('in %s %s', $this->escapeXml($path), 0 < $line ? ' line '.$line : '');
}
/**
* Formats an array as a string.
*/
private function formatArgs(array $args): string
{
$result = [];
foreach ($args as $key => $item) {
if ('object' === $item[0]) {
$formattedValue = sprintf('object(%s)', $item[1]);
} elseif ('array' === $item[0]) {
$formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]);
} elseif ('null' === $item[0]) {
$formattedValue = 'null';
} elseif ('boolean' === $item[0]) {
$formattedValue = strtolower(var_export($item[1], true));
} elseif ('resource' === $item[0]) {
$formattedValue = 'resource';
} else {
$formattedValue = str_replace("\n", '', $this->escapeXml(var_export($item[1], true)));
}
$result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $this->escapeXml($key), $formattedValue);
}
return implode(', ', $result);
}
}

View File

@ -0,0 +1,36 @@
<?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\ErrorHandler\Exception;
/**
* Class (or Trait or Interface) Not Found Exception.
*
* @author Konstanton Myakshin <koc-dp@yandex.ru>
*/
class ClassNotFoundException extends FatalErrorException
{
public function __construct(string $message, \ErrorException $previous)
{
parent::__construct(
$message,
$previous->getCode(),
$previous->getSeverity(),
$previous->getFile(),
$previous->getLine(),
null,
true,
null,
$previous->getPrevious()
);
$this->setTrace($previous->getTrace());
}
}

View File

@ -0,0 +1,16 @@
<?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\ErrorHandler\Exception;
class ErrorRendererNotFoundException extends \RuntimeException
{
}

View File

@ -0,0 +1,77 @@
<?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\ErrorHandler\Exception;
/**
* Fatal Error Exception.
*
* @author Konstanton Myakshin <koc-dp@yandex.ru>
*/
class FatalErrorException extends \ErrorException
{
public function __construct(string $message, int $code, int $severity, string $filename, int $lineno, int $traceOffset = null, bool $traceArgs = true, array $trace = null, \Throwable $previous = null)
{
parent::__construct($message, $code, $severity, $filename, $lineno, $previous);
if (null !== $trace) {
if (!$traceArgs) {
foreach ($trace as &$frame) {
unset($frame['args'], $frame['this'], $frame);
}
}
$this->setTrace($trace);
} elseif (null !== $traceOffset) {
if (\function_exists('xdebug_get_function_stack')) {
$trace = xdebug_get_function_stack();
if (0 < $traceOffset) {
array_splice($trace, -$traceOffset);
}
foreach ($trace as &$frame) {
if (!isset($frame['type'])) {
// XDebug pre 2.1.1 doesn't currently set the call type key http://bugs.xdebug.org/view.php?id=695
if (isset($frame['class'])) {
$frame['type'] = '::';
}
} elseif ('dynamic' === $frame['type']) {
$frame['type'] = '->';
} elseif ('static' === $frame['type']) {
$frame['type'] = '::';
}
// XDebug also has a different name for the parameters array
if (!$traceArgs) {
unset($frame['params'], $frame['args']);
} elseif (isset($frame['params']) && !isset($frame['args'])) {
$frame['args'] = $frame['params'];
unset($frame['params']);
}
}
unset($frame);
$trace = array_reverse($trace);
} else {
$trace = [];
}
$this->setTrace($trace);
}
}
protected function setTrace($trace)
{
$traceReflector = new \ReflectionProperty('Exception', 'trace');
$traceReflector->setAccessible(true);
$traceReflector->setValue($this, $trace);
}
}

View File

@ -0,0 +1,51 @@
<?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\ErrorHandler\Exception;
/**
* Fatal Throwable Error.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class FatalThrowableError extends FatalErrorException
{
private $originalClassName;
public function __construct(\Throwable $e)
{
$this->originalClassName = \get_class($e);
if ($e instanceof \ParseError) {
$severity = E_PARSE;
} elseif ($e instanceof \TypeError) {
$severity = E_RECOVERABLE_ERROR;
} else {
$severity = E_ERROR;
}
\ErrorException::__construct(
$e->getMessage(),
$e->getCode(),
$severity,
$e->getFile(),
$e->getLine(),
$e->getPrevious()
);
$this->setTrace($e->getTrace());
}
public function getOriginalClassName(): string
{
return $this->originalClassName;
}
}

View File

@ -0,0 +1,380 @@
<?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\ErrorHandler\Exception;
use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
/**
* FlattenException wraps a PHP Error or Exception to be able to serialize it.
*
* Basically, this class removes all objects from the trace.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class FlattenException
{
private $title;
private $message;
private $code;
private $previous;
private $trace;
private $traceAsString;
private $class;
private $statusCode;
private $headers;
private $file;
private $line;
public static function create(\Exception $exception, $statusCode = null, array $headers = [])
{
return static::createFromThrowable($exception, $statusCode, $headers);
}
public static function createFromThrowable(\Throwable $exception, ?int $statusCode = null, array $headers = []): self
{
$e = new static();
$e->setMessage($exception->getMessage());
$e->setCode($exception->getCode());
if ($exception instanceof HttpExceptionInterface) {
$statusCode = $exception->getStatusCode();
$headers = array_merge($headers, $exception->getHeaders());
} elseif ($exception instanceof RequestExceptionInterface) {
$statusCode = 400;
}
if (null === $statusCode) {
$statusCode = 500;
}
if (class_exists(Response::class) && isset(Response::$statusTexts[$statusCode])) {
$title = Response::$statusTexts[$statusCode];
} else {
$title = 'Whoops, looks like something went wrong.';
}
$e->setTitle($title);
$e->setStatusCode($statusCode);
$e->setHeaders($headers);
$e->setTraceFromThrowable($exception);
$e->setClass($exception instanceof FatalThrowableError ? $exception->getOriginalClassName() : \get_class($exception));
$e->setFile($exception->getFile());
$e->setLine($exception->getLine());
$previous = $exception->getPrevious();
if ($previous instanceof \Throwable) {
$e->setPrevious(static::createFromThrowable($previous));
}
return $e;
}
public function toArray()
{
$exceptions = [];
foreach (array_merge([$this], $this->getAllPrevious()) as $exception) {
$exceptions[] = [
'message' => $exception->getMessage(),
'class' => $exception->getClass(),
'trace' => $exception->getTrace(),
];
}
return $exceptions;
}
public function getStatusCode()
{
return $this->statusCode;
}
/**
* @return $this
*/
public function setStatusCode($code)
{
$this->statusCode = $code;
return $this;
}
public function getHeaders()
{
return $this->headers;
}
/**
* @return $this
*/
public function setHeaders(array $headers)
{
$this->headers = $headers;
return $this;
}
public function getClass()
{
return $this->class;
}
/**
* @return $this
*/
public function setClass($class)
{
$this->class = 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class;
return $this;
}
public function getFile()
{
return $this->file;
}
/**
* @return $this
*/
public function setFile($file)
{
$this->file = $file;
return $this;
}
public function getLine()
{
return $this->line;
}
/**
* @return $this
*/
public function setLine($line)
{
$this->line = $line;
return $this;
}
public function getTitle()
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getMessage()
{
return $this->message;
}
/**
* @return $this
*/
public function setMessage($message)
{
if (false !== strpos($message, "class@anonymous\0")) {
$message = preg_replace_callback('/class@anonymous\x00.*?\.php0x?[0-9a-fA-F]++/', function ($m) {
return class_exists($m[0], false) ? get_parent_class($m[0]).'@anonymous' : $m[0];
}, $message);
}
$this->message = $message;
return $this;
}
public function getCode()
{
return $this->code;
}
/**
* @return $this
*/
public function setCode($code)
{
$this->code = $code;
return $this;
}
public function getPrevious()
{
return $this->previous;
}
/**
* @return $this
*/
public function setPrevious(self $previous)
{
$this->previous = $previous;
return $this;
}
public function getAllPrevious()
{
$exceptions = [];
$e = $this;
while ($e = $e->getPrevious()) {
$exceptions[] = $e;
}
return $exceptions;
}
public function getTrace()
{
return $this->trace;
}
/**
* @deprecated since 4.1, use {@see setTraceFromThrowable()} instead.
*/
public function setTraceFromException(\Exception $exception)
{
@trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.1, use "setTraceFromThrowable()" instead.', __METHOD__), E_USER_DEPRECATED);
$this->setTraceFromThrowable($exception);
}
public function setTraceFromThrowable(\Throwable $throwable)
{
$this->traceAsString = $throwable->getTraceAsString();
return $this->setTrace($throwable->getTrace(), $throwable->getFile(), $throwable->getLine());
}
/**
* @return $this
*/
public function setTrace($trace, $file, $line)
{
$this->trace = [];
$this->trace[] = [
'namespace' => '',
'short_class' => '',
'class' => '',
'type' => '',
'function' => '',
'file' => $file,
'line' => $line,
'args' => [],
];
foreach ($trace as $entry) {
$class = '';
$namespace = '';
if (isset($entry['class'])) {
$parts = explode('\\', $entry['class']);
$class = array_pop($parts);
$namespace = implode('\\', $parts);
}
$this->trace[] = [
'namespace' => $namespace,
'short_class' => $class,
'class' => isset($entry['class']) ? $entry['class'] : '',
'type' => isset($entry['type']) ? $entry['type'] : '',
'function' => isset($entry['function']) ? $entry['function'] : null,
'file' => isset($entry['file']) ? $entry['file'] : null,
'line' => isset($entry['line']) ? $entry['line'] : null,
'args' => isset($entry['args']) ? $this->flattenArgs($entry['args']) : [],
];
}
return $this;
}
private function flattenArgs($args, $level = 0, &$count = 0)
{
$result = [];
foreach ($args as $key => $value) {
if (++$count > 1e4) {
return ['array', '*SKIPPED over 10000 entries*'];
}
if ($value instanceof \__PHP_Incomplete_Class) {
// is_object() returns false on PHP<=7.1
$result[$key] = ['incomplete-object', $this->getClassNameFromIncomplete($value)];
} elseif (\is_object($value)) {
$result[$key] = ['object', \get_class($value)];
} elseif (\is_array($value)) {
if ($level > 10) {
$result[$key] = ['array', '*DEEP NESTED ARRAY*'];
} else {
$result[$key] = ['array', $this->flattenArgs($value, $level + 1, $count)];
}
} elseif (null === $value) {
$result[$key] = ['null', null];
} elseif (\is_bool($value)) {
$result[$key] = ['boolean', $value];
} elseif (\is_int($value)) {
$result[$key] = ['integer', $value];
} elseif (\is_float($value)) {
$result[$key] = ['float', $value];
} elseif (\is_resource($value)) {
$result[$key] = ['resource', get_resource_type($value)];
} else {
$result[$key] = ['string', (string) $value];
}
}
return $result;
}
private function getClassNameFromIncomplete(\__PHP_Incomplete_Class $value)
{
$array = new \ArrayObject($value);
return $array['__PHP_Incomplete_Class_Name'];
}
public function getTraceAsString()
{
return $this->traceAsString;
}
public function getAsString()
{
$message = '';
$next = false;
foreach (array_reverse(array_merge([$this], $this->getAllPrevious())) as $exception) {
if ($next) {
$message .= 'Next ';
} else {
$next = true;
}
$message .= $exception->getClass();
if ('' != $exception->getMessage()) {
$message .= ': '.$exception->getMessage();
}
$message .= ' in '.$exception->getFile().':'.$exception->getLine().
"\nStack trace:\n".$exception->getTraceAsString()."\n\n";
}
return rtrim($message);
}
}

View File

@ -0,0 +1,21 @@
<?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\ErrorHandler\Exception;
/**
* Out of memory exception.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class OutOfMemoryException extends FatalErrorException
{
}

View File

@ -0,0 +1,67 @@
<?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\ErrorHandler\Exception;
/**
* Data Object that represents a Silenced Error.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class SilencedErrorContext implements \JsonSerializable
{
public $count = 1;
private $severity;
private $file;
private $line;
private $trace;
public function __construct(int $severity, string $file, int $line, array $trace = [], int $count = 1)
{
$this->severity = $severity;
$this->file = $file;
$this->line = $line;
$this->trace = $trace;
$this->count = $count;
}
public function getSeverity()
{
return $this->severity;
}
public function getFile()
{
return $this->file;
}
public function getLine()
{
return $this->line;
}
public function getTrace()
{
return $this->trace;
}
public function JsonSerialize()
{
return [
'severity' => $this->severity,
'file' => $this->file,
'line' => $this->line,
'trace' => $this->trace,
'count' => $this->count,
];
}
}

View File

@ -0,0 +1,36 @@
<?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\ErrorHandler\Exception;
/**
* Undefined Function Exception.
*
* @author Konstanton Myakshin <koc-dp@yandex.ru>
*/
class UndefinedFunctionException extends FatalErrorException
{
public function __construct(string $message, \ErrorException $previous)
{
parent::__construct(
$message,
$previous->getCode(),
$previous->getSeverity(),
$previous->getFile(),
$previous->getLine(),
null,
true,
null,
$previous->getPrevious()
);
$this->setTrace($previous->getTrace());
}
}

View File

@ -0,0 +1,36 @@
<?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\ErrorHandler\Exception;
/**
* Undefined Method Exception.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class UndefinedMethodException extends FatalErrorException
{
public function __construct(string $message, \ErrorException $previous)
{
parent::__construct(
$message,
$previous->getCode(),
$previous->getSeverity(),
$previous->getFile(),
$previous->getLine(),
null,
true,
null,
$previous->getPrevious()
);
$this->setTrace($previous->getTrace());
}
}

View File

@ -0,0 +1,177 @@
<?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\ErrorHandler;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
/**
* ExceptionHandler converts an exception to a Response object.
*
* It is mostly useful in debug mode to replace the default PHP/XDebug
* output with something prettier and more useful.
*
* As this class is mainly used during Kernel boot, where nothing is yet
* available, the Response content is always HTML.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Nicolas Grekas <p@tchwork.com>
*
* @final
*/
class ExceptionHandler
{
private $charset;
private $errorRenderer;
private $handler;
private $caughtBuffer;
private $caughtLength;
/**
* Registers the exception handler.
*
* @param bool $debug Enable/disable debug mode, where the stack trace is displayed
* @param string|null $charset The charset used by exception messages
* @param string|null $fileLinkFormat The IDE link template
*
* @return static
*/
public static function register($debug = true, $charset = null, $fileLinkFormat = null)
{
$handler = new static($debug, $charset, $fileLinkFormat);
$prev = set_exception_handler([$handler, 'handle']);
if (\is_array($prev) && $prev[0] instanceof ErrorHandler) {
restore_exception_handler();
$prev[0]->setExceptionHandler([$handler, 'handle']);
}
return $handler;
}
public function __construct(bool $debug = true, string $charset = null, $fileLinkFormat = null, HtmlErrorRenderer $errorRenderer = null)
{
$this->charset = $charset ?: ini_get('default_charset') ?: 'UTF-8';
$this->errorRenderer = $errorRenderer ?? new HtmlErrorRenderer($debug, $this->charset, $fileLinkFormat);
}
/**
* Sets the format for links to source files.
*
* @param string|FileLinkFormatter $fileLinkFormat The format for links to source files
*
* @return string The previous file link format
*/
public function setFileLinkFormat($fileLinkFormat)
{
return $this->errorRenderer->setFileLinkFormat($fileLinkFormat);
}
/**
* Sets a user exception handler.
*
* @param callable $handler An handler that will be called on Exception
*
* @return callable|null The previous exception handler if any
*/
public function setHandler(callable $handler = null)
{
$old = $this->handler;
$this->handler = $handler;
return $old;
}
/**
* Sends a response for the given Exception.
*
* To be as fail-safe as possible, the exception is first handled
* by our simple exception handler, then by the user exception handler.
* The latter takes precedence and any output from the former is cancelled,
* if and only if nothing bad happens in this handling path.
*/
public function handle(\Exception $exception)
{
if (null === $this->handler || $exception instanceof OutOfMemoryException) {
$this->sendPhpResponse($exception);
return;
}
$caughtLength = $this->caughtLength = 0;
ob_start(function ($buffer) {
$this->caughtBuffer = $buffer;
return '';
});
$this->sendPhpResponse($exception);
while (null === $this->caughtBuffer && ob_end_flush()) {
// Empty loop, everything is in the condition
}
if (isset($this->caughtBuffer[0])) {
ob_start(function ($buffer) {
if ($this->caughtLength) {
// use substr_replace() instead of substr() for mbstring overloading resistance
$cleanBuffer = substr_replace($buffer, '', 0, $this->caughtLength);
if (isset($cleanBuffer[0])) {
$buffer = $cleanBuffer;
}
}
return $buffer;
});
echo $this->caughtBuffer;
$caughtLength = ob_get_length();
}
$this->caughtBuffer = null;
try {
($this->handler)($exception);
$this->caughtLength = $caughtLength;
} catch (\Exception $e) {
if (!$caughtLength) {
// All handlers failed. Let PHP handle that now.
throw $exception;
}
}
}
/**
* Sends the error associated with the given Exception as a plain PHP response.
*
* This method uses plain PHP functions like header() and echo to output
* the response.
*
* @param \Exception|FlattenException $exception An \Exception or FlattenException instance
*/
public function sendPhpResponse($exception)
{
if (!$exception instanceof FlattenException) {
$exception = FlattenException::create($exception);
}
if (!headers_sent()) {
header(sprintf('HTTP/1.0 %s', $exception->getStatusCode()));
foreach ($exception->getHeaders() as $name => $value) {
header($name.': '.$value, false);
}
header('Content-Type: text/html; charset='.$this->charset);
}
echo $this->errorRenderer->render($exception);
}
}

View File

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

View File

@ -0,0 +1,32 @@
<?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\ErrorHandler\FatalErrorHandler;
use Symfony\Component\ErrorHandler\Exception\FatalErrorException;
/**
* Attempts to convert fatal errors to exceptions.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
interface FatalErrorHandlerInterface
{
/**
* Attempts to convert an error into an exception.
*
* @param array $error An array as returned by error_get_last()
* @param FatalErrorException $exception A FatalErrorException instance
*
* @return FatalErrorException|null A FatalErrorException instance if the class is able to convert the error, null otherwise
*/
public function handleError(array $error, FatalErrorException $exception);
}

View File

@ -0,0 +1,84 @@
<?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\ErrorHandler\FatalErrorHandler;
use Symfony\Component\ErrorHandler\Exception\FatalErrorException;
use Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException;
/**
* ErrorHandler for undefined functions.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class UndefinedFunctionFatalErrorHandler implements FatalErrorHandlerInterface
{
/**
* {@inheritdoc}
*/
public function handleError(array $error, FatalErrorException $exception)
{
$messageLen = \strlen($error['message']);
$notFoundSuffix = '()';
$notFoundSuffixLen = \strlen($notFoundSuffix);
if ($notFoundSuffixLen > $messageLen) {
return;
}
if (0 !== substr_compare($error['message'], $notFoundSuffix, -$notFoundSuffixLen)) {
return;
}
$prefix = 'Call to undefined function ';
$prefixLen = \strlen($prefix);
if (0 !== strpos($error['message'], $prefix)) {
return;
}
$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".', $functionName, $namespacePrefix);
} else {
$functionName = $fullyQualifiedFunctionName;
$message = sprintf('Attempted to call function "%s" from the global namespace.', $functionName);
}
$candidates = [];
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) {
sort($candidates);
$last = array_pop($candidates).'"?';
if ($candidates) {
$candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last;
} else {
$candidates = '"'.$last;
}
$message .= "\nDid you mean to call ".$candidates;
}
return new UndefinedFunctionException($message, $exception);
}
}

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\ErrorHandler\FatalErrorHandler;
use Symfony\Component\ErrorHandler\Exception\FatalErrorException;
use Symfony\Component\ErrorHandler\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 an undefined method named "%s" of class "%s".', $methodName, $className);
if (!class_exists($className) || null === $methods = get_class_methods($className)) {
// failed to get the class or its methods on which an unknown method was called (for example on an anonymous class)
return new UndefinedMethodException($message, $exception);
}
$candidates = [];
foreach ($methods as $definedMethodName) {
$lev = levenshtein($methodName, $definedMethodName);
if ($lev <= \strlen($methodName) / 3 || false !== strpos($definedMethodName, $methodName)) {
$candidates[] = $definedMethodName;
}
}
if ($candidates) {
sort($candidates);
$last = array_pop($candidates).'"?';
if ($candidates) {
$candidates = 'e.g. "'.implode('", "', $candidates).'" or "'.$last;
} else {
$candidates = '"'.$last;
}
$message .= "\nDid you mean to call ".$candidates;
}
return new UndefinedMethodException($message, $exception);
}
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2019 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,12 @@
ErrorHandler Component
======================
The ErrorHandler component provides tools to manage and display errors and exceptions.
Resources
---------
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/symfony/issues) and
[send Pull Requests](https://github.com/symfony/symfony/pulls)
in the [main Symfony repository](https://github.com/symfony/symfony)

View File

@ -0,0 +1,51 @@
<?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\ErrorHandler\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\ErrorHandler\DependencyInjection\ErrorHandlerPass;
use Symfony\Component\ErrorHandler\DependencyInjection\ErrorRenderer;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorHandler\ErrorRenderer\JsonErrorRenderer;
class ErrorHandlerPassTest extends TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$container->setParameter('kernel.debug', true);
$definition = $container->register('error_handler.error_renderer', ErrorRenderer::class)
->addArgument([])
;
$container->register('error_handler.renderer.html', HtmlErrorRenderer::class)
->addTag('error_handler.renderer')
;
$container->register('error_handler.renderer.json', JsonErrorRenderer::class)
->addTag('error_handler.renderer')
;
(new ErrorHandlerPass())->process($container);
$serviceLocatorDefinition = $container->getDefinition((string) $definition->getArgument(0));
$this->assertSame(ServiceLocator::class, $serviceLocatorDefinition->getClass());
$expected = [
'html' => new ServiceClosureArgument(new Reference('error_handler.renderer.html')),
'json' => new ServiceClosureArgument(new Reference('error_handler.renderer.json')),
];
$this->assertEquals($expected, $serviceLocatorDefinition->getArgument(0));
}
}

View File

@ -0,0 +1,67 @@
<?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\ErrorHandler\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\DependencyInjection\ErrorRenderer;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
class ErrorRendererTest extends TestCase
{
/**
* @expectedException \Symfony\Component\ErrorHandler\Exception\ErrorRendererNotFoundException
* @expectedExceptionMessage No error renderer found for format "foo".
*/
public function testInvalidErrorRenderer()
{
$container = $this->getMockBuilder('Psr\Container\ContainerInterface')->getMock();
$container->expects($this->once())->method('has')->with('foo')->willReturn(false);
$exception = FlattenException::create(new \Exception('Foo'));
(new ErrorRenderer($container))->render($exception, 'foo');
}
public function testCustomErrorRenderer()
{
$container = $this->getMockBuilder('Psr\Container\ContainerInterface')->getMock();
$container
->expects($this->once())
->method('has')
->with('foo')
->willReturn(true)
;
$container
->expects($this->once())
->method('get')
->willReturn(new FooErrorRenderer())
;
$errorRenderer = new ErrorRenderer($container);
$exception = FlattenException::create(new \RuntimeException('Foo'));
$this->assertSame('Foo', $errorRenderer->render($exception, 'foo'));
}
}
class FooErrorRenderer implements ErrorRendererInterface
{
public static function getFormat(): string
{
return 'foo';
}
public function render(FlattenException $exception): string
{
return $exception->getMessage();
}
}

View File

@ -9,16 +9,16 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Tests;
namespace Symfony\Component\ErrorHandler\Tests;
use PHPUnit\Framework\TestCase;
use Psr\Log\LogLevel;
use Psr\Log\NullLogger;
use Symfony\Component\Debug\BufferingLogger;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\Exception\SilencedErrorContext;
use Symfony\Component\Debug\Tests\Fixtures\ErrorHandlerThatUsesThePreviousOne;
use Symfony\Component\Debug\Tests\Fixtures\LoggerThatSetAnErrorHandler;
use Symfony\Component\ErrorHandler\BufferingLogger;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext;
use Symfony\Component\ErrorHandler\Tests\Fixtures\ErrorHandlerThatUsesThePreviousOne;
use Symfony\Component\ErrorHandler\Tests\Fixtures\LoggerThatSetAnErrorHandler;
/**
* ErrorHandlerTest.
@ -33,7 +33,7 @@ class ErrorHandlerTest extends TestCase
$handler = ErrorHandler::register();
try {
$this->assertInstanceOf('Symfony\Component\Debug\ErrorHandler', $handler);
$this->assertInstanceOf('Symfony\Component\ErrorHandler\ErrorHandler', $handler);
$this->assertSame($handler, ErrorHandler::register());
$newHandler = new ErrorHandler();
@ -151,21 +151,21 @@ class ErrorHandlerTest extends TestCase
$handler->setDefaultLogger($logger, [E_USER_NOTICE => LogLevel::CRITICAL]);
$loggers = [
E_DEPRECATED => [null, LogLevel::INFO],
E_USER_DEPRECATED => [null, LogLevel::INFO],
E_NOTICE => [$logger, LogLevel::WARNING],
E_USER_NOTICE => [$logger, LogLevel::CRITICAL],
E_STRICT => [null, LogLevel::WARNING],
E_WARNING => [null, LogLevel::WARNING],
E_USER_WARNING => [null, LogLevel::WARNING],
E_COMPILE_WARNING => [null, LogLevel::WARNING],
E_CORE_WARNING => [null, LogLevel::WARNING],
E_USER_ERROR => [null, LogLevel::CRITICAL],
E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL],
E_COMPILE_ERROR => [null, LogLevel::CRITICAL],
E_PARSE => [null, LogLevel::CRITICAL],
E_ERROR => [null, LogLevel::CRITICAL],
E_COMPILE_WARNING => [null, LogLevel::WARNING],
E_CORE_ERROR => [null, LogLevel::CRITICAL],
E_CORE_WARNING => [null, LogLevel::WARNING],
E_DEPRECATED => [null, LogLevel::INFO],
E_ERROR => [null, LogLevel::CRITICAL],
E_NOTICE => [$logger, LogLevel::WARNING],
E_PARSE => [null, LogLevel::CRITICAL],
E_RECOVERABLE_ERROR => [null, LogLevel::CRITICAL],
E_STRICT => [null, LogLevel::WARNING],
E_USER_DEPRECATED => [null, LogLevel::INFO],
E_USER_ERROR => [null, LogLevel::CRITICAL],
E_USER_NOTICE => [$logger, LogLevel::CRITICAL],
E_USER_WARNING => [null, LogLevel::WARNING],
E_WARNING => [null, LogLevel::WARNING],
];
$this->assertSame($loggers, $handler->setLoggers([]));
} finally {
@ -375,21 +375,21 @@ class ErrorHandlerTest extends TestCase
$handler = new ErrorHandler($bootLogger);
$loggers = [
E_DEPRECATED => [$bootLogger, LogLevel::INFO],
E_USER_DEPRECATED => [$bootLogger, LogLevel::INFO],
E_NOTICE => [$bootLogger, LogLevel::WARNING],
E_USER_NOTICE => [$bootLogger, LogLevel::WARNING],
E_STRICT => [$bootLogger, LogLevel::WARNING],
E_WARNING => [$bootLogger, LogLevel::WARNING],
E_USER_WARNING => [$bootLogger, LogLevel::WARNING],
E_COMPILE_WARNING => [$bootLogger, LogLevel::WARNING],
E_CORE_WARNING => [$bootLogger, LogLevel::WARNING],
E_USER_ERROR => [$bootLogger, LogLevel::CRITICAL],
E_RECOVERABLE_ERROR => [$bootLogger, LogLevel::CRITICAL],
E_COMPILE_ERROR => [$bootLogger, LogLevel::CRITICAL],
E_PARSE => [$bootLogger, LogLevel::CRITICAL],
E_ERROR => [$bootLogger, LogLevel::CRITICAL],
E_COMPILE_WARNING => [$bootLogger, LogLevel::WARNING],
E_CORE_ERROR => [$bootLogger, LogLevel::CRITICAL],
E_CORE_WARNING => [$bootLogger, LogLevel::WARNING],
E_DEPRECATED => [$bootLogger, LogLevel::INFO],
E_ERROR => [$bootLogger, LogLevel::CRITICAL],
E_NOTICE => [$bootLogger, LogLevel::WARNING],
E_PARSE => [$bootLogger, LogLevel::CRITICAL],
E_RECOVERABLE_ERROR => [$bootLogger, LogLevel::CRITICAL],
E_STRICT => [$bootLogger, LogLevel::WARNING],
E_USER_DEPRECATED => [$bootLogger, LogLevel::INFO],
E_USER_ERROR => [$bootLogger, LogLevel::CRITICAL],
E_USER_NOTICE => [$bootLogger, LogLevel::WARNING],
E_USER_WARNING => [$bootLogger, LogLevel::WARNING],
E_WARNING => [$bootLogger, LogLevel::WARNING],
];
$this->assertSame($loggers, $handler->setLoggers([]));
@ -490,7 +490,7 @@ class ErrorHandlerTest extends TestCase
$handler->handleException($exception);
$this->assertInstanceOf('Symfony\Component\Debug\Exception\ClassNotFoundException', $args[0]);
$this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\ClassNotFoundException', $args[0]);
$this->assertStringStartsWith("Attempted to load class \"IReallyReallyDoNotExistAnywhereInTheRepositoryISwear\" from the global namespace.\nDid you forget a \"use\" statement", $args[0]->getMessage());
}

View File

@ -0,0 +1,62 @@
<?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\ErrorHandler\Tests\ErrorRenderer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
class ErrorRendererTest extends TestCase
{
/**
* @expectedException \Symfony\Component\ErrorHandler\Exception\ErrorRendererNotFoundException
* @expectedExceptionMessage No error renderer found for format "foo".
*/
public function testErrorRendererNotFound()
{
$exception = FlattenException::create(new \Exception('foo'));
(new ErrorRenderer([]))->render($exception, 'foo');
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Error renderer "stdClass" must implement "Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface".
*/
public function testInvalidErrorRenderer()
{
$exception = FlattenException::create(new \Exception('foo'));
(new ErrorRenderer([new \stdClass()]))->render($exception, 'foo');
}
public function testCustomErrorRenderer()
{
$renderers = [new FooErrorRenderer()];
$errorRenderer = new ErrorRenderer($renderers);
$exception = FlattenException::create(new \RuntimeException('Foo'));
$this->assertSame('Foo', $errorRenderer->render($exception, 'foo'));
}
}
class FooErrorRenderer implements ErrorRendererInterface
{
public static function getFormat(): string
{
return 'foo';
}
public function render(FlattenException $exception): string
{
return $exception->getMessage();
}
}

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\ErrorHandler\Tests\ErrorRenderer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
class HtmlErrorRendererTest extends TestCase
{
public function testRender()
{
$exception = FlattenException::create(new \RuntimeException('Foo'));
$expected = '<!DOCTYPE html>%A<html>%A<head>%A<title>Internal Server Error</title>%A<h1 class="break-long-words exception-message">Foo</h1>%A<abbr title="RuntimeException">RuntimeException</abbr>%A';
$this->assertStringMatchesFormat($expected, (new HtmlErrorRenderer())->render($exception));
}
}

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\ErrorHandler\Tests\ErrorRenderer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorRenderer\JsonErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
class JsonErrorRendererTest extends TestCase
{
public function testRender()
{
$exception = FlattenException::create(new \RuntimeException('Foo'));
$expected = '{"title":"Internal Server Error","status":500,"detail":"Foo","exceptions":[{"message":"Foo","class":"RuntimeException","trace":';
$this->assertStringStartsWith($expected, (new JsonErrorRenderer())->render($exception));
}
}

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\ErrorHandler\Tests\ErrorRenderer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorRenderer\TxtErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
class TxtErrorRendererTest extends TestCase
{
public function testRender()
{
$exception = FlattenException::create(new \RuntimeException('Foo'));
$expected = '[title] Internal Server Error%A[status] 500%A[detail] Foo%A[1] RuntimeException: Foo%A';
$this->assertStringMatchesFormat($expected, (new TxtErrorRenderer())->render($exception));
}
}

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\ErrorHandler\Tests\ErrorRenderer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\ErrorRenderer\XmlErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
class XmlErrorRendererTest extends TestCase
{
public function testRender()
{
$exception = FlattenException::create(new \RuntimeException('Foo'));
$expected = '<?xml version="1.0" encoding="UTF-8" ?>%A<problem xmlns="urn:ietf:rfc:7807">%A<title>Internal Server Error</title>%A<status>500</status>%A<detail>Foo</detail>%A';
$this->assertStringMatchesFormat($expected, (new XmlErrorRenderer())->render($exception));
}
}

View File

@ -9,11 +9,11 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Tests\Exception;
namespace Symfony\Component\ErrorHandler\Tests\Exception;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\Exception\FatalThrowableError;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\FatalThrowableError;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

View File

@ -9,11 +9,11 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Tests;
namespace Symfony\Component\ErrorHandler\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\Exception\OutOfMemoryException;
use Symfony\Component\Debug\ExceptionHandler;
use Symfony\Component\ErrorHandler\Exception\OutOfMemoryException;
use Symfony\Component\ErrorHandler\ExceptionHandler;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -39,7 +39,7 @@ class ExceptionHandlerTest extends TestCase
$handler->sendPhpResponse(new \RuntimeException('Foo'));
$response = ob_get_clean();
$this->assertContains('Whoops, looks like something went wrong.', $response);
$this->assertContains('The server returned a "500 Internal Server Error".', $response);
$this->assertNotContains('<div class="trace trace-as-html">', $response);
$handler = new ExceptionHandler(true);
@ -69,7 +69,7 @@ content="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg"
$handler->sendPhpResponse(new NotFoundHttpException('Foo'));
$response = ob_get_clean();
$this->assertContains('Sorry, the page you are looking for could not be found.', $response);
$this->assertContains('The server returned a "404 Not Found".', $response);
$expectedHeaders = [
['HTTP/1.0 404', true, null],
@ -110,7 +110,7 @@ content="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg"
{
$exception = new \Exception('foo');
$handler = $this->getMockBuilder('Symfony\Component\Debug\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock();
$handler = $this->getMockBuilder('Symfony\Component\ErrorHandler\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock();
$handler
->expects($this->exactly(2))
->method('sendPhpResponse');
@ -128,7 +128,7 @@ content="0;url=data:text/html;base64,PHNjcmlwdD5hbGVydCgndGVzdDMnKTwvc2NyaXB0Pg"
{
$exception = new OutOfMemoryException('foo', 0, E_ERROR, __FILE__, __LINE__);
$handler = $this->getMockBuilder('Symfony\Component\Debug\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock();
$handler = $this->getMockBuilder('Symfony\Component\ErrorHandler\ExceptionHandler')->setMethods(['sendPhpResponse'])->getMock();
$handler
->expects($this->once())
->method('sendPhpResponse');

View File

@ -9,13 +9,13 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Tests\FatalErrorHandler;
namespace Symfony\Component\ErrorHandler\Tests\FatalErrorHandler;
use Composer\Autoload\ClassLoader as ComposerClassLoader;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\DebugClassLoader;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\FatalErrorHandler\ClassNotFoundFatalErrorHandler;
use Symfony\Component\ErrorHandler\Exception\FatalErrorException;
use Symfony\Component\ErrorHandler\FatalErrorHandler\ClassNotFoundFatalErrorHandler;
class ClassNotFoundFatalErrorHandlerTest extends TestCase
{
@ -32,7 +32,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase
}
if ($function[0] instanceof ComposerClassLoader) {
$function[0]->add('Symfony_Component_Debug_Tests_Fixtures', \dirname(\dirname(\dirname(\dirname(\dirname(__DIR__))))));
$function[0]->add('Symfony_Component_ErrorHandler_Tests_Fixtures', \dirname(\dirname(\dirname(\dirname(\dirname(__DIR__))))));
break;
}
}
@ -60,7 +60,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase
array_map('spl_autoload_register', $autoloaders);
}
$this->assertInstanceOf('Symfony\Component\Debug\Exception\ClassNotFoundException', $exception);
$this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\ClassNotFoundException', $exception);
$this->assertSame($translatedMessage, $exception->getMessage());
$this->assertSame($error['type'], $exception->getSeverity());
$this->assertSame($error['file'], $exception->getFile());
@ -70,7 +70,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase
public function provideClassNotFoundData()
{
$autoloader = new ComposerClassLoader();
$autoloader->add('Symfony\Component\Debug\Exception\\', realpath(__DIR__.'/../../Exception'));
$autoloader->add('Symfony\Component\ErrorHandler\Exception\\', realpath(__DIR__.'/../../Exception'));
$debugClassLoader = new DebugClassLoader([$autoloader, 'loadClass']);
@ -98,9 +98,9 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => 'Class \'UndefinedFunctionException\' not found',
'message' => 'Class \'UndefinedFuncException\' not found',
],
"Attempted to load class \"UndefinedFunctionException\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?",
"Attempted to load class \"UndefinedFuncException\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Tests\Fixtures\UndefinedFuncException\"?",
],
[
[
@ -109,7 +109,16 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase
'file' => 'foo.php',
'message' => 'Class \'PEARClass\' not found',
],
"Attempted to load class \"PEARClass\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony_Component_Debug_Tests_Fixtures_PEARClass\"?",
"Attempted to load class \"PEARClass\" from the global namespace.\nDid you forget a \"use\" statement for \"Symfony_Component_ErrorHandler_Tests_Fixtures_PEARClass\"?",
],
[
[
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => 'Class \'Foo\\Bar\\UndefinedFuncException\' not found',
],
"Attempted to load class \"UndefinedFuncException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Tests\Fixtures\UndefinedFuncException\"?",
],
[
[
@ -118,16 +127,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase
'file' => 'foo.php',
'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found',
],
"Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?",
],
[
[
'type' => 1,
'line' => 12,
'file' => 'foo.php',
'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found',
],
"Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?",
"Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException\"?",
[$autoloader, 'loadClass'],
],
[
@ -137,7 +137,7 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase
'file' => 'foo.php',
'message' => 'Class \'Foo\\Bar\\UndefinedFunctionException\' not found',
],
"Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\Debug\Exception\UndefinedFunctionException\"?",
"Attempted to load class \"UndefinedFunctionException\" from namespace \"Foo\Bar\".\nDid you forget a \"use\" statement for \"Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException\"?",
[$debugClassLoader, 'loadClass'],
],
[
@ -171,6 +171,6 @@ class ClassNotFoundFatalErrorHandlerTest extends TestCase
$handler = new ClassNotFoundFatalErrorHandler();
$exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line']));
$this->assertInstanceOf('Symfony\Component\Debug\Exception\ClassNotFoundException', $exception);
$this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\ClassNotFoundException', $exception);
}
}

View File

@ -9,11 +9,11 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Tests\FatalErrorHandler;
namespace Symfony\Component\ErrorHandler\Tests\FatalErrorHandler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedFunctionFatalErrorHandler;
use Symfony\Component\ErrorHandler\Exception\FatalErrorException;
use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedFunctionFatalErrorHandler;
class UndefinedFunctionFatalErrorHandlerTest extends TestCase
{
@ -25,7 +25,7 @@ class UndefinedFunctionFatalErrorHandlerTest extends TestCase
$handler = new UndefinedFunctionFatalErrorHandler();
$exception = $handler->handleError($error, new FatalErrorException('', 0, $error['type'], $error['file'], $error['line']));
$this->assertInstanceOf('Symfony\Component\Debug\Exception\UndefinedFunctionException', $exception);
$this->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\UndefinedFunctionException', $exception);
// class names are case insensitive and PHP do not return the same
$this->assertSame(strtolower($translatedMessage), strtolower($exception->getMessage()));
$this->assertSame($error['type'], $exception->getSeverity());
@ -43,7 +43,7 @@ class UndefinedFunctionFatalErrorHandlerTest extends TestCase
'file' => 'foo.php',
'message' => 'Call to undefined function test_namespaced_function()',
],
"Attempted to call function \"test_namespaced_function\" from the global namespace.\nDid you mean to call \"\\symfony\\component\\debug\\tests\\fatalerrorhandler\\test_namespaced_function\"?",
"Attempted to call function \"test_namespaced_function\" from the global namespace.\nDid you mean to call \"\\symfony\\component\\errorhandler\\tests\\fatalerrorhandler\\test_namespaced_function\"?",
],
[
[
@ -52,7 +52,7 @@ class UndefinedFunctionFatalErrorHandlerTest extends TestCase
'file' => 'foo.php',
'message' => 'Call to undefined function Foo\\Bar\\Baz\\test_namespaced_function()',
],
"Attempted to call function \"test_namespaced_function\" from namespace \"Foo\\Bar\\Baz\".\nDid you mean to call \"\\symfony\\component\\debug\\tests\\fatalerrorhandler\\test_namespaced_function\"?",
"Attempted to call function \"test_namespaced_function\" from namespace \"Foo\\Bar\\Baz\".\nDid you mean to call \"\\symfony\\component\\errorhandler\\tests\\fatalerrorhandler\\test_namespaced_function\"?",
],
[
[

View File

@ -9,11 +9,11 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Tests\FatalErrorHandler;
namespace Symfony\Component\ErrorHandler\Tests\FatalErrorHandler;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\Exception\FatalErrorException;
use Symfony\Component\Debug\FatalErrorHandler\UndefinedMethodFatalErrorHandler;
use Symfony\Component\ErrorHandler\Exception\FatalErrorException;
use Symfony\Component\ErrorHandler\FatalErrorHandler\UndefinedMethodFatalErrorHandler;
class UndefinedMethodFatalErrorHandlerTest extends TestCase
{
@ -25,7 +25,7 @@ class UndefinedMethodFatalErrorHandlerTest extends TestCase
$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->assertInstanceOf('Symfony\Component\ErrorHandler\Exception\UndefinedMethodException', $exception);
$this->assertSame($translatedMessage, $exception->getMessage());
$this->assertSame($error['type'], $exception->getSeverity());
$this->assertSame($error['file'], $exception->getFile());

View File

@ -1,6 +1,6 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
namespace Symfony\Component\ErrorHandler\Tests\Fixtures;
class ErrorHandlerThatUsesThePreviousOne
{

View File

@ -1,8 +1,8 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
namespace Symfony\Component\ErrorHandler\Tests\Fixtures;
use Symfony\Component\Debug\BufferingLogger;
use Symfony\Component\ErrorHandler\BufferingLogger;
class LoggerThatSetAnErrorHandler extends BufferingLogger
{

View File

@ -0,0 +1,5 @@
<?php
class Symfony_Component_ErrorHandler_Tests_Fixtures_PEARClass
{
}

View File

@ -1,6 +1,6 @@
<?php
namespace Symfony\Component\Debug\Tests\Fixtures;
namespace Symfony\Component\ErrorHandler\Tests\Fixtures;
class ToStringThrower
{

View File

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

View File

@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug;
namespace Symfony\Component\ErrorHandler;
function headers_sent()
{
@ -21,7 +21,7 @@ function header($str, $replace = true, $status = null)
Tests\testHeader($str, $replace, $status);
}
namespace Symfony\Component\Debug\Tests;
namespace Symfony\Component\ErrorHandler\Tests;
function testHeader()
{

View File

@ -9,9 +9,9 @@
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug\Tests;
namespace Symfony\Component\ErrorHandler\Tests;
use Symfony\Component\Debug\ExceptionHandler;
use Symfony\Component\ErrorHandler\ExceptionHandler;
class MockExceptionHandler extends ExceptionHandler
{

View File

@ -5,7 +5,7 @@ display_errors=0
--FILE--
<?php
namespace Symfony\Component\Debug;
namespace Symfony\Component\ErrorHandler;
$vendor = __DIR__;
while (!file_exists($vendor.'/vendor')) {
@ -26,9 +26,9 @@ if (true) {
?>
--EXPECTF--
object(Symfony\Component\Debug\Exception\ClassNotFoundException)#%d (8) {
object(Symfony\Component\ErrorHandler\Exception\ClassNotFoundException)#%d (8) {
["message":protected]=>
string(131) "Attempted to load class "missing" from namespace "Symfony\Component\Debug".
string(138) "Attempted to load class "missing" from namespace "Symfony\Component\ErrorHandler".
Did you forget a "use" statement for another namespace?"
["string":"Exception":private]=>
string(0) ""

View File

@ -3,7 +3,7 @@ Test rethrowing in custom exception handler
--FILE--
<?php
namespace Symfony\Component\Debug;
namespace Symfony\Component\ErrorHandler;
$vendor = __DIR__;
while (!file_exists($vendor.'/vendor')) {

View File

@ -3,7 +3,9 @@ Test catching fatal errors when handlers are nested
--FILE--
<?php
namespace Symfony\Component\Debug;
namespace Symfony\Component\ErrorHandler;
use Symfony\Component\Debug\Debug;
$vendor = __DIR__;
while (!file_exists($vendor.'/vendor')) {
@ -35,8 +37,8 @@ array(1) {
[0]=>
string(37) "Error and exception handlers do match"
}
object(Symfony\Component\Debug\Exception\FatalErrorException)#%d (%d) {
object(Symfony\Component\ErrorHandler\Exception\FatalErrorException)#%d (%d) {
["message":protected]=>
string(179) "Error: Class Symfony\Component\Debug\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)"
string(186) "Error: Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)"
%a
}

View File

@ -0,0 +1,42 @@
{
"name": "symfony/error-handler",
"type": "library",
"description": "Symfony ErrorHandler Component",
"keywords": [],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": "^7.1.3",
"psr/log": "~1.0"
},
"require-dev": {
"symfony/debug": "^4.4",
"symfony/dependency-injection": "^4.4",
"symfony/http-kernel": "^4.4"
},
"conflict": {
"symfony/http-kernel": "<4.4"
},
"autoload": {
"psr-4": { "Symfony\\Component\\ErrorHandler\\": "" },
"exclude-from-classmap": [
"/Tests/"
]
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "4.4-dev"
}
}
}

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/5.2/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony ErrorHandler Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\HttpKernel\DataCollector;
use Symfony\Component\Debug\Exception\SilencedErrorContext;
use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;

View File

@ -15,8 +15,11 @@ use Psr\Log\LoggerInterface;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\ExceptionHandler;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\ErrorRendererNotFoundException;
use Symfony\Component\ErrorHandler\ExceptionHandler;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
@ -43,6 +46,7 @@ class DebugHandlersListener implements EventSubscriberInterface
private $fileLinkFormat;
private $scope;
private $charset;
private $errorRenderer;
private $firstCall = true;
private $hasTerminatedWithException;
@ -55,7 +59,7 @@ class DebugHandlersListener implements EventSubscriberInterface
* @param string|FileLinkFormatter|null $fileLinkFormat The format for links to source files
* @param bool $scope Enables/disables scoping mode
*/
public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = E_ALL, ?int $throwAt = E_ALL, bool $scream = true, $fileLinkFormat = null, bool $scope = true, string $charset = null)
public function __construct(callable $exceptionHandler = null, LoggerInterface $logger = null, $levels = E_ALL, ?int $throwAt = E_ALL, bool $scream = true, $fileLinkFormat = null, bool $scope = true, string $charset = null, ErrorRenderer $errorRenderer = null)
{
$this->exceptionHandler = $exceptionHandler;
$this->logger = $logger;
@ -65,6 +69,7 @@ class DebugHandlersListener implements EventSubscriberInterface
$this->fileLinkFormat = $fileLinkFormat;
$this->scope = $scope;
$this->charset = $charset;
$this->errorRenderer = $errorRenderer;
}
/**
@ -162,10 +167,17 @@ class DebugHandlersListener implements EventSubscriberInterface
$debug = $this->scream && $this->scope;
$controller = function (Request $request) use ($debug) {
$e = $request->attributes->get('exception');
$handler = new ExceptionHandler($debug, $this->charset, $this->fileLinkFormat);
if (null === $this->errorRenderer) {
$this->errorRenderer = new ErrorRenderer([new HtmlErrorRenderer($debug, $this->charset, $this->fileLinkFormat)]);
}
return new Response($handler->getHtml($e), $e->getStatusCode(), $e->getHeaders());
$e = $request->attributes->get('exception');
try {
return new Response($this->errorRenderer->render($e, $request->getRequestFormat()), $e->getStatusCode(), $e->getHeaders());
} catch (ErrorRendererNotFoundException $_) {
return new Response($this->errorRenderer->render($e), $e->getStatusCode(), $e->getHeaders());
}
};
(new ExceptionListener($controller, $this->logger, $debug))->onKernelException($event);

View File

@ -12,11 +12,10 @@
namespace Symfony\Component\HttpKernel\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\HttpKernelInterface;

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\HttpKernel\Tests\DataCollector;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector;

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\HttpKernel\Tests\DataCollector;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Debug\Exception\SilencedErrorContext;
use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
@ -106,7 +106,7 @@ class LoggerDataCollectorTest extends TestCase
$logs = array_map(function ($v) {
if (isset($v['context']['exception'])) {
$e = &$v['context']['exception'];
$e = isset($e["\0*\0message"]) ? [$e["\0*\0message"], $e["\0*\0severity"]] : [$e["\0Symfony\Component\Debug\Exception\SilencedErrorContext\0severity"]];
$e = isset($e["\0*\0message"]) ? [$e["\0*\0message"], $e["\0*\0severity"]] : [$e["\0Symfony\Component\ErrorHandler\Exception\SilencedErrorContext\0severity"]];
}
return $v;

View File

@ -19,8 +19,8 @@ use Symfony\Component\Console\Event\ConsoleEvent;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\ExceptionHandler;
use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\ErrorHandler\ExceptionHandler;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\KernelEvent;

View File

@ -17,9 +17,10 @@
],
"require": {
"php": "^7.1.3",
"symfony/error-handler": "^4.4",
"symfony/event-dispatcher": "^4.3",
"symfony/http-foundation": "^4.4|^5.0",
"symfony/debug": "^3.4|^4.0|^5.0",
"symfony/debug": "^4.4|^5.0",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-php73": "^1.9",
"psr/log": "~1.0"

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\Messenger\EventListener;
use Psr\Log\LoggerInterface;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;

Some files were not shown because too many files have changed in this diff Show More