Completed
Push — master ( 6ecf0e...66b66d )
by Guilh
08:02
created

ViewHandler::createResponse()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 9
cts 9
cp 1
rs 8.439
c 0
b 0
f 0
cc 5
eloc 14
nc 8
nop 3
crap 5
1
<?php
2
3
/*
4
 * This file is part of the FOSRestBundle package.
5
 *
6
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace FOS\RestBundle\View;
13
14
use FOS\RestBundle\Context\Context;
15
use FOS\RestBundle\Serializer\Serializer;
16
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
17
use Symfony\Component\Form\FormInterface;
18
use Symfony\Component\HttpFoundation\RedirectResponse;
19
use Symfony\Component\HttpFoundation\Request;
20
use Symfony\Component\HttpFoundation\RequestStack;
21
use Symfony\Component\HttpFoundation\Response;
22
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
23
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
24
use Symfony\Component\Templating\TemplateReferenceInterface;
25
26
/**
27
 * View may be used in controllers to build up a response in a format agnostic way
28
 * The View class takes care of encoding your data in json, xml, or renders a
29
 * template for html via the Serializer component.
30
 *
31
 * @author Jordi Boggiano <[email protected]>
32
 * @author Lukas K. Smith <[email protected]>
33
 */
34
class ViewHandler implements ConfigurableViewHandlerInterface
35
{
36
    /**
37
     * Key format, value a callable that returns a Response instance.
38
     *
39
     * @var array
40
     */
41
    protected $customHandlers = [];
42
43
    /**
44
     * The supported formats as keys and if the given formats
45
     * uses templating is denoted by a true value.
46
     *
47
     * @var array
48
     */
49
    protected $formats;
50
51
    /**
52
     *  HTTP response status code for a failed validation.
53
     *
54
     * @var int
55
     */
56
    protected $failedValidationCode;
57
58
    /**
59
     * HTTP response status code when the view data is null.
60
     *
61
     * @var int
62
     */
63
    protected $emptyContentCode;
64
65
    /**
66
     * Whether or not to serialize null view data.
67
     *
68
     * @var bool
69
     */
70
    protected $serializeNull;
71
72
    /**
73
     * If to force a redirect for the given key format,
74
     * with value being the status code to use.
75
     *
76
     * @var array
77
     */
78
    protected $forceRedirects;
79
80
    /**
81
     * @var string
82
     */
83
    protected $defaultEngine;
84
85
    /**
86
     * @var array
87
     */
88
    protected $exclusionStrategyGroups = [];
89
90
    /**
91
     * @var string
92
     */
93
    protected $exclusionStrategyVersion;
94
95
    /**
96
     * @var bool
97
     */
98
    protected $serializeNullStrategy;
99
100
    private $urlGenerator;
101
    private $serializer;
102
    private $templating;
103
    private $requestStack;
104
105
    /**
106
     * Constructor.
107
     *
108
     * @param UrlGeneratorInterface $urlGenerator         The URL generator
109
     * @param Serializer            $serializer
110
     * @param EngineInterface       $templating           The configured templating engine
111
     * @param RequestStack          $requestStack         The request stack
112
     * @param array                 $formats              the supported formats as keys and if the given formats uses templating is denoted by a true value
113
     * @param int                   $failedValidationCode The HTTP response status code for a failed validation
114
     * @param int                   $emptyContentCode     HTTP response status code when the view data is null
115
     * @param bool                  $serializeNull        Whether or not to serialize null view data
116
     * @param array                 $forceRedirects       If to force a redirect for the given key format, with value being the status code to use
117
     * @param string                $defaultEngine        default engine (twig, php ..)
118
     */
119 81
    public function __construct(
120
        UrlGeneratorInterface $urlGenerator,
121
        Serializer $serializer,
122
        EngineInterface $templating = null,
123
        RequestStack $requestStack,
124
        array $formats = null,
125
        $failedValidationCode = Response::HTTP_BAD_REQUEST,
126
        $emptyContentCode = Response::HTTP_NO_CONTENT,
127
        $serializeNull = false,
128
        array $forceRedirects = null,
129
        $defaultEngine = 'twig'
130
    ) {
131 81
        $this->urlGenerator = $urlGenerator;
132 81
        $this->serializer = $serializer;
133 81
        $this->templating = $templating;
134 81
        $this->requestStack = $requestStack;
135 81
        $this->formats = (array) $formats;
136 81
        $this->failedValidationCode = $failedValidationCode;
137 81
        $this->emptyContentCode = $emptyContentCode;
138 81
        $this->serializeNull = $serializeNull;
139 81
        $this->forceRedirects = (array) $forceRedirects;
140 81
        $this->defaultEngine = $defaultEngine;
141 81
    }
142
143
    /**
144
     * Sets the default serialization groups.
145
     *
146
     * @param array|string $groups
147
     */
148 1
    public function setExclusionStrategyGroups($groups)
149
    {
150 1
        $this->exclusionStrategyGroups = (array) $groups;
151 1
    }
152
153
    /**
154
     * Sets the default serialization version.
155
     *
156
     * @param string $version
157
     */
158 7
    public function setExclusionStrategyVersion($version)
159
    {
160 7
        $this->exclusionStrategyVersion = $version;
161 7
    }
162
163
    /**
164
     * If nulls should be serialized.
165
     *
166
     * @param bool $isEnabled
167
     */
168 25
    public function setSerializeNullStrategy($isEnabled)
169
    {
170 25
        $this->serializeNullStrategy = $isEnabled;
171 25
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176 44
    public function supports($format)
177
    {
178 44
        return isset($this->customHandlers[$format]) || isset($this->formats[$format]);
179
    }
180
181
    /**
182
     * Registers a custom handler.
183
     *
184
     * The handler must have the following signature: handler(ViewHandler $viewHandler, View $view, Request $request, $format)
185
     * It can use the public methods of this class to retrieve the needed data and return a
186
     * Response object ready to be sent.
187
     *
188
     * @param string   $format
189
     * @param callable $callable
190
     *
191
     * @throws \InvalidArgumentException
192
     */
193 16
    public function registerHandler($format, $callable)
194
    {
195 16
        if (!is_callable($callable)) {
196 1
            throw new \InvalidArgumentException('Registered view callback must be callable.');
197
        }
198
199 15
        $this->customHandlers[$format] = $callable;
200 15
    }
201
202
    /**
203
     * Gets a response HTTP status code from a View instance.
204
     *
205
     * By default it will return 200. However if there is a FormInterface stored for
206
     * the key 'form' in the View's data it will return the failed_validation
207
     * configuration if the form instance has errors.
208
     *
209
     * @param View  $view
210
     * @param mixed $content
211
     *
212
     * @return int HTTP status code
213
     */
214 55
    protected function getStatusCode(View $view, $content = null)
215
    {
216 55
        $form = $this->getFormFromView($view);
217
218 55
        if ($form && $form->isSubmitted() && !$form->isValid()) {
219 7
            return $this->failedValidationCode;
220
        }
221
222 48
        $statusCode = $view->getStatusCode();
223 48
        if (null !== $statusCode) {
224 15
            return $statusCode;
225
        }
226
227 33
        return null !== $content ? Response::HTTP_OK : $this->emptyContentCode;
228
    }
229
230
    /**
231
     * If the given format uses the templating system for rendering.
232
     *
233
     * @param string $format
234
     *
235
     * @return bool
236
     */
237 46
    public function isFormatTemplating($format)
238
    {
239 46
        return !empty($this->formats[$format]);
240
    }
241
242
    /**
243
     * Gets or creates a JMS\Serializer\SerializationContext and initializes it with
244
     * the view exclusion strategies, groups & versions if a new context is created.
245
     *
246
     * @param View $view
247
     *
248
     * @return Context
249
     */
250 31
    protected function getSerializationContext(View $view)
251
    {
252 31
        $context = $view->getContext();
253
254 31
        $groups = $context->getGroups();
255 31
        if (empty($groups) && $this->exclusionStrategyGroups) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->exclusionStrategyGroups of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
256 1
            $context->addGroups($this->exclusionStrategyGroups);
257 1
        }
258
259 31
        if (null === $context->getVersion() && $this->exclusionStrategyVersion) {
260 5
            $context->setVersion($this->exclusionStrategyVersion);
261 5
        }
262
263 31
        if (null === $context->getSerializeNull() && null !== $this->serializeNullStrategy) {
264 18
            $context->setSerializeNull($this->serializeNullStrategy);
265 18
        }
266
267 31
        return $context;
268
    }
269
270
    /**
271
     * Handles a request with the proper handler.
272
     *
273
     * Decides on which handler to use based on the request format.
274
     *
275
     * @param View    $view
276
     * @param Request $request
277
     *
278
     * @throws UnsupportedMediaTypeHttpException
279
     *
280
     * @return Response
281
     */
282 40
    public function handle(View $view, Request $request = null)
283
    {
284 40
        if (null === $request) {
285 12
            $request = $this->requestStack->getCurrentRequest();
286 12
        }
287
288 40
        $format = $view->getFormat() ?: $request->getRequestFormat();
0 ignored issues
show
Bug introduced by
It seems like $request is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
289
290 40
        if (!$this->supports($format)) {
291 1
            $msg = "Format '$format' not supported, handler must be implemented";
292 1
            throw new UnsupportedMediaTypeHttpException($msg);
293
        }
294
295 39
        if (isset($this->customHandlers[$format])) {
296 10
            return call_user_func($this->customHandlers[$format], $this, $view, $request, $format);
297
        }
298
299 29
        return $this->createResponse($view, $request, $format);
0 ignored issues
show
Bug introduced by
It seems like $request can be null; however, createResponse() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
300
    }
301
302
    /**
303
     * Creates the Response from the view.
304
     *
305
     * @param View   $view
306
     * @param string $location
307
     * @param string $format
308
     *
309
     * @return Response
310
     */
311 8
    public function createRedirectResponse(View $view, $location, $format)
312
    {
313 8
        $content = null;
314 8
        if (($view->getStatusCode() === Response::HTTP_CREATED || $view->getStatusCode() === Response::HTTP_ACCEPTED) && $view->getData() !== null) {
315 1
            $response = $this->initResponse($view, $format);
316 1
        } else {
317 7
            $response = $view->getResponse();
318 7
            if ('html' === $format && isset($this->forceRedirects[$format])) {
319 2
                $redirect = new RedirectResponse($location);
320 2
                $content = $redirect->getContent();
321 2
                $response->setContent($content);
322 2
            }
323
        }
324
325 8
        $code = isset($this->forceRedirects[$format])
326 8
            ? $this->forceRedirects[$format] : $this->getStatusCode($view, $content);
327
328 8
        $response->setStatusCode($code);
329 8
        $response->headers->set('Location', $location);
330
331 8
        return $response;
332
    }
333
334
    /**
335
     * Renders the view data with the given template.
336
     *
337
     * @param View   $view
338
     * @param string $format
339
     *
340
     * @return string
341
     */
342 15
    public function renderTemplate(View $view, $format)
343
    {
344 15
        if (null === $this->templating) {
345
            throw new \LogicException(sprintf('An instance of %s must be injected in %s to render templates.', EngineInterface::class, __CLASS__));
346 15
        }
347 15
348 2
        $data = $this->prepareTemplateParameters($view);
349
350
        $template = $view->getTemplate();
351
        if ($template instanceof TemplateReferenceInterface) {
352 2
            if (null === $template->get('format')) {
353
                $template->set('format', $format);
354
            }
355
356 2
            if (null === $template->get('engine')) {
357
                $engine = $view->getEngine() ?: $this->defaultEngine;
358 15
                $template->set('engine', $engine);
359
            }
360
        }
361
362
        return $this->templating->render($template, $data);
0 ignored issues
show
Bug introduced by
It seems like $template defined by $view->getTemplate() on line 350 can also be of type null; however, Symfony\Component\Templa...gineInterface::render() does only seem to accept string|object<Symfony\Co...lateReferenceInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
363
    }
364
365
    /**
366
     * Prepares view data for use by templating engine.
367
     *
368 22
     * @param View $view
369
     *
370 22
     * @return array
371
     */
372 22
    public function prepareTemplateParameters(View $view)
373 2
    {
374 22
        $data = $view->getData();
375 12
376 12
        if ($data instanceof FormInterface) {
377
            $data = [$view->getTemplateVar() => $data->getData(), 'form' => $data];
378 22
        } elseif (empty($data) || !is_array($data) || is_numeric((key($data)))) {
379 2
            $data = [$view->getTemplateVar() => $data];
380 2
        }
381
382 22
        if (isset($data['form']) && $data['form'] instanceof FormInterface) {
383 22
            $data['form'] = $data['form']->createView();
384 2
        }
385 2
386
        $templateData = $view->getTemplateData();
387 22
        if (is_callable($templateData)) {
388
            $templateData = call_user_func($templateData, $this, $view);
389
        }
390
391
        return array_merge($data, $templateData);
392
    }
393
394
    /**
395
     * Handles creation of a Response using either redirection or the templating/serializer service.
396
     *
397
     * @param View    $view
398
     * @param Request $request
399 51
     * @param string  $format
400
     *
401 51
     * @return Response
402
     */
403
    public function createResponse(View $view, Request $request, $format)
404 51
    {
405 51
        $route = $view->getRoute();
406
407 51
        $location = $route
408 8
            ? $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
409
            : $view->getLocation();
410
411 43
        if ($location) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $location of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
412
            return $this->createRedirectResponse($view, $location, $format);
413 43
        }
414 43
415 43
        $response = $this->initResponse($view, $format);
416
417 43
        if (!$response->headers->has('Content-Type')) {
418
            $mimeType = $request->attributes->get('media_type');
419
            if (null === $mimeType) {
420
                $mimeType = $request->getMimeType($format);
421
            }
422
423
            $response->headers->set('Content-Type', $mimeType);
424
        }
425
426
        return $response;
427
    }
428 44
429
    /**
430 44
     * Initializes a response object that represents the view and holds the view's status code.
431 44
     *
432 15
     * @param View   $view
433 44
     * @param string $format
434 28
     *
435
     * @return Response
436 28
     */
437 6
    private function initResponse(View $view, $format)
438 6
    {
439
        $content = null;
440 28
        if ($this->isFormatTemplating($format)) {
441 28
            $content = $this->renderTemplate($view, $format);
442
        } elseif ($this->serializeNull || null !== $view->getData()) {
443 28
            $data = $this->getDataFromView($view);
444 28
445
            if ($data instanceof FormInterface && $data->isSubmitted() && !$data->isValid()) {
446 44
                $view->getContext()->setAttribute('status_code', $this->failedValidationCode);
447 44
            }
448
449 44
            $context = $this->getSerializationContext($view);
450 39
            $context->setAttribute('template_data', $view->getTemplateData());
451 39
452
            $content = $this->serializer->serialize($data, $format, $context);
453 44
        }
454
455
        $response = $view->getResponse();
456
        $response->setStatusCode($this->getStatusCode($view, $content));
457
458
        if (null !== $content) {
459
            $response->setContent($content);
460
        }
461
462
        return $response;
463 55
    }
464
465 55
    /**
466
     * Returns the form from the given view if present, false otherwise.
467 55
     *
468 7
     * @param View $view
469
     *
470
     * @return bool|FormInterface
471 48
     */
472 4
    protected function getFormFromView(View $view)
473
    {
474
        $data = $view->getData();
475 44
476
        if ($data instanceof FormInterface) {
477
            return $data;
478
        }
479
480
        if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
481
            return $data['form'];
482
        }
483
484
        return false;
485 28
    }
486
487 28
    /**
488
     * Returns the data from a view.
489 28
     *
490 22
     * @param View $view
491
     *
492
     * @return mixed|null
493 6
     */
494
    private function getDataFromView(View $view)
495
    {
496
        $form = $this->getFormFromView($view);
497
498
        if (false === $form) {
499
            return $view->getData();
500
        }
501
502
        return $form;
503
    }
504
}
505