Completed
Push — master ( 6d3bf3...03ec6c )
by
unknown
09:00
created

ViewHandler::reset()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 2
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
    private $options;
106
107
    /**
108
     * Constructor.
109
     *
110
     * @param UrlGeneratorInterface $urlGenerator         The URL generator
111
     * @param Serializer            $serializer
112
     * @param EngineInterface       $templating           The configured templating engine
113
     * @param RequestStack          $requestStack         The request stack
114
     * @param array                 $formats              the supported formats as keys and if the given formats uses templating is denoted by a true value
115
     * @param int                   $failedValidationCode The HTTP response status code for a failed validation
116
     * @param int                   $emptyContentCode     HTTP response status code when the view data is null
117
     * @param bool                  $serializeNull        Whether or not to serialize null view data
118
     * @param array                 $forceRedirects       If to force a redirect for the given key format, with value being the status code to use
119 81
     * @param string                $defaultEngine        default engine (twig, php ..)
120
     * @param array                 $options              config options
121
     */
122
    public function __construct(
123
        UrlGeneratorInterface $urlGenerator,
124
        Serializer $serializer,
125
        EngineInterface $templating = null,
126
        RequestStack $requestStack,
127
        array $formats = null,
128
        $failedValidationCode = Response::HTTP_BAD_REQUEST,
129
        $emptyContentCode = Response::HTTP_NO_CONTENT,
130
        $serializeNull = false,
131 81
        array $forceRedirects = null,
132 81
        $defaultEngine = 'twig',
133 81
        array $options = []
134 81
    ) {
135 81
        $this->urlGenerator = $urlGenerator;
136 81
        $this->serializer = $serializer;
137 81
        $this->templating = $templating;
138 81
        $this->requestStack = $requestStack;
139 81
        $this->formats = (array) $formats;
140 81
        $this->failedValidationCode = $failedValidationCode;
141 81
        $this->emptyContentCode = $emptyContentCode;
142
        $this->serializeNull = $serializeNull;
143
        $this->forceRedirects = (array) $forceRedirects;
144
        $this->defaultEngine = $defaultEngine;
145
        $this->options = $options + [
146
            'exclusionStrategyGroups' => [],
147
            'exclusionStrategyVersion' => null,
148 1
            'serializeNullStrategy' => null,
149
            ];
150 1
        $this->reset();
151 1
    }
152
153
    /**
154
     * Sets the default serialization groups.
155
     *
156
     * @param array|string $groups
157
     */
158 7
    public function setExclusionStrategyGroups($groups)
159
    {
160 7
        $this->exclusionStrategyGroups = (array) $groups;
161 7
    }
162
163
    /**
164
     * Sets the default serialization version.
165
     *
166
     * @param string $version
167
     */
168 25
    public function setExclusionStrategyVersion($version)
169
    {
170 25
        $this->exclusionStrategyVersion = $version;
171 25
    }
172
173
    /**
174
     * If nulls should be serialized.
175
     *
176 44
     * @param bool $isEnabled
177
     */
178 44
    public function setSerializeNullStrategy($isEnabled)
179
    {
180
        $this->serializeNullStrategy = $isEnabled;
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186
    public function supports($format)
187
    {
188
        return isset($this->customHandlers[$format]) || isset($this->formats[$format]);
189
    }
190
191
    /**
192
     * Registers a custom handler.
193 16
     *
194
     * The handler must have the following signature: handler(ViewHandler $viewHandler, View $view, Request $request, $format)
195 16
     * It can use the public methods of this class to retrieve the needed data and return a
196 1
     * Response object ready to be sent.
197
     *
198
     * @param string   $format
199 15
     * @param callable $callable
200 15
     *
201
     * @throws \InvalidArgumentException
202
     */
203
    public function registerHandler($format, $callable)
204
    {
205
        if (!is_callable($callable)) {
206
            throw new \InvalidArgumentException('Registered view callback must be callable.');
207
        }
208
209
        $this->customHandlers[$format] = $callable;
210
    }
211
212
    /**
213
     * Gets a response HTTP status code from a View instance.
214 55
     *
215
     * By default it will return 200. However if there is a FormInterface stored for
216 55
     * the key 'form' in the View's data it will return the failed_validation
217
     * configuration if the form instance has errors.
218 55
     *
219 7
     * @param View  $view
220
     * @param mixed $content
221
     *
222 48
     * @return int HTTP status code
223 48
     */
224 15
    protected function getStatusCode(View $view, $content = null)
225
    {
226
        $form = $this->getFormFromView($view);
227 33
228
        if ($form && $form->isSubmitted() && !$form->isValid()) {
229
            return $this->failedValidationCode;
230
        }
231
232
        $statusCode = $view->getStatusCode();
233
        if (null !== $statusCode) {
234
            return $statusCode;
235
        }
236
237 46
        return null !== $content ? Response::HTTP_OK : $this->emptyContentCode;
238
    }
239 46
240
    /**
241
     * If the given format uses the templating system for rendering.
242
     *
243
     * @param string $format
244
     *
245
     * @return bool
246
     */
247
    public function isFormatTemplating($format)
248
    {
249
        return !empty($this->formats[$format]);
250 31
    }
251
252 31
    /**
253
     * Gets or creates a JMS\Serializer\SerializationContext and initializes it with
254 31
     * the view exclusion strategies, groups & versions if a new context is created.
255 31
     *
256 1
     * @param View $view
257 1
     *
258
     * @return Context
259 31
     */
260 5
    protected function getSerializationContext(View $view)
261 5
    {
262
        $context = $view->getContext();
263 31
264 18
        $groups = $context->getGroups();
265 18
        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...
266
            $context->setGroups($this->exclusionStrategyGroups);
267 31
        }
268
269
        if (null === $context->getVersion() && $this->exclusionStrategyVersion) {
270
            $context->setVersion($this->exclusionStrategyVersion);
271
        }
272
273
        if (null === $context->getSerializeNull() && null !== $this->serializeNullStrategy) {
274
            $context->setSerializeNull($this->serializeNullStrategy);
275
        }
276
277
        return $context;
278
    }
279
280
    /**
281
     * Handles a request with the proper handler.
282 40
     *
283
     * Decides on which handler to use based on the request format.
284 40
     *
285 12
     * @param View    $view
286 12
     * @param Request $request
287
     *
288 40
     * @throws UnsupportedMediaTypeHttpException
289
     *
290 40
     * @return Response
291 1
     */
292 1
    public function handle(View $view, Request $request = null)
293
    {
294
        if (null === $request) {
295 39
            $request = $this->requestStack->getCurrentRequest();
296 10
        }
297
298
        $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...
299 29
300
        if (!$this->supports($format)) {
301
            $msg = "Format '$format' not supported, handler must be implemented";
302
303
            throw new UnsupportedMediaTypeHttpException($msg);
304
        }
305
306
        if (isset($this->customHandlers[$format])) {
307
            return call_user_func($this->customHandlers[$format], $this, $view, $request, $format);
308
        }
309
310
        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...
311 8
    }
312
313 8
    /**
314 8
     * Creates the Response from the view.
315 1
     *
316 1
     * @param View   $view
317 7
     * @param string $location
318 7
     * @param string $format
319 2
     *
320 2
     * @return Response
321 2
     */
322 2
    public function createRedirectResponse(View $view, $location, $format)
323
    {
324
        $content = null;
325 8
        if ((Response::HTTP_CREATED === $view->getStatusCode() || Response::HTTP_ACCEPTED === $view->getStatusCode()) && null !== $view->getData()) {
326 8
            $response = $this->initResponse($view, $format);
327
        } else {
328 8
            $response = $view->getResponse();
329 8
            if ('html' === $format && isset($this->forceRedirects[$format])) {
330
                $redirect = new RedirectResponse($location);
331 8
                $content = $redirect->getContent();
332
                $response->setContent($content);
333
            }
334
        }
335
336
        $code = isset($this->forceRedirects[$format])
337
            ? $this->forceRedirects[$format] : $this->getStatusCode($view, $content);
338
339
        $response->setStatusCode($code);
340
        $response->headers->set('Location', $location);
341
342 15
        return $response;
343
    }
344 15
345
    /**
346 15
     * Renders the view data with the given template.
347 15
     *
348 2
     * @param View   $view
349
     * @param string $format
350
     *
351
     * @return string
352 2
     */
353
    public function renderTemplate(View $view, $format)
354
    {
355
        if (null === $this->templating) {
356 2
            throw new \LogicException(sprintf('An instance of %s must be injected in %s to render templates.', EngineInterface::class, __CLASS__));
357
        }
358 15
359
        $data = $this->prepareTemplateParameters($view);
360
361
        $template = $view->getTemplate();
362
        if ($template instanceof TemplateReferenceInterface) {
363
            if (null === $template->get('format')) {
364
                $template->set('format', $format);
365
            }
366
367
            if (null === $template->get('engine')) {
368 22
                $engine = $view->getEngine() ?: $this->defaultEngine;
369
                $template->set('engine', $engine);
370 22
            }
371
        }
372 22
373 2
        return $this->templating->render($template, $data);
0 ignored issues
show
Bug introduced by
It seems like $template defined by $view->getTemplate() on line 361 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...
374 22
    }
375 12
376 12
    /**
377
     * Prepares view data for use by templating engine.
378 22
     *
379 2
     * @param View $view
380 2
     *
381
     * @return array
382 22
     */
383 22
    public function prepareTemplateParameters(View $view)
384 2
    {
385 2
        $data = $view->getData();
386
387 22
        if ($data instanceof FormInterface) {
388
            $data = [$view->getTemplateVar() => $data->getData(), 'form' => $data];
389
        } elseif (empty($data) || !is_array($data) || is_numeric((key($data)))) {
390
            $data = [$view->getTemplateVar() => $data];
391
        }
392
393
        if (isset($data['form']) && $data['form'] instanceof FormInterface) {
394
            $data['form'] = $data['form']->createView();
395
        }
396
397
        $templateData = $view->getTemplateData();
398
        if (is_callable($templateData)) {
399 51
            $templateData = call_user_func($templateData, $this, $view);
400
        }
401 51
402
        return array_merge($data, $templateData);
403
    }
404 51
405 51
    /**
406
     * Handles creation of a Response using either redirection or the templating/serializer service.
407 51
     *
408 8
     * @param View    $view
409
     * @param Request $request
410
     * @param string  $format
411 43
     *
412
     * @return Response
413 43
     */
414 43
    public function createResponse(View $view, Request $request, $format)
415 43
    {
416
        $route = $view->getRoute();
417 43
418
        $location = $route
419
            ? $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
420
            : $view->getLocation();
421
422
        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...
423
            return $this->createRedirectResponse($view, $location, $format);
424
        }
425
426
        $response = $this->initResponse($view, $format);
427
428 44
        if (!$response->headers->has('Content-Type')) {
429
            $mimeType = $request->attributes->get('media_type');
430 44
            if (null === $mimeType) {
431 44
                $mimeType = $request->getMimeType($format);
432 15
            }
433 44
434 28
            $response->headers->set('Content-Type', $mimeType);
435
        }
436 28
437 6
        return $response;
438 6
    }
439
440 28
    /**
441 28
     * Initializes a response object that represents the view and holds the view's status code.
442
     *
443 28
     * @param View   $view
444 28
     * @param string $format
445
     *
446 44
     * @return Response
447 44
     */
448
    private function initResponse(View $view, $format)
449 44
    {
450 39
        $content = null;
451 39
        if ($this->isFormatTemplating($format)) {
452
            $content = $this->renderTemplate($view, $format);
453 44
        } elseif ($this->serializeNull || null !== $view->getData()) {
454
            $data = $this->getDataFromView($view);
455
456
            if ($data instanceof FormInterface && $data->isSubmitted() && !$data->isValid()) {
457
                $view->getContext()->setAttribute('status_code', $this->failedValidationCode);
458
            }
459
460
            $context = $this->getSerializationContext($view);
461
            $context->setAttribute('template_data', $view->getTemplateData());
462
463 55
            $content = $this->serializer->serialize($data, $format, $context);
464
        }
465 55
466
        $response = $view->getResponse();
467 55
        $response->setStatusCode($this->getStatusCode($view, $content));
468 7
469
        if (null !== $content) {
470
            $response->setContent($content);
471 48
        }
472 4
473
        return $response;
474
    }
475 44
476
    /**
477
     * Returns the form from the given view if present, false otherwise.
478
     *
479
     * @param View $view
480
     *
481
     * @return bool|FormInterface
482
     */
483
    protected function getFormFromView(View $view)
484
    {
485 28
        $data = $view->getData();
486
487 28
        if ($data instanceof FormInterface) {
488
            return $data;
489 28
        }
490 22
491
        if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
492
            return $data['form'];
493 6
        }
494
495
        return false;
496
    }
497
498
    /**
499
     * Returns the data from a view.
500
     *
501
     * @param View $view
502
     *
503
     * @return mixed|null
504
     */
505
    private function getDataFromView(View $view)
506
    {
507
        $form = $this->getFormFromView($view);
508
509
        if (false === $form) {
510
            return $view->getData();
511
        }
512
513
        return $form;
514
    }
515
516
    /**
517
     * Resets internal object state at the end of the request.
518
     */
519
    public function reset()
520
    {
521
        $this->exclusionStrategyGroups = $this->options['exclusionStrategyGroups'];
522
        $this->exclusionStrategyVersion = $this->options['exclusionStrategyVersion'];
523
        $this->serializeNullStrategy = $this->options['serializeNullStrategy'];
524
    }
525
}
526