Skip to content

Commit

Permalink
moved all twig related aspects of the ExceptionController into a new …
Browse files Browse the repository at this point in the history
…TwigExceptionController
  • Loading branch information
lsmith77 committed Dec 29, 2015
1 parent d1c0c87 commit 22d0f1f
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 84 deletions.
117 changes: 40 additions & 77 deletions Controller/ExceptionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@
namespace FOS\RestBundle\Controller;

use FOS\RestBundle\Negotiation\FormatNegotiator;
use FOS\RestBundle\Util\ExceptionWrapper;
use FOS\RestBundle\Util\StopFormatListenerException;
use FOS\RestBundle\Util\ExceptionWrapper;
use FOS\RestBundle\View\ExceptionWrapperHandlerInterface;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Debug\Exception\FlattenException;
Expand All @@ -32,7 +30,6 @@ class ExceptionController
private $exceptionWrapperHandler;
private $formatNegotiator;
private $viewHandler;
private $templating;
private $exceptionCodes;
private $exceptionMessages;
private $showException;
Expand All @@ -41,31 +38,24 @@ public function __construct(
ExceptionWrapperHandlerInterface $exceptionWrapperHandler,
FormatNegotiator $formatNegotiator,
ViewHandlerInterface $viewHandler,
EngineInterface $templating,
array $exceptionCodes,
array $exceptionMessages,
$showException
) {
$this->exceptionWrapperHandler = $exceptionWrapperHandler;
$this->formatNegotiator = $formatNegotiator;
$this->viewHandler = $viewHandler;
$this->templating = $templating;
$this->exceptionCodes = $exceptionCodes;
$this->exceptionMessages = $exceptionMessages;
$this->showException = $showException;
}

/**
* Creates a new ExceptionWrapper instance that can be overwritten by a custom
* ExceptionController class.
*
* @param array $parameters Template parameters
*
* @return ExceptionWrapper ExceptionWrapper instance
* @return ViewHandlerInterface
*/
protected function createExceptionWrapper(array $parameters)
protected function getViewHandler()
{
return $this->exceptionWrapperHandler->wrap($parameters);
return $this->viewHandler;
}

/**
Expand Down Expand Up @@ -97,20 +87,11 @@ public function showAction(Request $request, FlattenException $exception, DebugL
$request->headers->get('X-Php-Ob-Level', -1)
);
$code = $this->getStatusCode($exception);
$parameters = $this->getParameters($this->viewHandler, $currentContent, $code, $exception, $logger, $format);
$parameters = $this->getParameters($currentContent, $code, $exception, $logger, $format);
$showException = $request->attributes->get('showException', $this->showException);

try {
if (!$this->viewHandler->isFormatTemplating($format)) {
$parameters = $this->createExceptionWrapper($parameters);
}

$view = View::create($parameters, $code, $exception->getHeaders());
$view->setFormat($format);

if ($this->viewHandler->isFormatTemplating($format)) {
$view->setTemplate($this->findTemplate($request, $format, $code, $showException));
}
$view = $this->createView($format, $exception, $code, $parameters, $request, $showException);

$response = $this->viewHandler->handle($view);
} catch (\Exception $e) {
Expand Down Expand Up @@ -138,6 +119,38 @@ private function createPlainResponse($content, $status, $headers)
return new Response($content, $status, $headers);
}

/**
* Creates a new ExceptionWrapper instance that can be overwritten by a custom
* ExceptionController class.
*
* @param array $parameters output data
*
* @return ExceptionWrapper ExceptionWrapper instance
*/
protected function createExceptionWrapper(array $parameters)
{
return $this->exceptionWrapperHandler->wrap($parameters);
}

/**
* @param string $format
* @param FlattenException $exception
* @param int $code
* @param array $parameters
* @param Request $request
* @param bool $showException
*
* @return View
*/
protected function createView($format, FlattenException $exception, $code, $parameters, Request $request, $showException)
{
$parameters = $this->createExceptionWrapper($parameters);
$view = View::create($parameters, $code, $exception->getHeaders());
$view->setFormat($format);

return $view;
}

/**
* Gets and cleans any content that was already outputted.
*
Expand Down Expand Up @@ -247,7 +260,6 @@ protected function getFormat(Request $request, $format)
* Overwrite it in a custom ExceptionController class to add additionally parameters
* that should be passed to the view layer.
*
* @param ViewHandlerInterface $viewHandler
* @param string $currentContent
* @param int $code
* @param FlattenException $exception
Expand All @@ -256,64 +268,15 @@ protected function getFormat(Request $request, $format)
*
* @return array
*/
protected function getParameters(ViewHandlerInterface $viewHandler, $currentContent, $code, $exception, DebugLoggerInterface $logger = null, $format = 'html')
protected function getParameters($currentContent, $code, $exception, DebugLoggerInterface $logger = null, $format = 'html')
{
$parameters = [
return [
'status' => 'error',
'status_code' => $code,
'status_text' => array_key_exists($code, Response::$statusTexts) ? Response::$statusTexts[$code] : 'error',
'currentContent' => $currentContent,
'message' => $this->getExceptionMessage($exception),
'exception' => $exception,
];

if ($viewHandler->isFormatTemplating($format)) {
$parameters['logger'] = $logger;
}

return $parameters;
}

/**
* Finds the template for the given format and status code.
*
* Note this method needs to be overridden in case another
* engine than Twig should be supported;
*
* This code is inspired by TwigBundle and should be synchronized on a regular basis
* see src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php
*
* @param Request $request
* @param string $format
* @param int $statusCode
* @param bool $showException
*
* @return TemplateReference
*/
private function findTemplate(Request $request, $format, $statusCode, $showException)
{
$name = $showException ? 'exception' : 'error';
if ($showException && 'html' == $format) {
$name = 'exception_full';
}

// when not in debug, try to find a template for the specific HTTP status code and format
if (!$showException) {
$template = new TemplateReference('TwigBundle', 'Exception', $name.$statusCode, $format, 'twig');
if ($this->templating->exists($template)) {
return $template;
}
}

// try to find a template for the given format
$template = new TemplateReference('TwigBundle', 'Exception', $name, $format, 'twig');
if ($this->templating->exists($template)) {
return $template;
}

// default to a generic HTML exception
$request->setRequestFormat('html');

return new TemplateReference('TwigBundle', 'Exception', $showException ? 'exception_full' : $name, 'html', 'twig');
}
}
117 changes: 117 additions & 0 deletions Controller/TwigExceptionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

/*
* This file is part of the FOSRestBundle package.
*
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FOS\RestBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;

/**
* Custom ExceptionController that uses the view layer and supports HTTP response status code mapping.
* It additionally is able to prepare the template parameters for the core EngineInterface.
*/
class TwigExceptionController extends ExceptionController
{
/**
* @var EngineInterface
*/
private $templating;

public function setTemplating(EngineInterface $templating)
{
$this->templating = $templating;
}

public function getTemplating()
{
if (!$this->templating instanceof EngineInterface) {
throw new \RuntimeException('No templating engine set');
}

return $this->templating;
}

/**
* {inheritDoc}.
*/
protected function createView($format, FlattenException $exception, $code, $parameters, Request $request, $showException)
{
$view = parent::createView($format, $exception, $code, $parameters, $request, $showException);

if ($this->getViewHandler()->isFormatTemplating($format)) {
$view->setTemplate($this->findTemplate($request, $format, $code, $showException));
$view->setData($parameters);
}

return $view;
}

/**
* {inheritDoc}.
*/
protected function getParameters($currentContent, $code, $exception, DebugLoggerInterface $logger = null, $format = 'html')
{
$parameters = parent::getParameters($currentContent, $code, $exception, $logger, $format);

if ($this->getViewHandler()->isFormatTemplating($format)) {
$parameters['logger'] = $logger;
}

return $parameters;
}

/**
* Finds the template for the given format and status code.
*
* Note this method needs to be overridden in case another
* engine than Twig should be supported;
*
* This code is inspired by TwigBundle and should be synchronized on a regular basis
* see src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php
*
* @param Request $request
* @param string $format
* @param int $statusCode
* @param bool $showException
*
* @return TemplateReference
*/
protected function findTemplate(Request $request, $format, $statusCode, $showException)
{
$name = $showException ? 'exception' : 'error';
if ($showException && 'html' == $format) {
$name = 'exception_full';
}

// when not in debug, try to find a template for the specific HTTP status code and format
if (!$showException) {
$template = new TemplateReference('TwigBundle', 'Exception', $name.$statusCode, $format, 'twig');
if ($this->getTemplating()->exists($template)) {
return $template;
}
}

// try to find a template for the given format
$template = new TemplateReference('TwigBundle', 'Exception', $name, $format, 'twig');
if ($this->getTemplating()->exists($template)) {
return $template;
}

// default to a generic HTML exception
$request->setRequestFormat('html');

return new TemplateReference('TwigBundle', 'Exception', $showException ? 'exception_full' : $name, 'html', 'twig');
}
}
8 changes: 5 additions & 3 deletions DependencyInjection/FOSRestExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -345,15 +345,17 @@ private function loadException(array $config, XmlFileLoader $loader, ContainerBu

if ($config['exception']['exception_controller']) {
$container->getDefinition('fos_rest.exception_listener')->replaceArgument(0, $config['exception']['exception_controller']);
} elseif (isset($container->getParameter('kernel.bundles')['TwigBundle'])) {
$container->getDefinition('fos_rest.exception_listener')->replaceArgument(0, 'fos_rest.exception.twig_controller:showAction');
}

if ($config['view']['mime_types']['enabled']) {
$container->getDefinition('fos_rest.exception_format_negotiator')->replaceArgument(1, $config['view']['mime_types']['formats']);
}

$exceptionController = $container->getDefinition('fos_rest.controller.exception');
$exceptionController->replaceArgument(4, $config['exception']['codes']);
$exceptionController->replaceArgument(5, $config['exception']['messages']);
$exceptionController = $container->getDefinition('fos_rest.exception.controller');
$exceptionController->replaceArgument(3, $config['exception']['codes']);
$exceptionController->replaceArgument(4, $config['exception']['messages']);
}

foreach ($config['exception']['codes'] as $exception => $code) {
Expand Down
11 changes: 8 additions & 3 deletions Resources/config/exception_listener.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,25 @@
<service id="fos_rest.exception_listener" class="Symfony\Component\HttpKernel\EventListener\ExceptionListener">
<tag name="kernel.event_subscriber" />
<tag name="monolog.logger" channel="request" />
<argument>fos_rest.controller.exception:showAction</argument>
<argument>fos_rest.exception.controller:showAction</argument>
<argument type="service" id="logger" on-invalid="null" />
</service>

<service id="fos_rest.controller.exception" class="FOS\RestBundle\Controller\ExceptionController">
<service id="fos_rest.exception.controller" class="FOS\RestBundle\Controller\ExceptionController">
<argument type="service" id="fos_rest.exception_handler" />
<argument type="service" id="fos_rest.exception_format_negotiator" />
<argument type="service" id="fos_rest.view_handler" />
<argument type="service" id="templating" />
<argument type="collection" /> <!-- exception codes -->
<argument type="collection" /> <!-- exception messages -->
<argument>%kernel.debug%</argument>
</service>

<service id="fos_rest.exception.twig_controller" class="FOS\RestBundle\Controller\TwigExceptionController" parent="fos_rest.exception.controller" public="false">
<call method="setTemplating">
<argument type="service" id="templating.engine.twig" />
</call>
</service>

<service id="fos_rest.exception_format_negotiator" class="FOS\RestBundle\Negotiation\FormatNegotiator" >
<argument type="service" id="request_stack" />
<argument type="collection" /> <!-- mime types -->
Expand Down
11 changes: 10 additions & 1 deletion Resources/doc/4-exception-controller-support.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,16 @@ Alternatively the TwigBundle configuration can be used to enable the ExceptionCo
# app/config/config.yml
twig:
exception_controller: 'FOS\RestBundle\Controller\ExceptionController::showAction'
exception_controller: 'fos_rest.exception.twig_controller:showAction'
.. note::

FOSRestBundle defines to services for exception rendering, by default it
configures ``fos_rest.exception.controller`` which only supports rendering
via a serializer. In case no explicit controller is configured by the user
and TwigBundle is detected it will automatically configure
``fos_rest.exception.twig_controller`` which additionally also supports
rendering via Twig.

When enabling the RestBundle view-layer-aware ExceptionController it automatically
disables the TwigBundle exception listener and subsequent configuration.
Expand Down

3 comments on commit 22d0f1f

@phoenixgao
Copy link

Choose a reason for hiding this comment

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

after update I got this error

You have requested a non-existent service "fos_rest.exception.twig_controller"

What should I change?

@xabbuh
Copy link
Member

@xabbuh xabbuh commented on 22d0f1f Dec 31, 2015

Choose a reason for hiding this comment

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

@phoenixgao There's nothing you can do right now than switching back to a version not containing this commit. But we are aware of this and will fix it soon.

@xabbuh
Copy link
Member

@xabbuh xabbuh commented on 22d0f1f Dec 31, 2015

Choose a reason for hiding this comment

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

see also #1288 (comment)

Please sign in to comment.