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

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       $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(
  122.                 'If provided, the templating engine must be an instance of %s or %s, but %s was given.',
  123.                 EngineInterface::class,
  124.                 Environment::class,
  125.                 get_class($templating)
  126.             ));
  127.         }
  128.         $this->urlGenerator $urlGenerator;
  129.         $this->serializer $serializer;
  130.         $this->templating $templating;
  131.         $this->requestStack $requestStack;
  132.         $this->formats = (array) $formats;
  133.         $this->failedValidationCode $failedValidationCode;
  134.         $this->emptyContentCode $emptyContentCode;
  135.         $this->serializeNull $serializeNull;
  136.         $this->forceRedirects = (array) $forceRedirects;
  137.         $this->defaultEngine $defaultEngine;
  138.         $this->options $options + [
  139.             'exclusionStrategyGroups' => [],
  140.             'exclusionStrategyVersion' => null,
  141.             'serializeNullStrategy' => null,
  142.             ];
  143.         $this->reset();
  144.     }
  145.     /**
  146.      * Sets the default serialization groups.
  147.      *
  148.      * @param array|string $groups
  149.      */
  150.     public function setExclusionStrategyGroups($groups)
  151.     {
  152.         $this->exclusionStrategyGroups = (array) $groups;
  153.     }
  154.     /**
  155.      * Sets the default serialization version.
  156.      *
  157.      * @param string $version
  158.      */
  159.     public function setExclusionStrategyVersion($version)
  160.     {
  161.         $this->exclusionStrategyVersion $version;
  162.     }
  163.     /**
  164.      * If nulls should be serialized.
  165.      *
  166.      * @param bool $isEnabled
  167.      */
  168.     public function setSerializeNullStrategy($isEnabled)
  169.     {
  170.         $this->serializeNullStrategy $isEnabled;
  171.     }
  172.     /**
  173.      * {@inheritdoc}
  174.      */
  175.     public function supports($format)
  176.     {
  177.         return isset($this->customHandlers[$format]) || isset($this->formats[$format]);
  178.     }
  179.     /**
  180.      * Registers a custom handler.
  181.      *
  182.      * The handler must have the following signature: handler(ViewHandler $viewHandler, View $view, Request $request, $format)
  183.      * It can use the public methods of this class to retrieve the needed data and return a
  184.      * Response object ready to be sent.
  185.      *
  186.      * @param string   $format
  187.      * @param callable $callable
  188.      *
  189.      * @throws \InvalidArgumentException
  190.      */
  191.     public function registerHandler($format$callable)
  192.     {
  193.         if (!is_callable($callable)) {
  194.             throw new \InvalidArgumentException('Registered view callback must be callable.');
  195.         }
  196.         $this->customHandlers[$format] = $callable;
  197.     }
  198.     /**
  199.      * Gets a response HTTP status code from a View instance.
  200.      *
  201.      * By default it will return 200. However if there is a FormInterface stored for
  202.      * the key 'form' in the View's data it will return the failed_validation
  203.      * configuration if the form instance has errors.
  204.      *
  205.      * @param View  $view
  206.      * @param mixed $content
  207.      *
  208.      * @return int HTTP status code
  209.      */
  210.     protected function getStatusCode(View $view$content null)
  211.     {
  212.         $form $this->getFormFromView($view);
  213.         if ($form && $form->isSubmitted() && !$form->isValid()) {
  214.             return $this->failedValidationCode;
  215.         }
  216.         $statusCode $view->getStatusCode();
  217.         if (null !== $statusCode) {
  218.             return $statusCode;
  219.         }
  220.         return null !== $content Response::HTTP_OK $this->emptyContentCode;
  221.     }
  222.     /**
  223.      * If the given format uses the templating system for rendering.
  224.      *
  225.      * @param string $format
  226.      *
  227.      * @return bool
  228.      */
  229.     public function isFormatTemplating($format)
  230.     {
  231.         return !empty($this->formats[$format]);
  232.     }
  233.     /**
  234.      * Gets or creates a JMS\Serializer\SerializationContext and initializes it with
  235.      * the view exclusion strategies, groups & versions if a new context is created.
  236.      *
  237.      * @param View $view
  238.      *
  239.      * @return Context
  240.      */
  241.     protected function getSerializationContext(View $view)
  242.     {
  243.         $context $view->getContext();
  244.         $groups $context->getGroups();
  245.         if (empty($groups) && $this->exclusionStrategyGroups) {
  246.             $context->setGroups($this->exclusionStrategyGroups);
  247.         }
  248.         if (null === $context->getVersion() && $this->exclusionStrategyVersion) {
  249.             $context->setVersion($this->exclusionStrategyVersion);
  250.         }
  251.         if (null === $context->getSerializeNull() && null !== $this->serializeNullStrategy) {
  252.             $context->setSerializeNull($this->serializeNullStrategy);
  253.         }
  254.         return $context;
  255.     }
  256.     /**
  257.      * Handles a request with the proper handler.
  258.      *
  259.      * Decides on which handler to use based on the request format.
  260.      *
  261.      * @param View    $view
  262.      * @param Request $request
  263.      *
  264.      * @throws UnsupportedMediaTypeHttpException
  265.      *
  266.      * @return Response
  267.      */
  268.     public function handle(View $viewRequest $request null)
  269.     {
  270.         if (null === $request) {
  271.             $request $this->requestStack->getCurrentRequest();
  272.         }
  273.         $format $view->getFormat() ?: $request->getRequestFormat();
  274.         if (!$this->supports($format)) {
  275.             $msg "Format '$format' not supported, handler must be implemented";
  276.             throw new UnsupportedMediaTypeHttpException($msg);
  277.         }
  278.         if (isset($this->customHandlers[$format])) {
  279.             return call_user_func($this->customHandlers[$format], $this$view$request$format);
  280.         }
  281.         return $this->createResponse($view$request$format);
  282.     }
  283.     /**
  284.      * Creates the Response from the view.
  285.      *
  286.      * @param View   $view
  287.      * @param string $location
  288.      * @param string $format
  289.      *
  290.      * @return Response
  291.      */
  292.     public function createRedirectResponse(View $view$location$format)
  293.     {
  294.         $content null;
  295.         if ((Response::HTTP_CREATED === $view->getStatusCode() || Response::HTTP_ACCEPTED === $view->getStatusCode()) && null !== $view->getData()) {
  296.             $response $this->initResponse($view$format);
  297.         } else {
  298.             $response $view->getResponse();
  299.             if ('html' === $format && isset($this->forceRedirects[$format])) {
  300.                 $redirect = new RedirectResponse($location);
  301.                 $content $redirect->getContent();
  302.                 $response->setContent($content);
  303.             }
  304.         }
  305.         $code = isset($this->forceRedirects[$format])
  306.             ? $this->forceRedirects[$format] : $this->getStatusCode($view$content);
  307.         $response->setStatusCode($code);
  308.         $response->headers->set('Location'$location);
  309.         return $response;
  310.     }
  311.     /**
  312.      * Renders the view data with the given template.
  313.      *
  314.      * @param View   $view
  315.      * @param string $format
  316.      *
  317.      * @return string
  318.      */
  319.     public function renderTemplate(View $view$format)
  320.     {
  321.         if (null === $this->templating) {
  322.             throw new \LogicException(sprintf('An instance of %s must be injected in %s to render templates.'EngineInterface::class, __CLASS__));
  323.         }
  324.         $data $this->prepareTemplateParameters($view);
  325.         $template $view->getTemplate();
  326.         if ($template instanceof TemplateReferenceInterface) {
  327.             if (null === $template->get('format')) {
  328.                 $template->set('format'$format);
  329.             }
  330.             if (null === $template->get('engine')) {
  331.                 $engine $view->getEngine() ?: $this->defaultEngine;
  332.                 $template->set('engine'$engine);
  333.             }
  334.         }
  335.         return $this->templating->render($template$data);
  336.     }
  337.     /**
  338.      * Prepares view data for use by templating engine.
  339.      *
  340.      * @param View $view
  341.      *
  342.      * @return array
  343.      */
  344.     public function prepareTemplateParameters(View $view)
  345.     {
  346.         $data $view->getData();
  347.         if ($data instanceof FormInterface) {
  348.             $data = [$view->getTemplateVar() => $data->getData(), 'form' => $data];
  349.         } elseif (empty($data) || !is_array($data) || is_numeric((key($data)))) {
  350.             $data = [$view->getTemplateVar() => $data];
  351.         }
  352.         if (isset($data['form']) && $data['form'] instanceof FormInterface) {
  353.             $data['form'] = $data['form']->createView();
  354.         }
  355.         $templateData $view->getTemplateData();
  356.         if (is_callable($templateData)) {
  357.             $templateData call_user_func($templateData$this$view);
  358.         }
  359.         return array_merge($data$templateData);
  360.     }
  361.     /**
  362.      * Handles creation of a Response using either redirection or the templating/serializer service.
  363.      *
  364.      * @param View    $view
  365.      * @param Request $request
  366.      * @param string  $format
  367.      *
  368.      * @return Response
  369.      */
  370.     public function createResponse(View $viewRequest $request$format)
  371.     {
  372.         $route $view->getRoute();
  373.         $location $route
  374.             $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
  375.             : $view->getLocation();
  376.         if ($location) {
  377.             return $this->createRedirectResponse($view$location$format);
  378.         }
  379.         $response $this->initResponse($view$format);
  380.         if (!$response->headers->has('Content-Type')) {
  381.             $mimeType $request->attributes->get('media_type');
  382.             if (null === $mimeType) {
  383.                 $mimeType $request->getMimeType($format);
  384.             }
  385.             $response->headers->set('Content-Type'$mimeType);
  386.         }
  387.         return $response;
  388.     }
  389.     /**
  390.      * Initializes a response object that represents the view and holds the view's status code.
  391.      *
  392.      * @param View   $view
  393.      * @param string $format
  394.      *
  395.      * @return Response
  396.      */
  397.     private function initResponse(View $view$format)
  398.     {
  399.         $content null;
  400.         if ($this->isFormatTemplating($format)) {
  401.             $content $this->renderTemplate($view$format);
  402.         } elseif ($this->serializeNull || null !== $view->getData()) {
  403.             $data $this->getDataFromView($view);
  404.             if ($data instanceof FormInterface && $data->isSubmitted() && !$data->isValid()) {
  405.                 $view->getContext()->setAttribute('status_code'$this->failedValidationCode);
  406.             }
  407.             $context $this->getSerializationContext($view);
  408.             $context->setAttribute('template_data'$view->getTemplateData());
  409.             $content $this->serializer->serialize($data$format$context);
  410.         }
  411.         $response $view->getResponse();
  412.         $response->setStatusCode($this->getStatusCode($view$content));
  413.         if (null !== $content) {
  414.             $response->setContent($content);
  415.         }
  416.         return $response;
  417.     }
  418.     /**
  419.      * Returns the form from the given view if present, false otherwise.
  420.      *
  421.      * @param View $view
  422.      *
  423.      * @return bool|FormInterface
  424.      */
  425.     protected function getFormFromView(View $view)
  426.     {
  427.         $data $view->getData();
  428.         if ($data instanceof FormInterface) {
  429.             return $data;
  430.         }
  431.         if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
  432.             return $data['form'];
  433.         }
  434.         return false;
  435.     }
  436.     /**
  437.      * Returns the data from a view.
  438.      *
  439.      * @param View $view
  440.      *
  441.      * @return mixed|null
  442.      */
  443.     private function getDataFromView(View $view)
  444.     {
  445.         $form $this->getFormFromView($view);
  446.         if (false === $form) {
  447.             return $view->getData();
  448.         }
  449.         return $form;
  450.     }
  451.     /**
  452.      * Resets internal object state at the end of the request.
  453.      */
  454.     public function reset()
  455.     {
  456.         $this->exclusionStrategyGroups $this->options['exclusionStrategyGroups'];
  457.         $this->exclusionStrategyVersion $this->options['exclusionStrategyVersion'];
  458.         $this->serializeNullStrategy $this->options['serializeNullStrategy'];
  459.     }
  460. }