Completed
Pull Request — master (#1358)
by Guilh
07:06
created

ViewHandler::handle()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 19
ccs 11
cts 11
cp 1
rs 8.8571
cc 5
eloc 10
nc 12
nop 2
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\Bundle\FrameworkBundle\Templating\TemplateReferenceInterface;
18
use Symfony\Component\Form\FormInterface;
19
use Symfony\Component\HttpFoundation\RedirectResponse;
20
use Symfony\Component\HttpFoundation\Request;
21
use Symfony\Component\HttpFoundation\RequestStack;
22
use Symfony\Component\HttpFoundation\Response;
23
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
24
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
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 74
    public function __construct(
120
        UrlGeneratorInterface $urlGenerator,
121
        Serializer $serializer,
122
        EngineInterface $templating,
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 74
        $this->urlGenerator = $urlGenerator;
132 74
        $this->serializer = $serializer;
133 74
        $this->templating = $templating;
134 74
        $this->requestStack = $requestStack;
135 74
        $this->formats = (array) $formats;
136 74
        $this->failedValidationCode = $failedValidationCode;
137 74
        $this->emptyContentCode = $emptyContentCode;
138 74
        $this->serializeNull = $serializeNull;
139 74
        $this->forceRedirects = (array) $forceRedirects;
140 74
        $this->defaultEngine = $defaultEngine;
141 74
    }
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 5
    public function setExclusionStrategyVersion($version)
159
    {
160 5
        $this->exclusionStrategyVersion = $version;
161 5
    }
162
163
    /**
164
     * If nulls should be serialized.
165
     *
166
     * @param bool $isEnabled
167
     */
168 18
    public function setSerializeNullStrategy($isEnabled)
169
    {
170 18
        $this->serializeNullStrategy = $isEnabled;
171 18
    }
172
173
    /**
174
     * {@inheritdoc}
175
     */
176 37
    public function supports($format)
177
    {
178 37
        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 48
    protected function getStatusCode(View $view, $content = null)
215
    {
216 48
        $form = $this->getFormFromView($view);
217
218 48
        if ($form && $form->isSubmitted() && !$form->isValid()) {
219 7
            return $this->failedValidationCode;
220
        }
221
222 41
        $statusCode = $view->getStatusCode();
223 41
        if (null !== $statusCode) {
224 10
            return $statusCode;
225
        }
226
227 31
        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 39
    public function isFormatTemplating($format)
238
    {
239 39
        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 27
    protected function getSerializationContext(View $view)
251
    {
252 27
        $context = $view->getContext();
253
254 27
        $groups = $context->getGroups();
255 27
        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 27
        if (null === $context->getVersion() && $this->exclusionStrategyVersion) {
260 4
            $context->setVersion($this->exclusionStrategyVersion);
261 4
        }
262
263 27
        if (null === $context->getSerializeNull() && null !== $this->serializeNullStrategy) {
264 14
            $context->setSerializeNull($this->serializeNullStrategy);
265 14
        }
266
267 27
        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 33
    public function handle(View $view, Request $request = null)
283
    {
284 33
        if (null === $request) {
285 7
            $request = $this->requestStack->getCurrentRequest();
286 7
        }
287
288 33
        $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 33
        if (!$this->supports($format)) {
291 1
            $msg = "Format '$format' not supported, handler must be implemented";
292 1
            throw new UnsupportedMediaTypeHttpException($msg);
293
        }
294
295 32
        if (isset($this->customHandlers[$format])) {
296 10
            return call_user_func($this->customHandlers[$format], $this, $view, $request, $format);
297
        }
298
299 22
        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 12
    public function renderTemplate(View $view, $format)
343
    {
344 12
        $data = $this->prepareTemplateParameters($view);
345
346 12
        $template = $view->getTemplate();
347 12
        if ($template instanceof TemplateReferenceInterface) {
0 ignored issues
show
Bug introduced by
The class Symfony\Bundle\Framework...plateReferenceInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
348
            if (null === $template->get('format')) {
349
                $template->set('format', $format);
350
            }
351
352
            if (null === $template->get('engine')) {
353
                $engine = $view->getEngine() ?: $this->defaultEngine;
354
                $template->set('engine', $engine);
355
            }
356
        }
357
358 12
        return $this->templating->render($template, $data);
0 ignored issues
show
Bug introduced by
It seems like $template defined by $view->getTemplate() on line 346 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...
359
    }
360
361
    /**
362
     * Prepares view data for use by templating engine.
363
     *
364
     * @param View $view
365
     *
366
     * @return array
367
     */
368 19
    public function prepareTemplateParameters(View $view)
369
    {
370 19
        $data = $view->getData();
371
372 19
        if ($data instanceof FormInterface) {
373 2
            $data = [$view->getTemplateVar() => $data->getData(), 'form' => $data];
374 19
        } elseif (empty($data) || !is_array($data) || is_numeric((key($data)))) {
375 10
            $data = [$view->getTemplateVar() => $data];
376 10
        }
377
378 19
        if (isset($data['form']) && $data['form'] instanceof FormInterface) {
379 2
            $data['form'] = $data['form']->createView();
380 2
        }
381
382 19
        $templateData = $view->getTemplateData();
383 19
        if (is_callable($templateData)) {
384 2
            $templateData = call_user_func($templateData, $this, $view);
385 2
        }
386
387 19
        return array_merge($data, $templateData);
388
    }
389
390
    /**
391
     * Handles creation of a Response using either redirection or the templating/serializer service.
392
     *
393
     * @param View    $view
394
     * @param Request $request
395
     * @param string  $format
396
     *
397
     * @return Response
398
     */
399 44
    public function createResponse(View $view, Request $request, $format)
400
    {
401 44
        $route = $view->getRoute();
402
403
        $location = $route
404 44
            ? $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
405 44
            : $view->getLocation();
406
407 44
        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...
408 8
            return $this->createRedirectResponse($view, $location, $format);
409
        }
410
411 36
        $response = $this->initResponse($view, $format);
412
413 36
        if (!$response->headers->has('Content-Type')) {
414 36
            $response->headers->set('Content-Type', $request->getMimeType($format));
415 36
        }
416
417 36
        return $response;
418
    }
419
420
    /**
421
     * Initializes a response object that represents the view and holds the view's status code.
422
     *
423
     * @param View   $view
424
     * @param string $format
425
     *
426
     * @return Response
427
     */
428 37
    private function initResponse(View $view, $format)
429
    {
430 37
        $content = null;
431 37
        if ($this->isFormatTemplating($format)) {
432 12
            $content = $this->renderTemplate($view, $format);
433 37
        } elseif ($this->serializeNull || null !== $view->getData()) {
434 24
            $data = $this->getDataFromView($view);
435
436 24
            if ($data instanceof FormInterface && $data->isSubmitted() && !$data->isValid()) {
437 6
                $view->getContext()->setAttribute('status_code', $this->failedValidationCode);
438 6
            }
439
440 24
            $context = $this->getSerializationContext($view);
441 24
            $context->setAttribute('template_data', $view->getTemplateData());
442
443 24
            $content = $this->serializer->serialize($data, $format, $context);
444 24
        }
445
446 37
        $response = $view->getResponse();
447 37
        $response->setStatusCode($this->getStatusCode($view, $content));
448
449 37
        if (null !== $content) {
450 32
            $response->setContent($content);
451 32
        }
452
453 37
        return $response;
454
    }
455
456
    /**
457
     * Returns the form from the given view if present, false otherwise.
458
     *
459
     * @param View $view
460
     *
461
     * @return bool|FormInterface
462
     */
463 48
    protected function getFormFromView(View $view)
464
    {
465 48
        $data = $view->getData();
466
467 48
        if ($data instanceof FormInterface) {
468 7
            return $data;
469
        }
470
471 41
        if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
472 4
            return $data['form'];
473
        }
474
475 37
        return false;
476
    }
477
478
    /**
479
     * Returns the data from a view.
480
     *
481
     * @param View $view
482
     *
483
     * @return mixed|null
484
     */
485 24
    private function getDataFromView(View $view)
486
    {
487 24
        $form = $this->getFormFromView($view);
488
489 24
        if (false === $form) {
490 18
            return $view->getData();
491
        }
492
493 6
        return $form;
494
    }
495
}
496