[ErrorHandler] merge and remove the ErrorRenderer component

This commit is contained in:
Nicolas Grekas 2019-11-10 14:46:37 +01:00
parent 10a349c37d
commit 6c9157bbc2
99 changed files with 536 additions and 1640 deletions

View File

@ -18,7 +18,6 @@ Console
Debug Debug
----- -----
* Deprecated the `Debug` class, use the one from the `ErrorRenderer` component instead
* Deprecated the `FlattenException` class, use the one from the `ErrorRenderer` component instead * Deprecated the `FlattenException` class, use the one from the `ErrorRenderer` component instead
* Deprecated the component in favor of the `ErrorHandler` component * Deprecated the component in favor of the `ErrorHandler` component
@ -309,45 +308,35 @@ TwigBundle
* Deprecated all built-in error templates, use the error renderer mechanism of the `ErrorRenderer` component * Deprecated all built-in error templates, use the error renderer mechanism of the `ErrorRenderer` component
* Deprecated loading custom error templates in non-html formats. Custom HTML error pages based on Twig keep working as before: * Deprecated loading custom error templates in non-html formats. Custom HTML error pages based on Twig keep working as before:
Before (`templates/bundles/TwigBundle/Exception/error.jsonld.twig`): Before (`templates/bundles/TwigBundle/Exception/error.json.twig`):
```twig ```twig
{ {
"@id": "https://example.com", "type": "https://example.com/error",
"@type": "error", "title": "{{ status_text }}",
"@context": { "status": {{ status_code }}
"title": "{{ status_text }}",
"code": {{ status_code }},
"message": "{{ exception.message }}"
}
} }
``` ```
After (`App\ErrorRenderer\JsonLdErrorRenderer`): After (`App\Serializer\ProblemJsonNormalizer`):
```php ```php
class JsonLdErrorRenderer implements ErrorRendererInterface class ProblemJsonNormalizer implements NormalizerInterface
{ {
public static function getFormat(): string public function normalize($exception, $format = null, array $context = [])
{ {
return 'jsonld'; return [
'type' => 'https://example.com/error',
'title' => $exception->getStatusText(),
'status' => $exception->getStatusCode(),
];
} }
public function render(FlattenException $exception): string public function supportsNormalization($data, $format = null)
{ {
return json_encode([ return 'json' === $format && $data instanceof FlattenException;
'@id' => 'https://example.com',
'@type' => 'error',
'@context' => [
'title' => $exception->getTitle(),
'code' => $exception->getStatusCode(),
'message' => $exception->getMessage(),
],
]);
} }
} }
``` ```
Configure your rendering service tagging it with `error_renderer.renderer`.
Validator Validator
--------- ---------

View File

@ -57,7 +57,6 @@ Console
Debug Debug
----- -----
* Removed the `Debug` class, use the one from the `ErrorRenderer` component instead
* Removed the `FlattenException` class, use the one from the `ErrorRenderer` component instead * Removed the `FlattenException` class, use the one from the `ErrorRenderer` component instead
* Removed the component in favor of the `ErrorHandler` component * Removed the component in favor of the `ErrorHandler` component

View File

@ -48,7 +48,6 @@
"symfony/dom-crawler": "self.version", "symfony/dom-crawler": "self.version",
"symfony/dotenv": "self.version", "symfony/dotenv": "self.version",
"symfony/error-handler": "self.version", "symfony/error-handler": "self.version",
"symfony/error-renderer": "self.version",
"symfony/event-dispatcher": "self.version", "symfony/event-dispatcher": "self.version",
"symfony/expression-language": "self.version", "symfony/expression-language": "self.version",
"symfony/filesystem": "self.version", "symfony/filesystem": "self.version",

View File

@ -11,7 +11,7 @@
namespace Symfony\Bridge\Twig\Mime; namespace Symfony\Bridge\Twig\Mime;
use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Mime\Header\Headers; use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Part\AbstractPart; use Symfony\Component\Mime\Part\AbstractPart;
use Twig\Extra\CssInliner\CssInlinerExtension; use Twig\Extra\CssInliner\CssInlinerExtension;

View File

@ -32,7 +32,6 @@ class UnusedTagsPass implements CompilerPassInterface
'controller.service_arguments', 'controller.service_arguments',
'config_cache.resource_checker', 'config_cache.resource_checker',
'data_collector', 'data_collector',
'error_renderer.renderer',
'form.type', 'form.type',
'form.type_extension', 'form.type_extension',
'form.type_guesser', 'form.type_guesser',

View File

@ -33,7 +33,6 @@ use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass; use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\ErrorHandler\ErrorHandler; use Symfony\Component\ErrorHandler\ErrorHandler;
use Symfony\Component\ErrorRenderer\DependencyInjection\ErrorRendererPass;
use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
use Symfony\Component\Form\DependencyInjection\FormPass; use Symfony\Component\Form\DependencyInjection\FormPass;
use Symfony\Component\HttpClient\DependencyInjection\HttpClientPass; use Symfony\Component\HttpClient\DependencyInjection\HttpClientPass;
@ -92,7 +91,6 @@ class FrameworkBundle extends Bundle
KernelEvents::FINISH_REQUEST, KernelEvents::FINISH_REQUEST,
]; ];
$container->addCompilerPass(new ErrorRendererPass());
$container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
$container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING); $container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING);

View File

@ -194,12 +194,6 @@
<tag name="console.command" command="debug:form" /> <tag name="console.command" command="debug:form" />
</service> </service>
<service id="console.command.error_renderer_debug" class="Symfony\Component\ErrorRenderer\Command\DebugCommand">
<argument type="collection" /> <!-- All error renderers are injected here by ErrorRendererPass -->
<argument type="service" id="debug.file_link_formatter" on-invalid="null" />
<tag name="console.command" command="debug:error-renderer" />
</service>
<service id="console.command.secrets_set" class="Symfony\Bundle\FrameworkBundle\Command\SecretsSetCommand"> <service id="console.command.secrets_set" class="Symfony\Bundle\FrameworkBundle\Command\SecretsSetCommand">
<argument type="service" id="secrets.vault" /> <argument type="service" id="secrets.vault" />
<argument type="service" id="secrets.local_vault" on-invalid="ignore" /> <argument type="service" id="secrets.local_vault" on-invalid="ignore" />

View File

@ -5,12 +5,7 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd">
<services> <services>
<service id="error_renderer" class="Symfony\Component\ErrorRenderer\DependencyInjection\LazyLoadingErrorRenderer"> <service id="error_handler.error_renderer.html" class="Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer">
<argument /> <!-- error renderer locator -->
</service>
<service id="error_renderer.renderer.html" class="Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer">
<tag name="error_renderer.renderer" />
<argument>%kernel.debug%</argument> <argument>%kernel.debug%</argument>
<argument>%kernel.charset%</argument> <argument>%kernel.charset%</argument>
<argument type="service" id="debug.file_link_formatter" on-invalid="null" /> <argument type="service" id="debug.file_link_formatter" on-invalid="null" />
@ -19,21 +14,15 @@
<argument type="service" id="logger" on-invalid="null" /> <argument type="service" id="logger" on-invalid="null" />
</service> </service>
<service id="error_renderer.renderer.json" class="Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer"> <service id="error_handler.error_renderer.serializer" class="Symfony\Component\ErrorHandler\ErrorRenderer\SerializerErrorRenderer">
<tag name="error_renderer.renderer" /> <argument type="service" id="serializer" />
<argument type="service" id="request_stack" />
<argument type="service" id="error_renderer.html" />
<argument>%kernel.debug%</argument> <argument>%kernel.debug%</argument>
</service> </service>
<service id="error_renderer.renderer.xml" class="Symfony\Component\ErrorRenderer\ErrorRenderer\XmlErrorRenderer"> <service id="error_renderer.html" alias="error_handler.error_renderer.html" />
<tag name="error_renderer.renderer" format="atom" /> <service id="error_renderer.serializer" alias="error_handler.error_renderer.serializer" />
<tag name="error_renderer.renderer" /> <service id="error_renderer" alias="error_renderer.html" />
<argument>%kernel.debug%</argument>
<argument>%kernel.charset%</argument>
</service>
<service id="error_renderer.renderer.txt" class="Symfony\Component\ErrorRenderer\ErrorRenderer\TxtErrorRenderer">
<tag name="error_renderer.renderer" />
<argument>%kernel.debug%</argument>
</service>
</services> </services>
</container> </container>

View File

@ -12,6 +12,8 @@
<services> <services>
<defaults public="false" /> <defaults public="false" />
<service id="error_renderer" alias="error_renderer.serializer" />
<service id="serializer" class="Symfony\Component\Serializer\Serializer" public="true"> <service id="serializer" class="Symfony\Component\Serializer\Serializer" public="true">
<argument type="collection" /> <argument type="collection" />
<argument type="collection" /> <argument type="collection" />
@ -59,6 +61,12 @@
<tag name="serializer.normalizer" priority="-900" /> <tag name="serializer.normalizer" priority="-900" />
</service> </service>
<service id="serializer.normalizer.problem" class="Symfony\Component\Serializer\Normalizer\ProblemNormalizer">
<argument>%kernel.debug%</argument>
<!-- Run before serializer.normalizer.object -->
<tag name="serializer.normalizer" priority="-890" />
</service>
<service id="serializer.normalizer.object" class="Symfony\Component\Serializer\Normalizer\ObjectNormalizer"> <service id="serializer.normalizer.object" class="Symfony\Component\Serializer\Normalizer\ObjectNormalizer">
<argument type="service" id="serializer.mapping.class_metadata_factory" /> <argument type="service" id="serializer.mapping.class_metadata_factory" />
<argument type="service" id="serializer.name_converter.metadata_aware" /> <argument type="service" id="serializer.name_converter.metadata_aware" />

View File

@ -21,7 +21,6 @@
"symfony/cache": "^4.4|^5.0", "symfony/cache": "^4.4|^5.0",
"symfony/config": "^4.3.4|^5.0", "symfony/config": "^4.3.4|^5.0",
"symfony/dependency-injection": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0",
"symfony/error-renderer": "^4.4|^5.0",
"symfony/http-foundation": "^4.4|^5.0", "symfony/http-foundation": "^4.4|^5.0",
"symfony/http-kernel": "^4.4", "symfony/http-kernel": "^4.4",
"symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-mbstring": "~1.0",
@ -50,7 +49,7 @@
"symfony/process": "^3.4|^4.0|^5.0", "symfony/process": "^3.4|^4.0|^5.0",
"symfony/security-csrf": "^3.4|^4.0|^5.0", "symfony/security-csrf": "^3.4|^4.0|^5.0",
"symfony/security-http": "^3.4|^4.0|^5.0", "symfony/security-http": "^3.4|^4.0|^5.0",
"symfony/serializer": "^4.3|^5.0", "symfony/serializer": "^4.4|^5.0",
"symfony/stopwatch": "^3.4|^4.0|^5.0", "symfony/stopwatch": "^3.4|^4.0|^5.0",
"symfony/translation": "^4.4|^5.0", "symfony/translation": "^4.4|^5.0",
"symfony/templating": "^3.4|^4.0|^5.0", "symfony/templating": "^3.4|^4.0|^5.0",

View File

@ -70,6 +70,6 @@ class JsonLoginTest extends AbstractWebTestCase
$this->assertSame(400, $response->getStatusCode()); $this->assertSame(400, $response->getStatusCode());
$this->assertSame('application/json', $response->headers->get('Content-Type')); $this->assertSame('application/json', $response->headers->get('Content-Type'));
$this->assertSame(['title' => 'Bad Request', 'status' => 400, 'detail' => 'Whoops, looks like something went wrong.'], json_decode($response->getContent(), true)); $this->assertSame(['type' => 'https://tools.ietf.org/html/rfc2616#section-10', 'title' => 'An error occurred', 'status' => 400, 'detail' => 'Bad Request'], json_decode($response->getContent(), true));
} }
} }

View File

@ -1,6 +1,9 @@
imports: imports:
- { resource: ./../config/framework.yml } - { resource: ./../config/framework.yml }
framework:
serializer: ~
security: security:
encoders: encoders:
Symfony\Component\Security\Core\User\User: plaintext Symfony\Component\Security\Core\User\User: plaintext

View File

@ -35,6 +35,7 @@
"symfony/form": "^3.4|^4.0|^5.0", "symfony/form": "^3.4|^4.0|^5.0",
"symfony/framework-bundle": "^4.4|^5.0", "symfony/framework-bundle": "^4.4|^5.0",
"symfony/http-foundation": "^3.4|^4.0|^5.0", "symfony/http-foundation": "^3.4|^4.0|^5.0",
"symfony/serializer": "^4.4|^5.0",
"symfony/translation": "^3.4|^4.0|^5.0", "symfony/translation": "^3.4|^4.0|^5.0",
"symfony/twig-bundle": "^4.4|^5.0", "symfony/twig-bundle": "^4.4|^5.0",
"symfony/twig-bridge": "^3.4|^4.0|^5.0", "symfony/twig-bridge": "^3.4|^4.0|^5.0",

View File

@ -6,8 +6,8 @@ CHANGELOG
* marked the `TemplateIterator` as `internal` * marked the `TemplateIterator` as `internal`
* added HTML comment to beginning and end of `exception_full.html.twig` * added HTML comment to beginning and end of `exception_full.html.twig`
* added a new `TwigHtmlErrorRenderer` for `html` format, integrated with the `ErrorRenderer` component * added a new `TwigHtmlErrorRenderer` for `html` format, integrated with the `ErrorHandler` component
* deprecated `ExceptionController` and `PreviewErrorController` controllers, use `ErrorController` from the `HttpKernel` component instead * deprecated `ExceptionController` and `PreviewErrorController` controllers, use `ErrorController` from the `HttpKernel` component instead
* deprecated all built-in error templates in favor of the new error renderer mechanism * deprecated all built-in error templates in favor of the new error renderer mechanism
* deprecated `twig.exception_controller` configuration option, set it to "null" and use `framework.error_controller` configuration instead * deprecated `twig.exception_controller` configuration option, set it to "null" and use `framework.error_controller` configuration instead

View File

@ -11,9 +11,9 @@
namespace Symfony\Bundle\TwigBundle\ErrorRenderer; namespace Symfony\Bundle\TwigBundle\ErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface; use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Twig\Environment; use Twig\Environment;
use Twig\Error\LoaderError; use Twig\Error\LoaderError;
use Twig\Loader\ExistsLoaderInterface; use Twig\Loader\ExistsLoaderInterface;
@ -40,34 +40,20 @@ class TwigHtmlErrorRenderer implements ErrorRendererInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function getFormat(): string public function render(\Throwable $exception): FlattenException
{ {
return 'html'; $exception = $this->htmlErrorRenderer->render($exception);
}
/** if ($this->debug || !$template = $this->findTemplate($exception->getStatusCode());
* {@inheritdoc} return $exception;
*/
public function render(FlattenException $exception): string
{
$debug = $this->debug && ($exception->getHeaders()['X-Debug'] ?? true);
if ($debug) {
return $this->htmlErrorRenderer->render($exception);
} }
$template = $this->findTemplate($exception->getStatusCode()); return $exception->setAsString($this->twig->render($template, [
if (null === $template) {
return $this->htmlErrorRenderer->render($exception);
}
return $this->twig->render($template, [
'legacy' => false, // to be removed in 5.0 'legacy' => false, // to be removed in 5.0
'exception' => $exception, 'exception' => $exception,
'status_code' => $exception->getStatusCode(), 'status_code' => $exception->getStatusCode(),
'status_text' => $exception->getTitle(), 'status_text' => $exception->getStatusText(),
]); ]));
} }
private function findTemplate(int $statusCode): ?string private function findTemplate(int $statusCode): ?string

View File

@ -162,10 +162,9 @@
<argument /> <!-- runtime locator --> <argument /> <!-- runtime locator -->
</service> </service>
<service id="twig.error_renderer.html" class="Symfony\Bundle\TwigBundle\ErrorRenderer\TwigHtmlErrorRenderer"> <service id="twig.error_renderer.html" class="Symfony\Bundle\TwigBundle\ErrorRenderer\TwigHtmlErrorRenderer" decorates="error_renderer.html">
<tag name="error_renderer.renderer" priority="1" />
<argument type="service" id="twig" /> <argument type="service" id="twig" />
<argument type="service" id="error_renderer.renderer.html" /> <argument type="service" id="twig.error_renderer.html.inner" />
<argument>%kernel.debug%</argument> <argument>%kernel.debug%</argument>
</service> </service>
</services> </services>

View File

@ -13,8 +13,7 @@ namespace Symfony\Bundle\TwigBundle\Tests\ErrorRenderer;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Bundle\TwigBundle\ErrorRenderer\TwigHtmlErrorRenderer; use Symfony\Bundle\TwigBundle\ErrorRenderer\TwigHtmlErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Twig\Environment; use Twig\Environment;
use Twig\Loader\ArrayLoader; use Twig\Loader\ArrayLoader;
@ -23,7 +22,7 @@ class TwigHtmlErrorRendererTest extends TestCase
{ {
public function testFallbackToNativeRendererIfDebugOn() public function testFallbackToNativeRendererIfDebugOn()
{ {
$exception = FlattenException::createFromThrowable(new \Exception()); $exception = new \Exception();
$twig = $this->createMock(Environment::class); $twig = $this->createMock(Environment::class);
$nativeRenderer = $this->createMock(HtmlErrorRenderer::class); $nativeRenderer = $this->createMock(HtmlErrorRenderer::class);
@ -33,12 +32,12 @@ class TwigHtmlErrorRendererTest extends TestCase
->with($exception) ->with($exception)
; ;
(new TwigHtmlErrorRenderer($twig, $nativeRenderer, true))->render($exception); (new TwigHtmlErrorRenderer($twig, $nativeRenderer, true))->render(new \Exception());
} }
public function testFallbackToNativeRendererIfCustomTemplateNotFound() public function testFallbackToNativeRendererIfCustomTemplateNotFound()
{ {
$exception = FlattenException::createFromThrowable(new NotFoundHttpException()); $exception = new NotFoundHttpException();
$twig = new Environment(new ArrayLoader([])); $twig = new Environment(new ArrayLoader([]));
@ -54,7 +53,7 @@ class TwigHtmlErrorRendererTest extends TestCase
public function testRenderCustomErrorTemplate() public function testRenderCustomErrorTemplate()
{ {
$exception = FlattenException::createFromThrowable(new NotFoundHttpException()); $exception = new NotFoundHttpException();
$twig = new Environment(new ArrayLoader([ $twig = new Environment(new ArrayLoader([
'@Twig/Exception/error404.html.twig' => '<h1>Page Not Found</h1>', '@Twig/Exception/error404.html.twig' => '<h1>Page Not Found</h1>',
@ -68,6 +67,6 @@ class TwigHtmlErrorRendererTest extends TestCase
$content = (new TwigHtmlErrorRenderer($twig, $nativeRenderer, false))->render($exception); $content = (new TwigHtmlErrorRenderer($twig, $nativeRenderer, false))->render($exception);
$this->assertSame('<h1>Page Not Found</h1>', $content); $this->assertSame('<h1>Page Not Found</h1>', $content->getAsString());
} }
} }

View File

@ -15,7 +15,7 @@ use Symfony\Bundle\TwigBundle\Tests\TestCase;
use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Bundle\TwigBundle\TwigBundle;
use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\ErrorRenderer\ErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\Kernel;
@ -65,7 +65,8 @@ class EmptyAppKernel extends Kernel
'strict_variables' => false, 'strict_variables' => false,
'exception_controller' => null, 'exception_controller' => null,
]); ]);
$container->register('error_renderer', ErrorRenderer::class); $container->register('error_renderer.html', HtmlErrorRenderer::class);
$container->setAlias('error_renderer', 'error_renderer.html');
$container->setParameter('debug.file_link_format', null); $container->setParameter('debug.file_link_format', null);
}); });
} }

View File

@ -17,7 +17,6 @@
], ],
"require": { "require": {
"php": "^7.1.3", "php": "^7.1.3",
"symfony/error-renderer": "^4.4|^5.0",
"symfony/twig-bridge": "^4.4|^5.0", "symfony/twig-bridge": "^4.4|^5.0",
"symfony/http-foundation": "^4.3|^5.0", "symfony/http-foundation": "^4.3|^5.0",
"symfony/http-kernel": "^4.4", "symfony/http-kernel": "^4.4",

View File

@ -11,7 +11,7 @@
namespace Symfony\Bundle\WebProfilerBundle\Controller; namespace Symfony\Bundle\WebProfilerBundle\Controller;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -42,11 +42,7 @@ class ExceptionController
$this->profiler = $profiler; $this->profiler = $profiler;
$this->twig = $twig; $this->twig = $twig;
$this->debug = $debug; $this->debug = $debug;
$this->errorRenderer = $errorRenderer; $this->errorRenderer = $errorRenderer ?? new HtmlErrorRenderer($debug, $this->twig->getCharset(), $fileLinkFormat);
if (null === $errorRenderer) {
$this->errorRenderer = new HtmlErrorRenderer($debug, $this->twig->getCharset(), $fileLinkFormat);
}
} }
/** /**

View File

@ -11,7 +11,7 @@
namespace Symfony\Bundle\WebProfilerBundle\Controller; namespace Symfony\Bundle\WebProfilerBundle\Controller;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Profiler\Profiler; use Symfony\Component\HttpKernel\Profiler\Profiler;
@ -25,12 +25,12 @@ use Symfony\Component\HttpKernel\Profiler\Profiler;
*/ */
class ExceptionPanelController class ExceptionPanelController
{ {
private $htmlErrorRenderer; private $errorRenderer;
private $profiler; private $profiler;
public function __construct(HtmlErrorRenderer $htmlErrorRenderer, ?Profiler $profiler) public function __construct(HtmlErrorRenderer $errorRenderer, Profiler $profiler = null)
{ {
$this->htmlErrorRenderer = $htmlErrorRenderer; $this->errorRenderer = $errorRenderer;
$this->profiler = $profiler; $this->profiler = $profiler;
} }
@ -48,7 +48,7 @@ class ExceptionPanelController
->getException() ->getException()
; ;
return new Response($this->htmlErrorRenderer->getBody($exception), 200, ['Content-Type' => 'text/html']); return new Response($this->errorRenderer->getBody($exception), 200, ['Content-Type' => 'text/html']);
} }
/** /**
@ -56,6 +56,6 @@ class ExceptionPanelController
*/ */
public function stylesheet(): Response public function stylesheet(): Response
{ {
return new Response($this->htmlErrorRenderer->getStylesheet(), 200, ['Content-Type' => 'text/css']); return new Response($this->errorRenderer->getStylesheet(), 200, ['Content-Type' => 'text/css']);
} }
} }

View File

@ -27,12 +27,12 @@
<argument type="service" id="twig" /> <argument type="service" id="twig" />
<argument>%kernel.debug%</argument> <argument>%kernel.debug%</argument>
<argument type="service" id="debug.file_link_formatter" /> <argument type="service" id="debug.file_link_formatter" />
<argument type="service" id="error_renderer.renderer.html" /> <argument type="service" id="error_handler.error_renderer.html" />
<deprecated>The "%service_id%" service is deprecated since Symfony 4.4, use the "web_profiler.controller.exception_panel" service instead.</deprecated> <deprecated>The "%service_id%" service is deprecated since Symfony 4.4, use the "web_profiler.controller.exception_panel" service instead.</deprecated>
</service> </service>
<service id="web_profiler.controller.exception_panel" class="Symfony\Bundle\WebProfilerBundle\Controller\ExceptionPanelController" public="true"> <service id="web_profiler.controller.exception_panel" class="Symfony\Bundle\WebProfilerBundle\Controller\ExceptionPanelController" public="true">
<argument type="service" id="error_renderer.renderer.html" /> <argument type="service" id="error_handler.error_renderer.html" />
<argument type="service" id="profiler" on-invalid="null" /> <argument type="service" id="profiler" on-invalid="null" />
</service> </service>

View File

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

View File

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

View File

@ -4,7 +4,7 @@ CHANGELOG
4.4.0 4.4.0
----- -----
* deprecated `FlattenException`, use the `FlattenException` of the `ErrorRenderer` component * deprecated `FlattenException`, use the `FlattenException` of the `ErrorHandler` component
* deprecated the whole component in favor of the `ErrorHandler` component * deprecated the whole component in favor of the `ErrorHandler` component
4.3.0 4.3.0

View File

@ -21,7 +21,7 @@ use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
* *
* @author Fabien Potencier <fabien@symfony.com> * @author Fabien Potencier <fabien@symfony.com>
* *
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorRenderer\Exception\FlattenException instead. * @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Exception\FlattenException instead.
*/ */
class FlattenException class FlattenException
{ {

View File

@ -34,4 +34,25 @@ class BufferingLogger extends AbstractLogger
return $logs; return $logs;
} }
public function __destruct()
{
foreach ($this->logs as [$level, $message, $context]) {
if (false !== strpos($message, '{')) {
foreach ($context as $key => $val) {
if (null === $val || is_scalar($val) || (\is_object($val) && \is_callable([$val, '__toString']))) {
$message = str_replace("{{$key}}", $val, $message);
} elseif ($val instanceof \DateTimeInterface) {
$message = str_replace("{{$key}}", $val->format(\DateTime::RFC3339), $message);
} elseif (\is_object($val)) {
$message = str_replace("{{$key}}", '[object '.\get_class($val).']', $message);
} else {
$message = str_replace("{{$key}}", '['.\gettype($val).']', $message);
}
}
}
error_log(sprintf('%s [%s] %s', date(\DateTime::RFC3339), $level, $message));
}
}
} }

View File

@ -18,39 +18,19 @@ namespace Symfony\Component\ErrorHandler;
*/ */
class Debug class Debug
{ {
private static $enabled = false; public static function enable(): ErrorHandler
/**
* Enables the debug tools.
*
* This method registers an error handler and an exception handler.
*/
public static function enable(int $errorReportingLevel = E_ALL, bool $displayErrors = true): void
{ {
if (static::$enabled) { error_reporting(-1);
return;
}
static::$enabled = true;
if (null !== $errorReportingLevel) {
error_reporting($errorReportingLevel);
} else {
error_reporting(E_ALL);
}
if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
ini_set('display_errors', 0); ini_set('display_errors', 0);
} elseif ($displayErrors && (!filter_var(ini_get('log_errors'), FILTER_VALIDATE_BOOLEAN) || ini_get('error_log'))) { } elseif (!filter_var(ini_get('log_errors'), FILTER_VALIDATE_BOOLEAN) || ini_get('error_log')) {
// CLI - display errors only if they're not already logged to STDERR // CLI - display errors only if they're not already logged to STDERR
ini_set('display_errors', 1); ini_set('display_errors', 1);
} }
if ($displayErrors) {
ErrorHandler::register(new ErrorHandler(new BufferingLogger()));
} else {
ErrorHandler::register()->throwAt(0, true);
}
DebugClassLoader::enable(); DebugClassLoader::enable();
return ErrorHandler::register(new ErrorHandler(new BufferingLogger()));
} }
} }

View File

@ -19,9 +19,10 @@ use Symfony\Component\ErrorHandler\ErrorEnhancer\ClassNotFoundErrorEnhancer;
use Symfony\Component\ErrorHandler\ErrorEnhancer\ErrorEnhancerInterface; use Symfony\Component\ErrorHandler\ErrorEnhancer\ErrorEnhancerInterface;
use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedFunctionErrorEnhancer; use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedFunctionErrorEnhancer;
use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedMethodErrorEnhancer; use Symfony\Component\ErrorHandler\ErrorEnhancer\UndefinedMethodErrorEnhancer;
use Symfony\Component\ErrorHandler\ErrorRenderer\CliErrorRenderer;
use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
/** /**
* A generic ErrorHandler for the PHP engine. * A generic ErrorHandler for the PHP engine.
@ -45,10 +46,8 @@ use Symfony\Component\ErrorRenderer\Exception\FlattenException;
* *
* @author Nicolas Grekas <p@tchwork.com> * @author Nicolas Grekas <p@tchwork.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info> * @author Grégoire Pineau <lyrixx@lyrixx.info>
*
* @final since Symfony 4.3
*/ */
class ErrorHandler final class ErrorHandler
{ {
private $levels = [ private $levels = [
E_DEPRECATED => 'Deprecated', E_DEPRECATED => 'Deprecated',
@ -145,10 +144,8 @@ class ErrorHandler
$handler->setExceptionHandler($p); $handler->setExceptionHandler($p);
$prev[0]->setExceptionHandler($p); $prev[0]->setExceptionHandler($p);
} }
} elseif (null === $prev && !\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) {
$handler->setExceptionHandler([$handler, 'sendPhpResponse']);
} else { } else {
$handler->setExceptionHandler($prev); $handler->setExceptionHandler($prev ?? [$handler, 'renderException']);
} }
$handler->throwAt(E_ALL & $handler->thrownErrors, true); $handler->throwAt(E_ALL & $handler->thrownErrors, true);
@ -280,7 +277,7 @@ class ErrorHandler
/** /**
* Sets a user exception handler. * Sets a user exception handler.
* *
* @param callable|null $handler A handler that must support \Throwable instances that will be called on Exception * @param callable(\Throwable $e)|null $handler
* *
* @return callable|null The previous exception handler * @return callable|null The previous exception handler
*/ */
@ -583,11 +580,7 @@ class ErrorHandler
} }
$exceptionHandler = $this->exceptionHandler; $exceptionHandler = $this->exceptionHandler;
if ((!\is_array($exceptionHandler) || !$exceptionHandler[0] instanceof self || 'sendPhpResponse' !== $exceptionHandler[1]) && !\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true)) { $this->exceptionHandler = null;
$this->exceptionHandler = [$this, 'sendPhpResponse'];
} else {
$this->exceptionHandler = null;
}
try { try {
if (null !== $exceptionHandler) { if (null !== $exceptionHandler) {
return $exceptionHandler($exception); return $exceptionHandler($exception);
@ -684,36 +677,26 @@ class ErrorHandler
} }
/** /**
* Sends the error associated with the given Exception as a plain PHP response. * Renders the given exception.
* *
* As this method is mainly called during Kernel boot, where nothing is yet * As this method is mainly called during boot where nothing is yet available,
* available, the Response content is always HTML. * the output is always either HTML or CLI depending where PHP runs.
*/ */
private function sendPhpResponse(\Throwable $exception) private function renderException(\Throwable $exception): void
{ {
$charset = ini_get('default_charset') ?: 'UTF-8'; $renderer = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? new CliErrorRenderer() : new HtmlErrorRenderer(0 !== $this->scopedErrors);
$statusCode = 500;
$headers = [];
if (class_exists(HtmlErrorRenderer::class)) { $exception = $renderer->render($exception);
$exception = FlattenException::createFromThrowable($exception);
$statusCode = $exception->getStatusCode();
$headers = $exception->getHeaders();
$response = (new HtmlErrorRenderer(0 !== $this->scopedErrors))->render($exception);
} else {
$message = htmlspecialchars($exception->getMessage(), ENT_COMPAT | ENT_SUBSTITUTE, $charset);
$response = sprintf('<!DOCTYPE html><html><head><meta charset="%s" /><meta name="robots" content="noindex,nofollow" /></head><body>%s</body></html>', $charset, $message);
}
if (!headers_sent()) { if (!headers_sent()) {
header(sprintf('HTTP/1.0 %s', $statusCode)); http_response_code($exception->getStatusCode());
foreach ($headers as $name => $value) {
foreach ($exception->getHeaders() as $name => $value) {
header($name.': '.$value, false); header($name.': '.$value, false);
} }
header('Content-Type: text/html; charset='.$charset);
} }
echo $response; echo $exception->getAsString();
} }
/** /**

View File

@ -0,0 +1,46 @@
<?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;
use Symfony\Component\VarDumper\Cloner\VarCloner;
use Symfony\Component\VarDumper\Dumper\CliDumper;
/**
* @author Nicolas Grekas <p@tchwork.com>
*/
class CliErrorRenderer implements ErrorRendererInterface
{
/**
* {@inheritdoc}
*/
public function render(\Throwable $exception): FlattenException
{
$cloner = new VarCloner();
$dumper = new class() extends CliDumper {
protected function supportsColors(): bool
{
$outputStream = $this->outputStream;
$this->outputStream = STDOUT;
try {
return parent::supportsColors();
} finally {
$this->outputStream = $outputStream;
}
}
};
return FlattenException::createFromThrowable($exception)
->setAsString($dumper->dump($cloner->cloneVar($exception), true));
}
}

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\ErrorRenderer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
/**
* Formats an exception to be used as response content.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
interface ErrorRendererInterface
{
/**
* Renders a Throwable as a FlattenException.
*/
public function render(\Throwable $exception): FlattenException;
}

View File

@ -9,10 +9,10 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace Symfony\Component\ErrorRenderer\ErrorRenderer; namespace Symfony\Component\ErrorHandler\ErrorRenderer;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
@ -52,23 +52,17 @@ class HtmlErrorRenderer implements ErrorRendererInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public static function getFormat(): string public function render(\Throwable $exception): FlattenException
{ {
return 'html'; $exception = FlattenException::createFromThrowable($exception, null, [
} 'Content-Type' => 'text/html; charset='.$this->charset,
]);
/**
* {@inheritdoc} return $exception->setAsString($this->renderException($exception));
*/
public function render(FlattenException $exception): string
{
return $this->renderException($exception);
} }
/** /**
* Gets the HTML content associated with the given exception. * Gets the HTML content associated with the given exception.
*
* @internal
*/ */
public function getBody(FlattenException $exception): string public function getBody(FlattenException $exception): string
{ {
@ -77,8 +71,6 @@ class HtmlErrorRenderer implements ErrorRendererInterface
/** /**
* Gets the stylesheet associated with the given exception. * Gets the stylesheet associated with the given exception.
*
* @internal
*/ */
public function getStylesheet(): string public function getStylesheet(): string
{ {
@ -91,11 +83,10 @@ class HtmlErrorRenderer implements ErrorRendererInterface
private function renderException(FlattenException $exception, string $debugTemplate = 'views/exception_full.html.php'): string private function renderException(FlattenException $exception, string $debugTemplate = 'views/exception_full.html.php'): string
{ {
$debug = $this->debug && ($exception->getHeaders()['X-Debug'] ?? true); $statusText = $this->escape($exception->getStatusText());
$statusText = $this->escape($exception->getTitle());
$statusCode = $this->escape($exception->getStatusCode()); $statusCode = $this->escape($exception->getStatusCode());
if (!$debug) { if (!$this->debug) {
return $this->include('views/error.html.php', [ return $this->include('views/error.html.php', [
'statusText' => $statusText, 'statusText' => $statusText,
'statusCode' => $statusCode, 'statusCode' => $statusCode,
@ -111,13 +102,13 @@ class HtmlErrorRenderer implements ErrorRendererInterface
'statusText' => $statusText, 'statusText' => $statusText,
'statusCode' => $statusCode, 'statusCode' => $statusCode,
'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null,
'currentContent' => $request ? $this->getAndCleanOutputBuffering($request->headers->get('X-Php-Ob-Level')) : null, 'currentContent' => $request ? $this->getAndCleanOutputBuffering($request->headers->get('X-Php-Ob-Level')) : '',
]); ]);
} }
private function getAndCleanOutputBuffering(int $startObLevel): string private function getAndCleanOutputBuffering(?int $startObLevel): string
{ {
if (ob_get_level() <= $startObLevel) { if (null === $startObLevel || ob_get_level() <= $startObLevel) {
return ''; return '';
} }
@ -321,7 +312,7 @@ class HtmlErrorRenderer implements ErrorRendererInterface
{ {
extract($context, EXTR_SKIP); extract($context, EXTR_SKIP);
ob_start(); ob_start();
include __DIR__.'/../Resources/'.$name; include __DIR__ . '/../Resources/' .$name;
return trim(ob_get_clean()); return trim(ob_get_clean());
} }

View File

@ -0,0 +1,50 @@
<?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\ErrorRenderer\Exception\FlattenException;
use Symfony\Component\Serializer\Exception\NotEncodableValueException;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Formats an exception using Serializer for rendering.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class SerializerErrorRenderer
{
private $serializer;
private $requestStack;
private $debug;
public function __construct(SerializerInterface $serializer, RequestStack $requestStack, bool $debug = true)
{
$this->serializer = $serializer;
$this->requestStack = $requestStack;
$this->debug = $debug;
}
/**
* {@inheritdoc}
*/
public function render(\Throwable $exception): FlattenException
{
$format = $this->requestStack->getCurrentRequest()->getPreferredFormat();
$flattenException = FlattenException::createFromThrowable($exception);
try {
return $flattenException->setAsString($this->serializer->serialize($flattenException, $format, ['exception' => $exception]));
} catch (NotEncodableValueException $_) {
return (new HtmlErrorHandler($this->debug))->render($exception);
}
}
}

View File

@ -9,7 +9,7 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace Symfony\Component\ErrorRenderer\Exception; namespace Symfony\Component\ErrorHandler\Exception;
use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException; use Symfony\Component\Debug\Exception\FlattenException as LegacyFlattenException;
use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface; use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface;
@ -25,7 +25,6 @@ use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
*/ */
class FlattenException extends LegacyFlattenException class FlattenException extends LegacyFlattenException
{ {
private $title;
private $message; private $message;
private $code; private $code;
private $previous; private $previous;
@ -33,9 +32,11 @@ class FlattenException extends LegacyFlattenException
private $traceAsString; private $traceAsString;
private $class; private $class;
private $statusCode; private $statusCode;
private $statusText;
private $headers; private $headers;
private $file; private $file;
private $line; private $line;
private $asString;
public static function create(\Exception $exception, $statusCode = null, array $headers = []): self public static function create(\Exception $exception, $statusCode = null, array $headers = []): self
{ {
@ -60,12 +61,12 @@ class FlattenException extends LegacyFlattenException
} }
if (class_exists(Response::class) && isset(Response::$statusTexts[$statusCode])) { if (class_exists(Response::class) && isset(Response::$statusTexts[$statusCode])) {
$title = Response::$statusTexts[$statusCode]; $statusText = Response::$statusTexts[$statusCode];
} else { } else {
$title = 'Whoops, looks like something went wrong.'; $statusText = 'Whoops, looks like something went wrong.';
} }
$e->setTitle($title); $e->setStatusText($statusText);
$e->setStatusCode($statusCode); $e->setStatusCode($statusCode);
$e->setHeaders($headers); $e->setHeaders($headers);
$e->setTraceFromThrowable($exception); $e->setTraceFromThrowable($exception);
@ -171,14 +172,14 @@ class FlattenException extends LegacyFlattenException
return $this; return $this;
} }
public function getTitle() public function getStatusText()
{ {
return $this->title; return $this->statusText;
} }
public function setTitle(string $title): self public function setStatusText(string $statusText): self
{ {
$this->title = $title; $this->statusText = $statusText;
return $this; return $this;
} }
@ -355,8 +356,19 @@ class FlattenException extends LegacyFlattenException
return $this->traceAsString; return $this->traceAsString;
} }
public function setAsString(?string $asString)
{
$this->asString = $asString;
return $this;
}
public function getAsString() public function getAsString()
{ {
if (null !== $this->asString) {
return $this->asString;
}
$message = ''; $message = '';
$next = false; $next = false;

View File

@ -1,25 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Debug;
if (!class_exists(Debug::class, false)) {
class_alias(\Symfony\Component\ErrorHandler\Debug::class, Debug::class);
}
if (false) {
/**
* @deprecated since Symfony 4.4, use Symfony\Component\ErrorHandler\Debug instead.
*/
class Debug extends \Symfony\Component\ErrorHandler\Debug
{
}
}

View File

@ -0,0 +1,43 @@
<!-- <?= $_message = sprintf('%s (%d %s)', $exceptionMessage, $statusCode, $statusText); ?> -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="<?= $this->charset; ?>" />
<meta name="robots" content="noindex,nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title><?= $_message; ?></title>
<link rel="icon" type="image/png" href="<?= $this->include('assets/images/favicon.png.base64'); ?>">
<style><?= $this->include('assets/css/exception.css'); ?></style>
<style><?= $this->include('assets/css/exception_full.css'); ?></style>
</head>
<body>
<?php if (class_exists('Symfony\Component\HttpKernel\Kernel')) { ?>
<header>
<div class="container">
<h1 class="logo"><?= $this->include('assets/images/symfony-logo.svg'); ?> Symfony Exception</h1>
<div class="help-link">
<a href="https://symfony.com/doc/<?= Symfony\Component\HttpKernel\Kernel::VERSION; ?>/index.html">
<span class="icon"><?= $this->include('assets/images/icon-book.svg'); ?></span>
<span class="hidden-xs-down">Symfony</span> Docs
</a>
</div>
<div class="help-link">
<a href="https://symfony.com/support">
<span class="icon"><?= $this->include('assets/images/icon-support.svg'); ?></span>
<span class="hidden-xs-down">Symfony</span> Support
</a>
</div>
</div>
</header>
<?php } ?>
<?= $this->include('views/exception.html.php', $context); ?>
<script>
<?= $this->include('assets/js/exception.js'); ?>
</script>
</body>
</html>
<!-- <?= $_message; ?> -->

View File

@ -0,0 +1,53 @@
<?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\HtmlErrorRendererInterface;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ProblemNormalizer;
use Symfony\Component\Serializer\Serializer;
class ErrorRendererTest extends TestCase
{
public function testDefaultContent()
{
$errorRenderer = new ErrorRenderer();
self::assertStringContainsString('<h2>The server returned a "500 Internal Server Error".</h2>', $errorRenderer->render(new \RuntimeException(), 'html'));
}
public function testCustomContent()
{
$errorRenderer = new ErrorRenderer(new CustomHtmlErrorRenderer());
$this->assertSame('Foo', $errorRenderer->render(new \RuntimeException('Foo'), 'html'));
}
public function testSerializerContent()
{
$exception = new \RuntimeException('Foo');
$errorRenderer = new ErrorRenderer(null, new Serializer([new ProblemNormalizer()], [new JsonEncoder()]));
$this->assertSame('{"type":"https:\/\/tools.ietf.org\/html\/rfc2616#section-10","title":"An error occurred","status":500,"detail":"Internal Server Error"}', $errorRenderer->render($exception, 'json'));
}
}
class CustomHtmlErrorRenderer implements HtmlErrorRendererInterface
{
public function render(FlattenException $exception): string
{
return $exception->getMessage();
}
}

View File

@ -9,21 +9,20 @@
* file that was distributed with this source code. * file that was distributed with this source code.
*/ */
namespace Symfony\Component\ErrorRenderer\Tests\ErrorRenderer; namespace Symfony\Component\ErrorHandler\Tests\ErrorRenderer;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
class HtmlErrorRendererTest extends TestCase class HtmlErrorRendererTest extends TestCase
{ {
/** /**
* @dataProvider getRenderData * @dataProvider getRenderData
*/ */
public function testRender(FlattenException $exception, ErrorRendererInterface $errorRenderer, string $expected) public function testRender(FlattenException $exception, HtmlErrorRenderer $errorRenderer, string $expected)
{ {
$this->assertStringMatchesFormat($expected, $errorRenderer->render($exception)); $this->assertStringMatchesFormat($expected, $errorRenderer->render($exception)->getAsString());
} }
public function getRenderData(): iterable public function getRenderData(): iterable
@ -55,17 +54,5 @@ HTML;
new HtmlErrorRenderer(false), new HtmlErrorRenderer(false),
$expectedNonDebug, $expectedNonDebug,
]; ];
yield '->render() returns the HTML content WITHOUT stack traces in debug mode FORCING non-debug via X-Debug header' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo'), null, ['X-Debug' => false]),
new HtmlErrorRenderer(true),
$expectedNonDebug,
];
yield '->render() returns the HTML content WITHOUT stack traces in non-debug mode EVEN FORCING debug via X-Debug header' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo'), null, ['X-Debug' => true]),
new HtmlErrorRenderer(false),
$expectedNonDebug,
];
} }
} }

View File

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

View File

@ -17,20 +17,18 @@
], ],
"require": { "require": {
"php": "^7.1.3", "php": "^7.1.3",
"psr/log": "~1.0" "psr/log": "~1.0",
"symfony/debug": "^4.4",
"symfony/var-dumper": "^4.4|^5.0"
}, },
"require-dev": { "require-dev": {
"symfony/http-kernel": "^3.4|^4.0|^5.0" "symfony/http-kernel": "^4.4|^5.0"
},
"suggest": {
"symfony/error-renderer": "For better error rendering"
}, },
"conflict": { "conflict": {
"symfony/http-kernel": "<3.4" "symfony/http-kernel": "<4.4"
}, },
"autoload": { "autoload": {
"psr-4": { "Symfony\\Component\\ErrorHandler\\": "" }, "psr-4": { "Symfony\\Component\\ErrorHandler\\": "" },
"classmap": [ "Resources/stubs/Debug.php" ],
"exclude-from-classmap": [ "exclude-from-classmap": [
"/Tests/" "/Tests/"
] ]

View File

@ -1,3 +0,0 @@
/Tests export-ignore
/phpunit.xml.dist export-ignore
/.gitignore export-ignore

View File

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

View File

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

View File

@ -1,125 +0,0 @@
<?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\ErrorRenderer\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
use Symfony\Component\HttpKernel\Debug\FileLinkFormatter;
/**
* A console command for retrieving information about error renderers.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*
* @internal
*/
class DebugCommand extends Command
{
protected static $defaultName = 'debug:error-renderer';
private $renderers;
private $fileLinkFormatter;
/**
* @param ErrorRendererInterface[] $renderers
*/
public function __construct(array $renderers, FileLinkFormatter $fileLinkFormatter = null)
{
$this->renderers = $renderers;
$this->fileLinkFormatter = $fileLinkFormatter;
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->addArgument('format', InputArgument::OPTIONAL, sprintf('Outputs a sample in a specific format (one of %s)', implode(', ', array_keys($this->renderers))))
->setDescription('Displays all available error renderers and their formats.')
->setHelp(<<<'EOF'
The <info>%command.name%</info> command displays all available error renderers and
their formats:
<info>php %command.full_name%</info>
Or output a sample in a specific format:
<info>php %command.full_name% json</info>
EOF
)
;
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$renderers = $this->renderers;
if ($format = $input->getArgument('format')) {
if (!isset($renderers[$format])) {
throw new InvalidArgumentException(sprintf('No error renderer found for format "%s". Known format are %s.', $format, implode(', ', array_keys($this->renderers))));
}
$exception = FlattenException::createFromThrowable(new \Exception('This is a sample exception.'), 500, ['X-Debug' => false]);
$io->writeln($renderers[$format]->render($exception));
} else {
$tableRows = [];
foreach ($renderers as $format => $renderer) {
$tableRows[] = [sprintf('<fg=cyan>%s</fg=cyan>', $format), $this->formatClassLink(\get_class($renderer))];
}
$io->title('Error Renderers');
$io->text('The following error renderers are available:');
$io->newLine();
$io->table(['Format', 'Class'], $tableRows);
}
return 0;
}
private function formatClassLink(string $class): string
{
if ('' === $fileLink = $this->getFileLink($class)) {
return $class;
}
return sprintf('<href=%s>%s</>', $fileLink, $class);
}
private function getFileLink(string $class): string
{
if (null === $this->fileLinkFormatter) {
return '';
}
try {
$r = new \ReflectionClass($class);
} catch (\ReflectionException $e) {
return '';
}
return $this->fileLinkFormatter->format($r->getFileName(), $r->getStartLine());
}
}

View File

@ -1,71 +0,0 @@
<?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\ErrorRenderer\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\ErrorRenderer\ErrorRenderer\ErrorRendererInterface;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class ErrorRendererPass implements CompilerPassInterface
{
private $rendererService;
private $rendererTag;
private $debugCommandService;
public function __construct(string $rendererService = 'error_renderer', string $rendererTag = 'error_renderer.renderer', string $debugCommandService = 'console.command.error_renderer_debug')
{
$this->rendererService = $rendererService;
$this->rendererTag = $rendererTag;
$this->debugCommandService = $debugCommandService;
}
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition($this->rendererService)) {
return;
}
$renderers = [];
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();
$priority = $tag['priority'] ?? 0;
if (!isset($renderers[$priority][$format])) {
$renderers[$priority][$format] = new Reference($serviceId);
}
}
}
if ($renderers) {
ksort($renderers);
$renderers = array_merge(...$renderers);
}
$definition = $container->getDefinition($this->rendererService);
$definition->replaceArgument(0, ServiceLocatorTagPass::register($container, $renderers));
if ($container->hasDefinition($this->debugCommandService)) {
$container->getDefinition($this->debugCommandService)->replaceArgument(0, $renderers);
}
}
}

View File

@ -1,44 +0,0 @@
<?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\ErrorRenderer\DependencyInjection;
use Psr\Container\ContainerInterface;
use Symfony\Component\ErrorRenderer\ErrorRenderer;
/**
* Lazily loads error renderers from the dependency injection container.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class LazyLoadingErrorRenderer extends ErrorRenderer
{
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

@ -1,77 +0,0 @@
<?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\ErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
/**
* Formats an exception to be used as response content.
*
* It delegates to implementations of ErrorRendererInterface depending on the format.
*
* @see ErrorRendererInterface
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class ErrorRenderer
{
private $renderers = [];
/**
* @param ErrorRendererInterface[] $renderers
*/
public function __construct(iterable $renderers)
{
foreach ($renderers as $renderer) {
$this->addRenderer($renderer);
}
}
/**
* Registers an error renderer that is format specific.
*
* By passing an explicit format you can register a renderer for a different format than what
* ErrorRendererInterface::getFormat() would return in order to register the same renderer for
* several format aliases.
*/
public function addRenderer(ErrorRendererInterface $renderer, string $format = null): self
{
$this->renderers[$format ?? $renderer::getFormat()] = $renderer;
return $this;
}
/**
* Renders an Exception and returns the Response content.
*
* @param \Throwable|FlattenException $exception A \Throwable 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 \Throwable) {
$exception = FlattenException::createFromThrowable($exception);
}
return $this->renderers[$format]->render($exception);
}
}

View File

@ -1,32 +0,0 @@
<?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\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
/**
* Interface for classes that can render errors in a specific format.
*
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
interface ErrorRendererInterface
{
/**
* Gets the format this renderer can return errors as.
*/
public static function getFormat(): string;
/**
* Returns the response content of the rendered exception.
*/
public function render(FlattenException $exception): string;
}

View File

@ -1,60 +0,0 @@
<?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\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class JsonErrorRenderer implements ErrorRendererInterface
{
private $debug;
public function __construct(bool $debug = false)
{
$this->debug = $debug;
}
/**
* {@inheritdoc}
*/
public static function getFormat(): string
{
return 'json';
}
/**
* {@inheritdoc}
*/
public function render(FlattenException $exception): string
{
$debug = $this->debug && ($exception->getHeaders()['X-Debug'] ?? true);
if ($debug) {
$message = $exception->getMessage();
} else {
$message = 404 === $exception->getStatusCode() ? 'Sorry, the page you are looking for could not be found.' : 'Whoops, looks like something went wrong.';
}
$content = [
'title' => $exception->getTitle(),
'status' => $exception->getStatusCode(),
'detail' => $message,
];
if ($debug) {
$content['exceptions'] = $exception->toArray();
}
return (string) json_encode($content, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_LINE_TERMINATORS | JSON_PRESERVE_ZERO_FRACTION);
}
}

View File

@ -1,104 +0,0 @@
<?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\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class TxtErrorRenderer implements ErrorRendererInterface
{
private $debug;
public function __construct(bool $debug = false)
{
$this->debug = $debug;
}
/**
* {@inheritdoc}
*/
public static function getFormat(): string
{
return 'txt';
}
/**
* {@inheritdoc}
*/
public function render(FlattenException $exception): string
{
$debug = $this->debug && ($exception->getHeaders()['X-Debug'] ?? true);
if ($debug) {
$message = $exception->getMessage();
} else {
$message = 404 === $exception->getStatusCode() ? 'Sorry, the page you are looking for could not be found.' : 'Whoops, looks like something went wrong.';
}
$content = sprintf("[title] %s\n", $exception->getTitle());
$content .= sprintf("[status] %s\n", $exception->getStatusCode());
$content .= sprintf("[detail] %s\n", $message);
if ($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

@ -1,125 +0,0 @@
<?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\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
/**
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class XmlErrorRenderer implements ErrorRendererInterface
{
private $debug;
private $charset;
public function __construct(bool $debug = false, 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
{
$debug = $this->debug && ($exception->getHeaders()['X-Debug'] ?? true);
$title = $this->escapeXml($exception->getTitle());
if ($debug) {
$message = $this->escapeXml($exception->getMessage());
} else {
$message = 404 === $exception->getStatusCode() ? 'Sorry, the page you are looking for could not be found.' : 'Whoops, looks like something went wrong.';
}
$statusCode = $this->escapeXml($exception->getStatusCode());
$charset = $this->escapeXml($this->charset);
$exceptions = '';
if ($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="{$charset}" ?>
<problem xmlns="urn:ietf:rfc:7807">
<title>{$title}</title>
<status>{$statusCode}</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

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

View File

@ -1,19 +0,0 @@
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

@ -1,12 +0,0 @@
ErrorRenderer Component
======================
The ErrorRenderer component provides tools to 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

@ -1,41 +0,0 @@
<!-- <?= $_message = sprintf('%s (%d %s)', $exceptionMessage, $statusCode, $statusText); ?> -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="<?= $this->charset; ?>" />
<meta name="robots" content="noindex,nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title><?= $_message; ?></title>
<link rel="icon" type="image/png" href="<?= $this->include('assets/images/favicon.png.base64'); ?>">
<style><?= $this->include('assets/css/exception.css'); ?></style>
<style><?= $this->include('assets/css/exception_full.css'); ?></style>
</head>
<body>
<header>
<div class="container">
<h1 class="logo"><?= $this->include('assets/images/symfony-logo.svg'); ?> Symfony Exception</h1>
<div class="help-link">
<a href="https://symfony.com/doc/<?= Symfony\Component\HttpKernel\Kernel::VERSION; ?>/index.html">
<span class="icon"><?= $this->include('assets/images/icon-book.svg'); ?></span>
<span class="hidden-xs-down">Symfony</span> Docs
</a>
</div>
<div class="help-link">
<a href="https://symfony.com/support">
<span class="icon"><?= $this->include('assets/images/icon-support.svg'); ?></span>
<span class="hidden-xs-down">Symfony</span> Support
</a>
</div>
</div>
</header>
<?= $this->include('views/exception.html.php', $context); ?>
<script>
<?= $this->include('assets/js/exception.js'); ?>
</script>
</body>
</html>
<!-- <?= $_message; ?> -->

View File

@ -1,88 +0,0 @@
<?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\ErrorRenderer\Tests\Command;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\ErrorRenderer\Command\DebugCommand;
use Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\TxtErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\XmlErrorRenderer;
class DebugCommandTest extends TestCase
{
public function testAvailableRenderers()
{
$tester = $this->createCommandTester();
$ret = $tester->execute([], ['decorated' => false]);
$this->assertEquals(0, $ret, 'Returns 0 in case of success');
$this->assertSame(<<<TXT
Error Renderers
===============
The following error renderers are available:
-------- -----------------------------------------------------------------
Format Class
-------- -----------------------------------------------------------------
json Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer
xml Symfony\Component\ErrorRenderer\ErrorRenderer\XmlErrorRenderer
txt Symfony\Component\ErrorRenderer\ErrorRenderer\TxtErrorRenderer
-------- -----------------------------------------------------------------
TXT
, $tester->getDisplay(true));
}
public function testFormatArgument()
{
$tester = $this->createCommandTester();
$ret = $tester->execute(['format' => 'json'], ['decorated' => false]);
$this->assertEquals(0, $ret, 'Returns 0 in case of success');
$this->assertSame(<<<TXT
{
"title": "Internal Server Error",
"status": 500,
"detail": "Whoops, looks like something went wrong."
}
TXT
, $tester->getDisplay(true));
}
private function createCommandTester()
{
$command = new DebugCommand([
'json' => new JsonErrorRenderer(false),
'xml' => new XmlErrorRenderer(false),
'txt' => new TxtErrorRenderer(false),
]);
$application = new Application();
$application->add($command);
return new CommandTester($application->find('debug:error-renderer'));
}
public function testInvalidFormat()
{
$this->expectException('Symfony\Component\Console\Exception\InvalidArgumentException');
$this->expectExceptionMessage('No error renderer found for format "foo". Known format are json, xml, txt.');
$tester = $this->createCommandTester();
$tester->execute(['format' => 'foo'], ['decorated' => false]);
}
}

View File

@ -1,67 +0,0 @@
<?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\ErrorRenderer\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\ErrorRenderer\DependencyInjection\ErrorRendererPass;
use Symfony\Component\ErrorRenderer\DependencyInjection\LazyLoadingErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer;
class ErrorRendererPassTest extends TestCase
{
public function testProcess()
{
$container = new ContainerBuilder();
$container->setParameter('kernel.debug', true);
$definition = $container->register('error_renderer', LazyLoadingErrorRenderer::class)
->addArgument([])
;
$container->register('error_renderer.renderer.html', HtmlErrorRenderer::class)
->addTag('error_renderer.renderer')
;
$container->register('error_renderer.renderer.json', JsonErrorRenderer::class)
->addTag('error_renderer.renderer')
;
(new ErrorRendererPass())->process($container);
$serviceLocatorDefinition = $container->getDefinition((string) $definition->getArgument(0));
$this->assertSame(ServiceLocator::class, $serviceLocatorDefinition->getClass());
$expected = [
'html' => new ServiceClosureArgument(new Reference('error_renderer.renderer.html')),
'json' => new ServiceClosureArgument(new Reference('error_renderer.renderer.json')),
];
$this->assertEquals($expected, $serviceLocatorDefinition->getArgument(0));
}
public function testServicesAreOrderedAccordingToPriority()
{
$container = new ContainerBuilder();
$definition = $container->register('error_renderer')->setArguments([null]);
$container->register('r2')->addTag('error_renderer.renderer', ['format' => 'json', 'priority' => 100]);
$container->register('r1')->addTag('error_renderer.renderer', ['format' => 'json', 'priority' => 200]);
$container->register('r3')->addTag('error_renderer.renderer', ['format' => 'json']);
(new ErrorRendererPass())->process($container);
$expected = [
'json' => new ServiceClosureArgument(new Reference('r1')),
];
$serviceLocatorDefinition = $container->getDefinition((string) $definition->getArgument(0));
$this->assertEquals($expected, $serviceLocatorDefinition->getArgument(0));
}
}

View File

@ -1,66 +0,0 @@
<?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\ErrorRenderer\Tests\DependencyInjection;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\ErrorRenderer\DependencyInjection\LazyLoadingErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
class LazyLoadingErrorRendererTest extends TestCase
{
public function testInvalidErrorRenderer()
{
$this->expectException('Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException');
$this->expectExceptionMessage('No error renderer found for format "foo".');
$container = $this->getMockBuilder(ContainerInterface::class)->getMock();
$container->expects($this->once())->method('has')->with('foo')->willReturn(false);
$exception = FlattenException::createFromThrowable(new \Exception('Foo'));
(new LazyLoadingErrorRenderer($container))->render($exception, 'foo');
}
public function testCustomErrorRenderer()
{
$container = $this->getMockBuilder(ContainerInterface::class)->getMock();
$container
->expects($this->once())
->method('has')
->with('foo')
->willReturn(true)
;
$container
->expects($this->once())
->method('get')
->willReturn(new FooErrorRenderer())
;
$errorRenderer = new LazyLoadingErrorRenderer($container);
$exception = FlattenException::createFromThrowable(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

@ -1,76 +0,0 @@
<?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\ErrorRenderer\Tests\ErrorRenderer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
class JsonErrorRendererTest extends TestCase
{
/**
* @dataProvider getRenderData
*/
public function testRender(FlattenException $exception, ErrorRendererInterface $errorRenderer, string $expected)
{
$this->assertStringMatchesFormat($expected, $errorRenderer->render($exception));
}
public function getRenderData(): iterable
{
$expectedDebug = <<<JSON
{
"title": "Internal Server Error",
"status": 500,
"detail": "Foo",
"exceptions": [
{
"message": "Foo",
"class": "RuntimeException",
"trace": [
%A
JSON;
$expectedNonDebug = <<<JSON
{
"title": "Internal Server Error",
"status": 500,
"detail": "Whoops, looks like something went wrong."
}
JSON;
yield '->render() returns the JSON content WITH stack traces in debug mode' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo')),
new JsonErrorRenderer(true),
$expectedDebug,
];
yield '->render() returns the JSON content WITHOUT stack traces in non-debug mode' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo')),
new JsonErrorRenderer(false),
$expectedNonDebug,
];
yield '->render() returns the JSON content WITHOUT stack traces in debug mode FORCING non-debug via X-Debug header' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo'), null, ['X-Debug' => false]),
new JsonErrorRenderer(true),
$expectedNonDebug,
];
yield '->render() returns the JSON content WITHOUT stack traces in non-debug mode EVEN FORCING debug via X-Debug header' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo'), null, ['X-Debug' => true]),
new JsonErrorRenderer(false),
$expectedNonDebug,
];
}
}

View File

@ -1,69 +0,0 @@
<?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\ErrorRenderer\Tests\ErrorRenderer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorRenderer\ErrorRenderer\TxtErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
class TxtErrorRendererTest extends TestCase
{
/**
* @dataProvider getRenderData
*/
public function testRender(FlattenException $exception, ErrorRendererInterface $errorRenderer, string $expected)
{
$this->assertStringMatchesFormat($expected, $errorRenderer->render($exception));
}
public function getRenderData(): iterable
{
$expectedDebug = <<<TXT
[title] Internal Server Error
[status] 500
[detail] Foo
[1] RuntimeException: Foo
in %A
TXT;
$expectedNonDebug = <<<TXT
[title] Internal Server Error
[status] 500
[detail] Whoops, looks like something went wrong.
TXT;
yield '->render() returns the TXT content WITH stack traces in debug mode' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo')),
new TxtErrorRenderer(true),
$expectedDebug,
];
yield '->render() returns the TXT content WITHOUT stack traces in non-debug mode' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo')),
new TxtErrorRenderer(false),
$expectedNonDebug,
];
yield '->render() returns the TXT content WITHOUT stack traces in debug mode FORCING non-debug via X-Debug header' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo'), null, ['X-Debug' => false]),
new TxtErrorRenderer(true),
$expectedNonDebug,
];
yield '->render() returns the TXT content WITHOUT stack traces in non-debug mode EVEN FORCING debug via X-Debug header' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo'), null, ['X-Debug' => true]),
new TxtErrorRenderer(false),
$expectedNonDebug,
];
}
}

View File

@ -1,75 +0,0 @@
<?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\ErrorRenderer\Tests\ErrorRenderer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorRenderer\ErrorRenderer\XmlErrorRenderer;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
class XmlErrorRendererTest extends TestCase
{
/**
* @dataProvider getRenderData
*/
public function testRender(FlattenException $exception, ErrorRendererInterface $errorRenderer, string $expected)
{
$this->assertStringMatchesFormat($expected, $errorRenderer->render($exception));
}
public function getRenderData(): iterable
{
$expectedDebug = <<<XML
<?xml version="1.0" encoding="UTF-8" ?>
<problem xmlns="urn:ietf:rfc:7807">
<title>Internal Server Error</title>
<status>500</status>
<detail>Foo</detail>
<exceptions><exception class="RuntimeException" message="Foo"><traces><trace>%A</trace></traces></exception></exceptions>
</problem>
XML;
$expectedNonDebug = <<<XML
<?xml version="1.0" encoding="UTF-8" ?>
<problem xmlns="urn:ietf:rfc:7807">
<title>Internal Server Error</title>
<status>500</status>
<detail>Whoops, looks like something went wrong.</detail>
</problem>
XML;
yield '->render() returns the XML content WITH stack traces in debug mode' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo')),
new XmlErrorRenderer(true),
$expectedDebug,
];
yield '->render() returns the XML content WITHOUT stack traces in non-debug mode' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo')),
new XmlErrorRenderer(false),
$expectedNonDebug,
];
yield '->render() returns the XML content WITHOUT stack traces in debug mode FORCING non-debug via X-Debug header' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo'), null, ['X-Debug' => false]),
new XmlErrorRenderer(true),
$expectedNonDebug,
];
yield '->render() returns the XML content WITHOUT stack traces in non-debug mode EVEN FORCING debug via X-Debug header' => [
FlattenException::createFromThrowable(new \RuntimeException('Foo'), null, ['X-Debug' => true]),
new XmlErrorRenderer(false),
$expectedNonDebug,
];
}
}

View File

@ -1,56 +0,0 @@
<?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\ErrorRenderer\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
class ErrorRendererTest extends TestCase
{
public function testErrorRendererNotFound()
{
$this->expectException('Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException');
$this->expectExceptionMessage('No error renderer found for format "foo".');
$exception = FlattenException::createFromThrowable(new \Exception('foo'));
(new ErrorRenderer([]))->render($exception, 'foo');
}
public function testInvalidErrorRenderer()
{
$this->expectException('TypeError');
new ErrorRenderer([new \stdClass()]);
}
public function testCustomErrorRenderer()
{
$renderers = [new FooErrorRenderer()];
$errorRenderer = new ErrorRenderer($renderers);
$exception = FlattenException::createFromThrowable(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

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

View File

@ -1,30 +0,0 @@
<?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 ErrorRenderer 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,9 +11,8 @@
namespace Symfony\Component\HttpKernel\Controller; namespace Symfony\Component\HttpKernel\Controller;
use Symfony\Component\ErrorRenderer\ErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface;
use Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException; use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\HttpKernelInterface;
@ -30,26 +29,22 @@ class ErrorController
private $controller; private $controller;
private $errorRenderer; private $errorRenderer;
public function __construct(HttpKernelInterface $kernel, $controller, ErrorRenderer $errorRenderer) public function __construct(HttpKernelInterface $kernel, $controller, ErrorRendererInterface $errorRenderer)
{ {
$this->kernel = $kernel; $this->kernel = $kernel;
$this->controller = $controller; $this->controller = $controller;
$this->errorRenderer = $errorRenderer; $this->errorRenderer = $errorRenderer;
} }
public function __invoke(Request $request, FlattenException $exception): Response public function __invoke(\Throwable $exception): Response
{ {
try { $exception = $this->errorRenderer->render($exception);
return new Response($this->errorRenderer->render($exception, $request->getPreferredFormat()), $exception->getStatusCode(), $exception->getHeaders());
} catch (ErrorRendererNotFoundException $_) { return new Response($exception->getAsString(), $exception->getStatusCode(), $exception->getHeaders());
return new Response($this->errorRenderer->render($exception), $exception->getStatusCode(), $exception->getHeaders());
}
} }
public function preview(Request $request, int $code): Response public function preview(Request $request, int $code): Response
{ {
$exception = FlattenException::createFromThrowable(new \Exception('This is a sample exception.'), $code, ['X-Debug' => false]);
/* /*
* This Request mimics the parameters set by * This Request mimics the parameters set by
* \Symfony\Component\HttpKernel\EventListener\ErrorListener::duplicateRequest, with * \Symfony\Component\HttpKernel\EventListener\ErrorListener::duplicateRequest, with
@ -57,7 +52,7 @@ class ErrorController
*/ */
$subRequest = $request->duplicate(null, null, [ $subRequest = $request->duplicate(null, null, [
'_controller' => $this->controller, '_controller' => $this->controller,
'exception' => $exception, 'exception' => new \Exception('This is a sample exception.'),
'logger' => null, 'logger' => null,
'showException' => false, 'showException' => false,
]); ]);

View File

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

View File

@ -123,7 +123,7 @@ class ErrorListener implements EventSubscriberInterface
{ {
$attributes = [ $attributes = [
'_controller' => $this->controller, '_controller' => $this->controller,
'exception' => FlattenException::createFromThrowable($exception), 'exception' => $exception,
'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null, 'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null,
]; ];
$request = $request->duplicate(null, null, $attributes); $request = $request->duplicate(null, null, $attributes);

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\HttpKernel\EventListener; namespace Symfony\Component\HttpKernel\EventListener;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;

View File

@ -12,10 +12,9 @@
namespace Symfony\Component\HttpKernel\Tests\Controller; namespace Symfony\Component\HttpKernel\Tests\Controller;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorRenderer\ErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer;
use Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer; use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\ErrorRenderer\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ErrorController; use Symfony\Component\HttpKernel\Controller\ErrorController;
@ -31,7 +30,7 @@ class ErrorControllerTest extends TestCase
public function testInvokeController(Request $request, FlattenException $exception, int $statusCode, string $content) public function testInvokeController(Request $request, FlattenException $exception, int $statusCode, string $content)
{ {
$kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock();
$errorRenderer = new ErrorRenderer([new HtmlErrorRenderer(), new JsonErrorRenderer()]); $errorRenderer = new ErrorRenderer(new HtmlErrorRenderer());
$controller = new ErrorController($kernel, null, $errorRenderer); $controller = new ErrorController($kernel, null, $errorRenderer);
$response = $controller($request, $exception); $response = $controller($request, $exception);
@ -55,33 +54,6 @@ class ErrorControllerTest extends TestCase
'The server returned a "404 Not Found".', 'The server returned a "404 Not Found".',
]; ];
$request = new Request();
$request->attributes->set('_format', 'json');
yield 'custom format via _format attribute' => [
$request,
FlattenException::createFromThrowable(new \Exception('foo')),
500,
'{"title": "Internal Server Error","status": 500,"detail": "Whoops, looks like something went wrong."}',
];
$request = new Request();
$request->headers->set('Accept', 'application/json');
yield 'custom format via Accept header' => [
$request,
FlattenException::createFromThrowable(new HttpException(405, 'Invalid request.')),
405,
'{"title": "Method Not Allowed","status": 405,"detail": "Whoops, looks like something went wrong."}',
];
$request = new Request();
$request->headers->set('Content-Type', 'application/json');
yield 'custom format via Content-Type header' => [
$request,
FlattenException::createFromThrowable(new HttpException(405, 'Invalid request.')),
405,
'{"title": "Method Not Allowed","status": 405,"detail": "Whoops, looks like something went wrong."}',
];
$request = new Request(); $request = new Request();
$request->attributes->set('_format', 'unknown'); $request->attributes->set('_format', 'unknown');
yield 'default HTML format for unknown formats' => [ yield 'default HTML format for unknown formats' => [
@ -116,7 +88,7 @@ class ErrorControllerTest extends TestCase
) )
->willReturn($response = new Response()); ->willReturn($response = new Response());
$controller = new ErrorController($kernel, $_controller, new ErrorRenderer([])); $controller = new ErrorController($kernel, $_controller, new ErrorRenderer());
$this->assertSame($response, $controller->preview(new Request(), $code)); $this->assertSame($response, $controller->preview(new Request(), $code));
} }

View File

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

View File

@ -18,7 +18,6 @@
"require": { "require": {
"php": "^7.1.3", "php": "^7.1.3",
"symfony/error-handler": "^4.4|^5.0", "symfony/error-handler": "^4.4|^5.0",
"symfony/error-renderer": "^4.4|^5.0",
"symfony/event-dispatcher": "^4.4", "symfony/event-dispatcher": "^4.4",
"symfony/http-foundation": "^4.4|^5.0", "symfony/http-foundation": "^4.4|^5.0",
"symfony/polyfill-ctype": "^1.8", "symfony/polyfill-ctype": "^1.8",
@ -40,7 +39,6 @@
"symfony/templating": "^3.4|^4.0|^5.0", "symfony/templating": "^3.4|^4.0|^5.0",
"symfony/translation": "^4.2|^5.0", "symfony/translation": "^4.2|^5.0",
"symfony/translation-contracts": "^1.1|^2", "symfony/translation-contracts": "^1.1|^2",
"symfony/var-dumper": "^4.1.1|^5.0",
"psr/cache": "~1.0", "psr/cache": "~1.0",
"twig/twig": "^1.34|^2.4|^3.0" "twig/twig": "^1.34|^2.4|^3.0"
}, },
@ -53,15 +51,13 @@
"symfony/console": ">=5", "symfony/console": ">=5",
"symfony/dependency-injection": "<4.3", "symfony/dependency-injection": "<4.3",
"symfony/translation": "<4.2", "symfony/translation": "<4.2",
"symfony/var-dumper": "<4.1.1",
"twig/twig": "<1.34|<2.4,>=2" "twig/twig": "<1.34|<2.4,>=2"
}, },
"suggest": { "suggest": {
"symfony/browser-kit": "", "symfony/browser-kit": "",
"symfony/config": "", "symfony/config": "",
"symfony/console": "", "symfony/console": "",
"symfony/dependency-injection": "", "symfony/dependency-injection": ""
"symfony/var-dumper": ""
}, },
"autoload": { "autoload": {
"psr-4": { "Symfony\\Component\\HttpKernel\\": "" }, "psr-4": { "Symfony\\Component\\HttpKernel\\": "" },

View File

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

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\Messenger\Stamp; namespace Symfony\Component\Messenger\Stamp;
use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Envelope;
/** /**

View File

@ -12,7 +12,7 @@
namespace Symfony\Component\Messenger\Tests\Stamp; namespace Symfony\Component\Messenger\Tests\Stamp;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp; use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
class RedeliveryStampTest extends TestCase class RedeliveryStampTest extends TestCase

View File

@ -25,7 +25,6 @@
"doctrine/persistence": "~1.0", "doctrine/persistence": "~1.0",
"symfony/console": "^3.4|^4.0|^5.0", "symfony/console": "^3.4|^4.0|^5.0",
"symfony/dependency-injection": "^3.4.19|^4.1.8|^5.0", "symfony/dependency-injection": "^3.4.19|^4.1.8|^5.0",
"symfony/error-renderer": "^4.4|^5.0",
"symfony/event-dispatcher": "^4.3|^5.0", "symfony/event-dispatcher": "^4.3|^5.0",
"symfony/http-kernel": "^4.4", "symfony/http-kernel": "^4.4",
"symfony/process": "^3.4|^4.0|^5.0", "symfony/process": "^3.4|^4.0|^5.0",

View File

@ -6,6 +6,7 @@ CHANGELOG
* deprecated the `XmlEncoder::TYPE_CASE_ATTRIBUTES` constant, use `XmlEncoder::TYPE_CAST_ATTRIBUTES` instead * deprecated the `XmlEncoder::TYPE_CASE_ATTRIBUTES` constant, use `XmlEncoder::TYPE_CAST_ATTRIBUTES` instead
* added option to output a UTF-8 BOM in CSV encoder via `CsvEncoder::OUTPUT_UTF8_BOM_KEY` context option * added option to output a UTF-8 BOM in CSV encoder via `CsvEncoder::OUTPUT_UTF8_BOM_KEY` context option
* added `ProblemNormalizer` to normalize errors according to the API Problem spec (RFC 7807)
4.3.0 4.3.0
----- -----

View File

@ -0,0 +1,74 @@
<?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\Serializer\Normalizer;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
/**
* Normalizes errors according to the API Problem spec (RFC 7807).
*
* @see https://tools.ietf.org/html/rfc7807
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
class ProblemNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface
{
private $debug;
private $defaultContext = [
'type' => 'https://tools.ietf.org/html/rfc2616#section-10',
'title' => 'An error occurred',
];
public function __construct(bool $debug = false, array $defaultContext = [])
{
$this->debug = $debug;
$this->defaultContext = $defaultContext + $this->defaultContext;
}
/**
* {@inheritdoc}
*/
public function normalize($exception, $format = null, array $context = [])
{
$context += $this->defaultContext;
$data = [
'type' => $context['type'],
'title' => $context['title'],
'status' => $context['status'] ?? $exception->getStatusCode(),
'detail' => $this->debug ? $exception->getMessage() : $exception->getStatusText(),
];
if ($this->debug) {
$data['class'] = $exception->getClass();
$data['trace'] = $exception->getTrace();
}
return $data;
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null): bool
{
return $data instanceof FlattenException;
}
/**
* {@inheritdoc}
*/
public function hasCacheableSupportsMethod(): bool
{
return true;
}
}

View File

@ -0,0 +1,48 @@
<?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\Serializer\Tests\Normalizer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\ErrorHandler\Exception\FlattenException;
use Symfony\Component\Serializer\Normalizer\ProblemNormalizer;
class ProblemNormalizerTest extends TestCase
{
/**
* @var ProblemNormalizer
*/
private $normalizer;
protected function setUp(): void
{
$this->normalizer = new ProblemNormalizer(false);
}
public function testSupportNormalization()
{
$this->assertTrue($this->normalizer->supportsNormalization(FlattenException::createFromThrowable(new \Exception())));
$this->assertFalse($this->normalizer->supportsNormalization(new \Exception()));
$this->assertFalse($this->normalizer->supportsNormalization(new \stdClass()));
}
public function testNormalize()
{
$expected = [
'type' => 'https://tools.ietf.org/html/rfc2616#section-10',
'title' => 'An error occurred',
'status' => 500,
'detail' => 'Internal Server Error',
];
$this->assertSame($expected, $this->normalizer->normalize(FlattenException::createFromThrowable(new \RuntimeException('Error'))));
}
}

View File

@ -20,17 +20,18 @@
"symfony/polyfill-ctype": "~1.8" "symfony/polyfill-ctype": "~1.8"
}, },
"require-dev": { "require-dev": {
"symfony/yaml": "^3.4|^4.0|^5.0", "doctrine/annotations": "~1.0",
"symfony/config": "^3.4|^4.0|^5.0", "doctrine/cache": "~1.0",
"symfony/property-access": "^3.4|^4.0|^5.0", "phpdocumentor/reflection-docblock": "^3.0|^4.0",
"symfony/http-foundation": "^3.4|^4.0|^5.0",
"symfony/cache": "^3.4|^4.0|^5.0", "symfony/cache": "^3.4|^4.0|^5.0",
"symfony/config": "^3.4|^4.0|^5.0",
"symfony/dependency-injection": "^3.4|^4.0|^5.0",
"symfony/error-handler": "^4.4|^5.0",
"symfony/http-foundation": "^3.4|^4.0|^5.0",
"symfony/property-access": "^3.4|^4.0|^5.0",
"symfony/property-info": "^3.4.13|~4.0|^5.0", "symfony/property-info": "^3.4.13|~4.0|^5.0",
"symfony/validator": "^3.4|^4.0|^5.0", "symfony/validator": "^3.4|^4.0|^5.0",
"doctrine/annotations": "~1.0", "symfony/yaml": "^3.4|^4.0|^5.0"
"symfony/dependency-injection": "^3.4|^4.0|^5.0",
"doctrine/cache": "~1.0",
"phpdocumentor/reflection-docblock": "^3.0|^4.0"
}, },
"conflict": { "conflict": {
"phpdocumentor/type-resolver": "<0.2.1", "phpdocumentor/type-resolver": "<0.2.1",