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

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