diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index 2253f5fd80c47..a0f876e5e57df 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -18,7 +18,6 @@ Console 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 component in favor of the `ErrorHandler` component @@ -307,45 +306,35 @@ TwigBundle * 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: - Before (`templates/bundles/TwigBundle/Exception/error.jsonld.twig`): + Before (`templates/bundles/TwigBundle/Exception/error.json.twig`): ```twig { - "@id": "https://example.com", - "@type": "error", - "@context": { - "title": "{{ status_text }}", - "code": {{ status_code }}, - "message": "{{ exception.message }}" - } + "type": "https://example.com/error", + "title": "{{ status_text }}", + "status": {{ status_code }} } ``` - After (`App\ErrorRenderer\JsonLdErrorRenderer`): + After (`App\Serializer\ProblemJsonNormalizer`): ```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([ - '@id' => 'https://example.com', - '@type' => 'error', - '@context' => [ - 'title' => $exception->getTitle(), - 'code' => $exception->getStatusCode(), - 'message' => $exception->getMessage(), - ], - ]); + return 'json' === $format && $data instanceof FlattenException; } } ``` - Configure your rendering service tagging it with `error_renderer.renderer`. - Validator --------- diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index e821a0a1382dd..b9a318f48f556 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -57,7 +57,6 @@ Console 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 component in favor of the `ErrorHandler` component diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 173165b03eb4a..ed85fa2ea7586 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -33,7 +33,6 @@ use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\ErrorHandler\ErrorHandler; -use Symfony\Component\ErrorRenderer\DependencyInjection\ErrorRendererPass; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\Form\DependencyInjection\FormPass; use Symfony\Component\HttpClient\DependencyInjection\HttpClientPass; @@ -92,7 +91,6 @@ public function build(ContainerBuilder $container) KernelEvents::FINISH_REQUEST, ]; - $container->addCompilerPass(new ErrorRendererPass()); $container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); $container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml index 73b9eff6fe426..7276892940acb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml @@ -194,12 +194,6 @@ - - - - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml index 4e3fc596f943c..6d5b713910cbe 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml @@ -5,12 +5,12 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - - + + + - %kernel.debug% %kernel.charset% @@ -18,22 +18,5 @@ - - - - %kernel.debug% - - - - - - %kernel.debug% - %kernel.charset% - - - - - %kernel.debug% - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 23da8b07bcb04..ac86d38916794 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -59,6 +59,12 @@ + + %kernel.debug% + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index e3faf21102453..c7be94a943632 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -50,7 +50,7 @@ "symfony/process": "^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/serializer": "^4.3|^5.0", + "symfony/serializer": "^4.4|^5.0", "symfony/stopwatch": "^3.4|^4.0|^5.0", "symfony/translation": "^4.4|^5.0", "symfony/templating": "^3.4|^4.0|^5.0", diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php index 4fbdb27c65989..a69f5e591d1fa 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php @@ -70,6 +70,6 @@ public function testDefaultJsonLoginBadRequest() $this->assertSame(400, $response->getStatusCode()); $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)); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml index d6ed10e896ff9..3522f27f13898 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml @@ -1,6 +1,9 @@ imports: - { resource: ./../config/framework.yml } +framework: + serializer: ~ + security: encoders: Symfony\Component\Security\Core\User\User: plaintext diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index 7f3a55477b095..c9202bad57b70 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -35,6 +35,7 @@ "symfony/form": "^3.4|^4.0|^5.0", "symfony/framework-bundle": "^4.4|^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/twig-bundle": "^4.4|^5.0", "symfony/twig-bridge": "^3.4|^4.0|^5.0", diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ErrorRendererPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ErrorRendererPass.php new file mode 100644 index 0000000000000..07886fa6766aa --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ErrorRendererPass.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\TwigBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Configures the Twig-based HTML error renderer. + * + * @author Yonel Ceruto + */ +final class ErrorRendererPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + $container->getDefinition('error_renderer')->setArgument(0, new Reference('twig.error_renderer.html')); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/ErrorRenderer/TwigHtmlErrorRenderer.php b/src/Symfony/Bundle/TwigBundle/ErrorRenderer/TwigHtmlErrorRenderer.php index b9c876a273cce..d649003e76a44 100644 --- a/src/Symfony/Bundle/TwigBundle/ErrorRenderer/TwigHtmlErrorRenderer.php +++ b/src/Symfony/Bundle/TwigBundle/ErrorRenderer/TwigHtmlErrorRenderer.php @@ -11,8 +11,8 @@ namespace Symfony\Bundle\TwigBundle\ErrorRenderer; -use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface; use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRendererInterface; use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Twig\Environment; use Twig\Error\LoaderError; @@ -24,7 +24,7 @@ * * @author Yonel Ceruto */ -class TwigHtmlErrorRenderer implements ErrorRendererInterface +class TwigHtmlErrorRenderer implements HtmlErrorRendererInterface { private $twig; private $htmlErrorRenderer; @@ -37,14 +37,6 @@ public function __construct(Environment $twig, HtmlErrorRenderer $htmlErrorRende $this->debug = $debug; } - /** - * {@inheritdoc} - */ - public static function getFormat(): string - { - return 'html'; - } - /** * {@inheritdoc} */ @@ -66,7 +58,7 @@ public function render(FlattenException $exception): string 'legacy' => false, // to be removed in 5.0 'exception' => $exception, 'status_code' => $exception->getStatusCode(), - 'status_text' => $exception->getTitle(), + 'status_text' => $exception->getStatusText(), ]); } diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index fa886981e9000..4195d84d8dff8 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -163,7 +163,6 @@ - %kernel.debug% diff --git a/src/Symfony/Bundle/TwigBundle/TwigBundle.php b/src/Symfony/Bundle/TwigBundle/TwigBundle.php index bd766c15219e7..8415c7a96aa93 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigBundle.php +++ b/src/Symfony/Bundle/TwigBundle/TwigBundle.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\TwigBundle; +use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ErrorRendererPass; use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ExceptionListenerPass; use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ExtensionPass; use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\RuntimeLoaderPass; @@ -37,6 +38,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new TwigLoaderPass()); $container->addCompilerPass(new ExceptionListenerPass()); $container->addCompilerPass(new RuntimeLoaderPass(), PassConfig::TYPE_BEFORE_REMOVING); + $container->addCompilerPass(new ErrorRendererPass()); } public function registerCommands(Application $application) diff --git a/src/Symfony/Component/ErrorRenderer/Command/DebugCommand.php b/src/Symfony/Component/ErrorRenderer/Command/DebugCommand.php deleted file mode 100644 index 58cb57b68cbf5..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/Command/DebugCommand.php +++ /dev/null @@ -1,125 +0,0 @@ - - * - * 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 - * - * @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 %command.name% command displays all available error renderers and -their formats: - - php %command.full_name% - -Or output a sample in a specific format: - - php %command.full_name% json - -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('%s', $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('%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()); - } -} diff --git a/src/Symfony/Component/ErrorRenderer/DependencyInjection/ErrorRendererPass.php b/src/Symfony/Component/ErrorRenderer/DependencyInjection/ErrorRendererPass.php deleted file mode 100644 index 1e297b7de13b3..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/DependencyInjection/ErrorRendererPass.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * 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 - */ -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); - } - } -} diff --git a/src/Symfony/Component/ErrorRenderer/DependencyInjection/LazyLoadingErrorRenderer.php b/src/Symfony/Component/ErrorRenderer/DependencyInjection/LazyLoadingErrorRenderer.php deleted file mode 100644 index 82936a29af099..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/DependencyInjection/LazyLoadingErrorRenderer.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * 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 - */ -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); - } -} diff --git a/src/Symfony/Component/ErrorRenderer/ErrorRenderer.php b/src/Symfony/Component/ErrorRenderer/ErrorRenderer.php index da984bdd18895..6b72edc2cace0 100644 --- a/src/Symfony/Component/ErrorRenderer/ErrorRenderer.php +++ b/src/Symfony/Component/ErrorRenderer/ErrorRenderer.php @@ -11,9 +11,11 @@ namespace Symfony\Component\ErrorRenderer; -use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface; -use Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException; +use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRendererInterface; use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\Serializer\Exception\NotEncodableValueException; +use Symfony\Component\Serializer\SerializerInterface; /** * Formats an exception to be used as response content. @@ -26,30 +28,13 @@ */ class ErrorRenderer { - private $renderers = []; + private $htmlErrorRenderer; + private $serializer; - /** - * @param ErrorRendererInterface[] $renderers - */ - public function __construct(iterable $renderers) + public function __construct(HtmlErrorRendererInterface $htmlErrorRenderer = null, SerializerInterface $serializer = null) { - 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; + $this->htmlErrorRenderer = $htmlErrorRenderer ?? new HtmlErrorRenderer(); + $this->serializer = $serializer; } /** @@ -59,19 +44,23 @@ public function addRenderer(ErrorRendererInterface $renderer, string $format = n * @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); + if ('html' === $format || null === $this->serializer) { + return $this->htmlErrorRenderer->render($exception); + } + + try { + $context = isset($exception->getHeaders()['X-Debug']) ? ['debug' => $exception->getHeaders()['X-Debug']] : []; + + return $this->serializer->serialize($exception, $format, $context); + } catch (NotEncodableValueException $_) { + return $this->htmlErrorRenderer->render($exception); + } } } diff --git a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRenderer.php b/src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRenderer.php index a9d0af516da13..7a9efa90cbcf4 100644 --- a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRenderer.php +++ b/src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRenderer.php @@ -20,7 +20,7 @@ /** * @author Yonel Ceruto */ -class HtmlErrorRenderer implements ErrorRendererInterface +class HtmlErrorRenderer implements HtmlErrorRendererInterface { private const GHOST_ADDONS = [ '02-14' => self::GHOST_HEART, @@ -49,14 +49,6 @@ public function __construct(bool $debug = false, string $charset = null, $fileLi $this->logger = $logger; } - /** - * {@inheritdoc} - */ - public static function getFormat(): string - { - return 'html'; - } - /** * {@inheritdoc} */ @@ -92,7 +84,7 @@ public function getStylesheet(): 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->getTitle()); + $statusText = $this->escape($exception->getStatusText()); $statusCode = $this->escape($exception->getStatusCode()); if (!$debug) { diff --git a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/ErrorRendererInterface.php b/src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRendererInterface.php similarity index 71% rename from src/Symfony/Component/ErrorRenderer/ErrorRenderer/ErrorRendererInterface.php rename to src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRendererInterface.php index 5c0d58060202d..dc85aa509f6e7 100644 --- a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/ErrorRendererInterface.php +++ b/src/Symfony/Component/ErrorRenderer/ErrorRenderer/HtmlErrorRendererInterface.php @@ -14,17 +14,12 @@ use Symfony\Component\ErrorRenderer\Exception\FlattenException; /** - * Interface for classes that can render errors in a specific format. + * Interface for classes that can render errors in HTML format. * * @author Yonel Ceruto */ -interface ErrorRendererInterface +interface HtmlErrorRendererInterface { - /** - * Gets the format this renderer can return errors as. - */ - public static function getFormat(): string; - /** * Returns the response content of the rendered exception. */ diff --git a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/JsonErrorRenderer.php b/src/Symfony/Component/ErrorRenderer/ErrorRenderer/JsonErrorRenderer.php deleted file mode 100644 index d708fb0f15c85..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/JsonErrorRenderer.php +++ /dev/null @@ -1,60 +0,0 @@ - - * - * 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 - */ -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); - } -} diff --git a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/TxtErrorRenderer.php b/src/Symfony/Component/ErrorRenderer/ErrorRenderer/TxtErrorRenderer.php deleted file mode 100644 index 2bafb2cfb4d8b..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/TxtErrorRenderer.php +++ /dev/null @@ -1,104 +0,0 @@ - - * - * 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 - */ -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); - } -} diff --git a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/XmlErrorRenderer.php b/src/Symfony/Component/ErrorRenderer/ErrorRenderer/XmlErrorRenderer.php deleted file mode 100644 index 290e0a63ffef1..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/ErrorRenderer/XmlErrorRenderer.php +++ /dev/null @@ -1,125 +0,0 @@ - - * - * 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 - */ -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 .= ''; - foreach ($exception->toArray() as $e) { - $exceptions .= sprintf('', $e['class'], $this->escapeXml($e['message'])); - foreach ($e['trace'] as $trace) { - $exceptions .= ''; - 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 .= ''; - } - $exceptions .= ''; - } - $exceptions .= ''; - } - - return << - - {$title} - {$statusCode} - {$message} - {$exceptions} - -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); - } -} diff --git a/src/Symfony/Component/ErrorRenderer/Exception/ErrorRendererNotFoundException.php b/src/Symfony/Component/ErrorRenderer/Exception/ErrorRendererNotFoundException.php deleted file mode 100644 index 4020ced161fc1..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/Exception/ErrorRendererNotFoundException.php +++ /dev/null @@ -1,16 +0,0 @@ - - * - * 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 -{ -} diff --git a/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php b/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php index da3454e05eb04..ad890a89030cb 100644 --- a/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php +++ b/src/Symfony/Component/ErrorRenderer/Exception/FlattenException.php @@ -26,7 +26,6 @@ */ class FlattenException extends LegacyFlattenException { - private $title; private $message; private $code; private $previous; @@ -34,6 +33,7 @@ class FlattenException extends LegacyFlattenException private $traceAsString; private $class; private $statusCode; + private $statusText; private $headers; private $file; private $line; @@ -61,12 +61,12 @@ public static function createFromThrowable(\Throwable $exception, int $statusCod } if (class_exists(Response::class) && isset(Response::$statusTexts[$statusCode])) { - $title = Response::$statusTexts[$statusCode]; + $statusText = Response::$statusTexts[$statusCode]; } 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->setHeaders($headers); $e->setTraceFromThrowable($exception); @@ -172,14 +172,14 @@ public function setLine($line): self 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; } diff --git a/src/Symfony/Component/ErrorRenderer/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/ErrorRenderer/Tests/Command/DebugCommandTest.php deleted file mode 100644 index c5a9768c8bb5a..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/Tests/Command/DebugCommandTest.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * 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(<<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(<<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]); - } -} diff --git a/src/Symfony/Component/ErrorRenderer/Tests/DependencyInjection/ErrorRendererPassTest.php b/src/Symfony/Component/ErrorRenderer/Tests/DependencyInjection/ErrorRendererPassTest.php deleted file mode 100644 index e69fd860b85d8..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/Tests/DependencyInjection/ErrorRendererPassTest.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * 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)); - } -} diff --git a/src/Symfony/Component/ErrorRenderer/Tests/DependencyInjection/LazyLoadingErrorRendererTest.php b/src/Symfony/Component/ErrorRenderer/Tests/DependencyInjection/LazyLoadingErrorRendererTest.php deleted file mode 100644 index 5811a0c026e0e..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/Tests/DependencyInjection/LazyLoadingErrorRendererTest.php +++ /dev/null @@ -1,66 +0,0 @@ - - * - * 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(); - } -} diff --git a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/HtmlErrorRendererTest.php b/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/HtmlErrorRendererTest.php index 4868d215c47ce..cc4b556157106 100644 --- a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/HtmlErrorRendererTest.php +++ b/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/HtmlErrorRendererTest.php @@ -12,8 +12,8 @@ namespace Symfony\Component\ErrorRenderer\Tests\ErrorRenderer; use PHPUnit\Framework\TestCase; -use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface; use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRendererInterface; use Symfony\Component\ErrorRenderer\Exception\FlattenException; class HtmlErrorRendererTest extends TestCase @@ -21,7 +21,7 @@ class HtmlErrorRendererTest extends TestCase /** * @dataProvider getRenderData */ - public function testRender(FlattenException $exception, ErrorRendererInterface $errorRenderer, string $expected) + public function testRender(FlattenException $exception, HtmlErrorRendererInterface $errorRenderer, string $expected) { $this->assertStringMatchesFormat($expected, $errorRenderer->render($exception)); } diff --git a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/JsonErrorRendererTest.php b/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/JsonErrorRendererTest.php deleted file mode 100644 index 55e6d5cdf227d..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/JsonErrorRendererTest.php +++ /dev/null @@ -1,76 +0,0 @@ - - * - * 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 = <<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, - ]; - } -} diff --git a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/TxtErrorRendererTest.php b/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/TxtErrorRendererTest.php deleted file mode 100644 index b6c1570df88d7..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/TxtErrorRendererTest.php +++ /dev/null @@ -1,69 +0,0 @@ - - * - * 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 = <<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, - ]; - } -} diff --git a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/XmlErrorRendererTest.php b/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/XmlErrorRendererTest.php deleted file mode 100644 index 3a756720ecc46..0000000000000 --- a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRenderer/XmlErrorRendererTest.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * 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 = << - - Internal Server Error - 500 - Foo - %A - -XML; - - $expectedNonDebug = << - - Internal Server Error - 500 - Whoops, looks like something went wrong. - - -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, - ]; - } -} diff --git a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRendererTest.php b/src/Symfony/Component/ErrorRenderer/Tests/ErrorRendererTest.php index 11ef6a7d7eb4f..541b56db6e2d1 100644 --- a/src/Symfony/Component/ErrorRenderer/Tests/ErrorRendererTest.php +++ b/src/Symfony/Component/ErrorRenderer/Tests/ErrorRendererTest.php @@ -13,42 +13,39 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorRenderer\ErrorRenderer; -use Symfony\Component\ErrorRenderer\ErrorRenderer\ErrorRendererInterface; +use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRendererInterface; use Symfony\Component\ErrorRenderer\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 testErrorRendererNotFound() + public function testDefaultContent() { - $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'); + $errorRenderer = new ErrorRenderer(); + + self::assertStringContainsString('

The server returned a "500 Internal Server Error".

', $errorRenderer->render(new \RuntimeException(), 'html')); } - public function testInvalidErrorRenderer() + public function testCustomContent() { - $this->expectException('TypeError'); - new ErrorRenderer([new \stdClass()]); + $errorRenderer = new ErrorRenderer(new CustomHtmlErrorRenderer()); + + $this->assertSame('Foo', $errorRenderer->render(new \RuntimeException('Foo'), 'html')); } - public function testCustomErrorRenderer() + public function testSerializerContent() { - $renderers = [new FooErrorRenderer()]; - $errorRenderer = new ErrorRenderer($renderers); + $exception = new \RuntimeException('Foo'); + $errorRenderer = new ErrorRenderer(null, new Serializer([new ProblemNormalizer()], [new JsonEncoder()])); - $exception = FlattenException::createFromThrowable(new \RuntimeException('Foo')); - $this->assertSame('Foo', $errorRenderer->render($exception, 'foo')); + $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 FooErrorRenderer implements ErrorRendererInterface +class CustomHtmlErrorRenderer implements HtmlErrorRendererInterface { - public static function getFormat(): string - { - return 'foo'; - } - public function render(FlattenException $exception): string { return $exception->getMessage(); diff --git a/src/Symfony/Component/ErrorRenderer/composer.json b/src/Symfony/Component/ErrorRenderer/composer.json index 90351239fc689..75ce751b347d3 100644 --- a/src/Symfony/Component/ErrorRenderer/composer.json +++ b/src/Symfony/Component/ErrorRenderer/composer.json @@ -27,7 +27,8 @@ "require-dev": { "symfony/console": "^4.4", "symfony/dependency-injection": "^4.4", - "symfony/http-kernel": "^4.4" + "symfony/http-kernel": "^4.4", + "symfony/serializer": "^4.4|^5.0" }, "conflict": { "symfony/http-kernel": "<4.4" diff --git a/src/Symfony/Component/HttpKernel/Controller/ErrorController.php b/src/Symfony/Component/HttpKernel/Controller/ErrorController.php index 4d58a61120cd0..5b9c5165d96d1 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ErrorController.php +++ b/src/Symfony/Component/HttpKernel/Controller/ErrorController.php @@ -12,7 +12,6 @@ namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\ErrorRenderer\ErrorRenderer; -use Symfony\Component\ErrorRenderer\Exception\ErrorRendererNotFoundException; use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -39,11 +38,9 @@ public function __construct(HttpKernelInterface $kernel, $controller, ErrorRende public function __invoke(Request $request, FlattenException $exception): Response { - try { - return new Response($this->errorRenderer->render($exception, $request->getPreferredFormat()), $exception->getStatusCode(), $exception->getHeaders()); - } catch (ErrorRendererNotFoundException $_) { - return new Response($this->errorRenderer->render($exception), $exception->getStatusCode(), $exception->getHeaders()); - } + $content = $this->errorRenderer->render($exception, $request->getPreferredFormat()); + + return new Response($content, $exception->getStatusCode(), $exception->getHeaders()); } public function preview(Request $request, int $code): Response diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php index 4a64af3ab1e33..99f7679271e8b 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ErrorControllerTest.php @@ -14,7 +14,6 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\ErrorRenderer\ErrorRenderer; use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; -use Symfony\Component\ErrorRenderer\ErrorRenderer\JsonErrorRenderer; use Symfony\Component\ErrorRenderer\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -31,7 +30,7 @@ class ErrorControllerTest extends TestCase public function testInvokeController(Request $request, FlattenException $exception, int $statusCode, string $content) { $kernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); - $errorRenderer = new ErrorRenderer([new HtmlErrorRenderer(), new JsonErrorRenderer()]); + $errorRenderer = new ErrorRenderer(new HtmlErrorRenderer()); $controller = new ErrorController($kernel, null, $errorRenderer); $response = $controller($request, $exception); @@ -55,33 +54,6 @@ public function getInvokeControllerDataProvider() '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->attributes->set('_format', 'unknown'); yield 'default HTML format for unknown formats' => [ @@ -116,7 +88,7 @@ public function testPreviewController() ) ->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)); } diff --git a/src/Symfony/Component/Serializer/CHANGELOG.md b/src/Symfony/Component/Serializer/CHANGELOG.md index 0958f5054062a..7f213186e3c7f 100644 --- a/src/Symfony/Component/Serializer/CHANGELOG.md +++ b/src/Symfony/Component/Serializer/CHANGELOG.md @@ -6,6 +6,7 @@ CHANGELOG * 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 `ProblemNormalizer` to normalize errors according to the API Problem spec (RFC 7807) 4.3.0 ----- diff --git a/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php b/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php new file mode 100644 index 0000000000000..dec64b61f7592 --- /dev/null +++ b/src/Symfony/Component/Serializer/Normalizer/ProblemNormalizer.php @@ -0,0 +1,79 @@ + + * + * 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\ErrorRenderer\Exception\FlattenException; + +/** + * Normalizes errors according to the API Problem spec (RFC 7807). + * + * @see https://tools.ietf.org/html/rfc7807 + * + * @author Kévin Dunglas + * @author Yonel Ceruto + */ +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 = []) + { + if (!$exception instanceof FlattenException) { + $exception = FlattenException::createFromThrowable($exception); + } + + $context += $this->defaultContext; + $debug = $this->debug && ($context['debug'] ?? true); + + $data = [ + 'type' => $context['type'], + 'title' => $context['title'], + 'status' => $context['status'] ?? $exception->getStatusCode(), + 'detail' => $debug ? $exception->getMessage() : $exception->getStatusText(), + ]; + if ($debug) { + $data['class'] = $exception->getClass(); + $data['trace'] = $exception->getTrace(); + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function supportsNormalization($data, $format = null): bool + { + return $data instanceof \Exception || $data instanceof FlattenException; + } + + /** + * {@inheritdoc} + */ + public function hasCacheableSupportsMethod(): bool + { + return true; + } +} diff --git a/src/Symfony/Component/Serializer/Tests/Normalizer/ProblemNormalizerTest.php b/src/Symfony/Component/Serializer/Tests/Normalizer/ProblemNormalizerTest.php new file mode 100644 index 0000000000000..409fbb5ffd98b --- /dev/null +++ b/src/Symfony/Component/Serializer/Tests/Normalizer/ProblemNormalizerTest.php @@ -0,0 +1,48 @@ + + * + * 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\ErrorRenderer\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(new \Exception())); + $this->assertTrue($this->normalizer->supportsNormalization(FlattenException::createFromThrowable(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(new \RuntimeException('Error'))); + } +} diff --git a/src/Symfony/Component/Serializer/composer.json b/src/Symfony/Component/Serializer/composer.json index 45c185a364e7e..da613fd62895a 100644 --- a/src/Symfony/Component/Serializer/composer.json +++ b/src/Symfony/Component/Serializer/composer.json @@ -20,6 +20,7 @@ "symfony/polyfill-ctype": "~1.8" }, "require-dev": { + "symfony/error-renderer": "^4.4|^5.0", "symfony/yaml": "^3.4|^4.0|^5.0", "symfony/config": "^3.4|^4.0|^5.0", "symfony/property-access": "^3.4|^4.0|^5.0", pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy