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

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