vendor/friendsofsymfony/rest-bundle/View/ViewHandler.php line 373

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the FOSRestBundle package.
  4.  *
  5.  * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace FOS\RestBundle\View;
  11. use FOS\RestBundle\Context\Context;
  12. use FOS\RestBundle\Serializer\Serializer;
  13. use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
  14. use Symfony\Component\Form\FormInterface;
  15. use Symfony\Component\HttpFoundation\RedirectResponse;
  16. use Symfony\Component\HttpFoundation\Request;
  17. use Symfony\Component\HttpFoundation\RequestStack;
  18. use Symfony\Component\HttpFoundation\Response;
  19. use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
  20. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  21. use Symfony\Component\Templating\TemplateReferenceInterface;
  22. /**
  23.  * View may be used in controllers to build up a response in a format agnostic way
  24.  * The View class takes care of encoding your data in json, xml, or renders a
  25.  * template for html via the Serializer component.
  26.  *
  27.  * @author Jordi Boggiano <j.boggiano@seld.be>
  28.  * @author Lukas K. Smith <smith@pooteeweet.org>
  29.  */
  30. class ViewHandler implements ConfigurableViewHandlerInterface
  31. {
  32.     /**
  33.      * Key format, value a callable that returns a Response instance.
  34.      *
  35.      * @var array
  36.      */
  37.     protected $customHandlers = [];
  38.     /**
  39.      * The supported formats as keys and if the given formats
  40.      * uses templating is denoted by a true value.
  41.      *
  42.      * @var array
  43.      */
  44.     protected $formats;
  45.     /**
  46.      *  HTTP response status code for a failed validation.
  47.      *
  48.      * @var int
  49.      */
  50.     protected $failedValidationCode;
  51.     /**
  52.      * HTTP response status code when the view data is null.
  53.      *
  54.      * @var int
  55.      */
  56.     protected $emptyContentCode;
  57.     /**
  58.      * Whether or not to serialize null view data.
  59.      *
  60.      * @var bool
  61.      */
  62.     protected $serializeNull;
  63.     /**
  64.      * If to force a redirect for the given key format,
  65.      * with value being the status code to use.
  66.      *
  67.      * @var array
  68.      */
  69.     protected $forceRedirects;
  70.     /**
  71.      * @var string
  72.      */
  73.     protected $defaultEngine;
  74.     /**
  75.      * @var array
  76.      */
  77.     protected $exclusionStrategyGroups = [];
  78.     /**
  79.      * @var string
  80.      */
  81.     protected $exclusionStrategyVersion;
  82.     /**
  83.      * @var bool
  84.      */
  85.     protected $serializeNullStrategy;
  86.     private $urlGenerator;
  87.     private $serializer;
  88.     private $templating;
  89.     private $requestStack;
  90.     private $options;
  91.     /**
  92.      * Constructor.
  93.      *
  94.      * @param UrlGeneratorInterface $urlGenerator         The URL generator
  95.      * @param Serializer            $serializer
  96.      * @param EngineInterface       $templating           The configured templating engine
  97.      * @param RequestStack          $requestStack         The request stack
  98.      * @param array                 $formats              the supported formats as keys and if the given formats uses templating is denoted by a true value
  99.      * @param int                   $failedValidationCode The HTTP response status code for a failed validation
  100.      * @param int                   $emptyContentCode     HTTP response status code when the view data is null
  101.      * @param bool                  $serializeNull        Whether or not to serialize null view data
  102.      * @param array                 $forceRedirects       If to force a redirect for the given key format, with value being the status code to use
  103.      * @param string                $defaultEngine        default engine (twig, php ..)
  104.      * @param array                 $options              config options
  105.      */
  106.     public function __construct(
  107.         UrlGeneratorInterface $urlGenerator,
  108.         Serializer $serializer,
  109.         EngineInterface $templating null,
  110.         RequestStack $requestStack,
  111.         array $formats null,
  112.         $failedValidationCode Response::HTTP_BAD_REQUEST,
  113.         $emptyContentCode Response::HTTP_NO_CONTENT,
  114.         $serializeNull false,
  115.         array $forceRedirects null,
  116.         $defaultEngine 'twig',
  117.         array $options = []
  118.     ) {
  119.         $this->urlGenerator $urlGenerator;
  120.         $this->serializer $serializer;
  121.         $this->templating $templating;
  122.         $this->requestStack $requestStack;
  123.         $this->formats = (array) $formats;
  124.         $this->failedValidationCode $failedValidationCode;
  125.         $this->emptyContentCode $emptyContentCode;
  126.         $this->serializeNull $serializeNull;
  127.         $this->forceRedirects = (array) $forceRedirects;
  128.         $this->defaultEngine $defaultEngine;
  129.         $this->options $options + [
  130.             'exclusionStrategyGroups' => [],
  131.             'exclusionStrategyVersion' => null,
  132.             'serializeNullStrategy' => null,
  133.             ];
  134.         $this->reset();
  135.     }
  136.     /**
  137.      * Sets the default serialization groups.
  138.      *
  139.      * @param array|string $groups
  140.      */
  141.     public function setExclusionStrategyGroups($groups)
  142.     {
  143.         $this->exclusionStrategyGroups = (array) $groups;
  144.     }
  145.     /**
  146.      * Sets the default serialization version.
  147.      *
  148.      * @param string $version
  149.      */
  150.     public function setExclusionStrategyVersion($version)
  151.     {
  152.         $this->exclusionStrategyVersion $version;
  153.     }
  154.     /**
  155.      * If nulls should be serialized.
  156.      *
  157.      * @param bool $isEnabled
  158.      */
  159.     public function setSerializeNullStrategy($isEnabled)
  160.     {
  161.         $this->serializeNullStrategy $isEnabled;
  162.     }
  163.     /**
  164.      * {@inheritdoc}
  165.      */
  166.     public function supports($format)
  167.     {
  168.         return isset($this->customHandlers[$format]) || isset($this->formats[$format]);
  169.     }
  170.     /**
  171.      * Registers a custom handler.
  172.      *
  173.      * The handler must have the following signature: handler(ViewHandler $viewHandler, View $view, Request $request, $format)
  174.      * It can use the public methods of this class to retrieve the needed data and return a
  175.      * Response object ready to be sent.
  176.      *
  177.      * @param string   $format
  178.      * @param callable $callable
  179.      *
  180.      * @throws \InvalidArgumentException
  181.      */
  182.     public function registerHandler($format$callable)
  183.     {
  184.         if (!is_callable($callable)) {
  185.             throw new \InvalidArgumentException('Registered view callback must be callable.');
  186.         }
  187.         $this->customHandlers[$format] = $callable;
  188.     }
  189.     /**
  190.      * Gets a response HTTP status code from a View instance.
  191.      *
  192.      * By default it will return 200. However if there is a FormInterface stored for
  193.      * the key 'form' in the View's data it will return the failed_validation
  194.      * configuration if the form instance has errors.
  195.      *
  196.      * @param View  $view
  197.      * @param mixed $content
  198.      *
  199.      * @return int HTTP status code
  200.      */
  201.     protected function getStatusCode(View $view$content null)
  202.     {
  203.         $form $this->getFormFromView($view);
  204.         if ($form && $form->isSubmitted() && !$form->isValid()) {
  205.             return $this->failedValidationCode;
  206.         }
  207.         $statusCode $view->getStatusCode();
  208.         if (null !== $statusCode) {
  209.             return $statusCode;
  210.         }
  211.         return null !== $content Response::HTTP_OK $this->emptyContentCode;
  212.     }
  213.     /**
  214.      * If the given format uses the templating system for rendering.
  215.      *
  216.      * @param string $format
  217.      *
  218.      * @return bool
  219.      */
  220.     public function isFormatTemplating($format)
  221.     {
  222.         return !empty($this->formats[$format]);
  223.     }
  224.     /**
  225.      * Gets or creates a JMS\Serializer\SerializationContext and initializes it with
  226.      * the view exclusion strategies, groups & versions if a new context is created.
  227.      *
  228.      * @param View $view
  229.      *
  230.      * @return Context
  231.      */
  232.     protected function getSerializationContext(View $view)
  233.     {
  234.         $context $view->getContext();
  235.         $groups $context->getGroups();
  236.         if (empty($groups) && $this->exclusionStrategyGroups) {
  237.             $context->setGroups($this->exclusionStrategyGroups);
  238.         }
  239.         if (null === $context->getVersion() && $this->exclusionStrategyVersion) {
  240.             $context->setVersion($this->exclusionStrategyVersion);
  241.         }
  242.         if (null === $context->getSerializeNull() && null !== $this->serializeNullStrategy) {
  243.             $context->setSerializeNull($this->serializeNullStrategy);
  244.         }
  245.         return $context;
  246.     }
  247.     /**
  248.      * Handles a request with the proper handler.
  249.      *
  250.      * Decides on which handler to use based on the request format.
  251.      *
  252.      * @param View    $view
  253.      * @param Request $request
  254.      *
  255.      * @throws UnsupportedMediaTypeHttpException
  256.      *
  257.      * @return Response
  258.      */
  259.     public function handle(View $viewRequest $request null)
  260.     {
  261.         if (null === $request) {
  262.             $request $this->requestStack->getCurrentRequest();
  263.         }
  264.         $format $view->getFormat() ?: $request->getRequestFormat();
  265.         if (!$this->supports($format)) {
  266.             $msg "Format '$format' not supported, handler must be implemented";
  267.             throw new UnsupportedMediaTypeHttpException($msg);
  268.         }
  269.         if (isset($this->customHandlers[$format])) {
  270.             return call_user_func($this->customHandlers[$format], $this$view$request$format);
  271.         }
  272.         return $this->createResponse($view$request$format);
  273.     }
  274.     /**
  275.      * Creates the Response from the view.
  276.      *
  277.      * @param View   $view
  278.      * @param string $location
  279.      * @param string $format
  280.      *
  281.      * @return Response
  282.      */
  283.     public function createRedirectResponse(View $view$location$format)
  284.     {
  285.         $content null;
  286.         if ((Response::HTTP_CREATED === $view->getStatusCode() || Response::HTTP_ACCEPTED === $view->getStatusCode()) && null !== $view->getData()) {
  287.             $response $this->initResponse($view$format);
  288.         } else {
  289.             $response $view->getResponse();
  290.             if ('html' === $format && isset($this->forceRedirects[$format])) {
  291.                 $redirect = new RedirectResponse($location);
  292.                 $content $redirect->getContent();
  293.                 $response->setContent($content);
  294.             }
  295.         }
  296.         $code = isset($this->forceRedirects[$format])
  297.             ? $this->forceRedirects[$format] : $this->getStatusCode($view$content);
  298.         $response->setStatusCode($code);
  299.         $response->headers->set('Location'$location);
  300.         return $response;
  301.     }
  302.     /**
  303.      * Renders the view data with the given template.
  304.      *
  305.      * @param View   $view
  306.      * @param string $format
  307.      *
  308.      * @return string
  309.      */
  310.     public function renderTemplate(View $view$format)
  311.     {
  312.         if (null === $this->templating) {
  313.             throw new \LogicException(sprintf('An instance of %s must be injected in %s to render templates.'EngineInterface::class, __CLASS__));
  314.         }
  315.         $data $this->prepareTemplateParameters($view);
  316.         $template $view->getTemplate();
  317.         if ($template instanceof TemplateReferenceInterface) {
  318.             if (null === $template->get('format')) {
  319.                 $template->set('format'$format);
  320.             }
  321.             if (null === $template->get('engine')) {
  322.                 $engine $view->getEngine() ?: $this->defaultEngine;
  323.                 $template->set('engine'$engine);
  324.             }
  325.         }
  326.         return $this->templating->render($template$data);
  327.     }
  328.     /**
  329.      * Prepares view data for use by templating engine.
  330.      *
  331.      * @param View $view
  332.      *
  333.      * @return array
  334.      */
  335.     public function prepareTemplateParameters(View $view)
  336.     {
  337.         $data $view->getData();
  338.         if ($data instanceof FormInterface) {
  339.             $data = [$view->getTemplateVar() => $data->getData(), 'form' => $data];
  340.         } elseif (empty($data) || !is_array($data) || is_numeric((key($data)))) {
  341.             $data = [$view->getTemplateVar() => $data];
  342.         }
  343.         if (isset($data['form']) && $data['form'] instanceof FormInterface) {
  344.             $data['form'] = $data['form']->createView();
  345.         }
  346.         $templateData $view->getTemplateData();
  347.         if (is_callable($templateData)) {
  348.             $templateData call_user_func($templateData$this$view);
  349.         }
  350.         return array_merge($data$templateData);
  351.     }
  352.     /**
  353.      * Handles creation of a Response using either redirection or the templating/serializer service.
  354.      *
  355.      * @param View    $view
  356.      * @param Request $request
  357.      * @param string  $format
  358.      *
  359.      * @return Response
  360.      */
  361.     public function createResponse(View $viewRequest $request$format)
  362.     {
  363.         $route $view->getRoute();
  364.         $location $route
  365.             $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
  366.             : $view->getLocation();
  367.         if ($location) {
  368.             return $this->createRedirectResponse($view$location$format);
  369.         }
  370.         $response $this->initResponse($view$format);
  371.         if (!$response->headers->has('Content-Type')) {
  372.             $mimeType $request->attributes->get('media_type');
  373.             if (null === $mimeType) {
  374.                 $mimeType $request->getMimeType($format);
  375.             }
  376.             $response->headers->set('Content-Type'$mimeType);
  377.         }
  378.         return $response;
  379.     }
  380.     /**
  381.      * Initializes a response object that represents the view and holds the view's status code.
  382.      *
  383.      * @param View   $view
  384.      * @param string $format
  385.      *
  386.      * @return Response
  387.      */
  388.     private function initResponse(View $view$format)
  389.     {
  390.         $content null;
  391.         if ($this->isFormatTemplating($format)) {
  392.             $content $this->renderTemplate($view$format);
  393.         } elseif ($this->serializeNull || null !== $view->getData()) {
  394.             $data $this->getDataFromView($view);
  395.             if ($data instanceof FormInterface && $data->isSubmitted() && !$data->isValid()) {
  396.                 $view->getContext()->setAttribute('status_code'$this->failedValidationCode);
  397.             }
  398.             $context $this->getSerializationContext($view);
  399.             $context->setAttribute('template_data'$view->getTemplateData());
  400.             $content $this->serializer->serialize($data$format$context);
  401.         }
  402.         $response $view->getResponse();
  403.         $response->setStatusCode($this->getStatusCode($view$content));
  404.         if (null !== $content) {
  405.             $response->setContent($content);
  406.         }
  407.         return $response;
  408.     }
  409.     /**
  410.      * Returns the form from the given view if present, false otherwise.
  411.      *
  412.      * @param View $view
  413.      *
  414.      * @return bool|FormInterface
  415.      */
  416.     protected function getFormFromView(View $view)
  417.     {
  418.         $data $view->getData();
  419.         if ($data instanceof FormInterface) {
  420.             return $data;
  421.         }
  422.         if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
  423.             return $data['form'];
  424.         }
  425.         return false;
  426.     }
  427.     /**
  428.      * Returns the data from a view.
  429.      *
  430.      * @param View $view
  431.      *
  432.      * @return mixed|null
  433.      */
  434.     private function getDataFromView(View $view)
  435.     {
  436.         $form $this->getFormFromView($view);
  437.         if (false === $form) {
  438.             return $view->getData();
  439.         }
  440.         return $form;
  441.     }
  442.     /**
  443.      * Resets internal object state at the end of the request.
  444.      */
  445.     public function reset()
  446.     {
  447.         $this->exclusionStrategyGroups $this->options['exclusionStrategyGroups'];
  448.         $this->exclusionStrategyVersion $this->options['exclusionStrategyVersion'];
  449.         $this->serializeNullStrategy $this->options['serializeNullStrategy'];
  450.     }
  451. }