Completed
Push — master ( 21573f...0b6989 )
by Lukas Kahwe
06:29
created

ViewHandler::prepareTemplateParameters()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 8

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 21
ccs 15
cts 15
cp 1
rs 7.1429
cc 8
eloc 12
nc 12
nop 1
crap 8
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\Adapter\SerializationContextAdapterInterface;
15
use FOS\RestBundle\Context\Adapter\SerializerAwareInterface;
16
use FOS\RestBundle\Context\ContextInterface;
17
use FOS\RestBundle\Context\GroupableContextInterface;
18
use FOS\RestBundle\Context\SerializeNullContextInterface;
19
use FOS\RestBundle\Context\VersionableContextInterface;
20
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
21
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
22
use Symfony\Component\Form\FormInterface;
23
use Symfony\Component\HttpFoundation\RedirectResponse;
24
use Symfony\Component\HttpFoundation\Request;
25
use Symfony\Component\HttpFoundation\RequestStack;
26
use Symfony\Component\HttpFoundation\Response;
27
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
28
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
29
30
/**
31
 * View may be used in controllers to build up a response in a format agnostic way
32
 * The View class takes care of encoding your data in json, xml, or renders a
33
 * template for html via the Serializer component.
34
 *
35
 * @author Jordi Boggiano <[email protected]>
36
 * @author Lukas K. Smith <[email protected]>
37
 */
38
class ViewHandler implements ConfigurableViewHandlerInterface
39
{
40
    /**
41
     * Key format, value a callable that returns a Response instance.
42
     *
43
     * @var array
44
     */
45
    protected $customHandlers = [];
46
47
    /**
48
     * The supported formats as keys and if the given formats
49
     * uses templating is denoted by a true value.
50
     *
51
     * @var array
52
     */
53
    protected $formats;
54
55
    /**
56
     *  HTTP response status code for a failed validation.
57
     *
58
     * @var int
59
     */
60
    protected $failedValidationCode;
61
62
    /**
63
     * HTTP response status code when the view data is null.
64
     *
65
     * @var int
66
     */
67
    protected $emptyContentCode;
68
69
    /**
70
     * Whether or not to serialize null view data.
71
     *
72
     * @var bool
73
     */
74
    protected $serializeNull;
75
76
    /**
77
     * If to force a redirect for the given key format,
78
     * with value being the status code to use.
79
     *
80
     * @var array
81
     */
82
    protected $forceRedirects;
83
84
    /**
85
     * @var string
86
     */
87
    protected $defaultEngine;
88
89
    /**
90
     * @var array
91
     */
92
    protected $exclusionStrategyGroups = [];
93
94
    /**
95
     * @var string
96
     */
97
    protected $exclusionStrategyVersion;
98
99
    /**
100
     * @var bool
101
     */
102
    protected $serializeNullStrategy;
103
104
    /**
105
     * @var SerializationContextAdapterInterface
106
     */
107
    protected $contextAdapter;
108
109
    private $urlGenerator;
110
    private $serializer;
111
    private $templating;
112
    private $requestStack;
113
    private $exceptionWrapperHandler;
114
115
    /**
116
     * Constructor.
117
     *
118
     * @param UrlGeneratorInterface            $urlGenerator            The URL generator
119
     * @param object                           $serializer              An object implementing a serialize() method
120
     * @param EngineInterface                  $templating              The configured templating engine
121
     * @param RequestStack                     $requestStack            The request stack
122
     * @param ExceptionWrapperHandlerInterface $exceptionWrapperHandler An exception wrapper handler
123
     * @param array                            $formats                 the supported formats as keys and if the given formats uses templating is denoted by a true value
124
     * @param int                              $failedValidationCode    The HTTP response status code for a failed validation
125
     * @param int                              $emptyContentCode        HTTP response status code when the view data is null
126
     * @param bool                             $serializeNull           Whether or not to serialize null view data
127
     * @param array                            $forceRedirects          If to force a redirect for the given key format, with value being the status code to use
128
     * @param string                           $defaultEngine           default engine (twig, php ..)
129
     */
130 73
    public function __construct(
131
        UrlGeneratorInterface $urlGenerator,
132
        $serializer,
133
        EngineInterface $templating,
134
        RequestStack $requestStack,
135
        ExceptionWrapperHandlerInterface $exceptionWrapperHandler,
136
        array $formats = null,
137
        $failedValidationCode = Response::HTTP_BAD_REQUEST,
138
        $emptyContentCode = Response::HTTP_NO_CONTENT,
139
        $serializeNull = false,
140
        array $forceRedirects = null,
141
        $defaultEngine = 'twig'
142
    ) {
143 73
        if (!method_exists($serializer, 'serialize')) {
144
            throw new \InvalidArgumentException('The $serializer argument must implement a serialize() method.');
145
        }
146
147 73
        $this->urlGenerator = $urlGenerator;
148 73
        $this->serializer = $serializer;
149 73
        $this->templating = $templating;
150 73
        $this->requestStack = $requestStack;
151 73
        $this->exceptionWrapperHandler = $exceptionWrapperHandler;
152 73
        $this->formats = (array) $formats;
153 73
        $this->failedValidationCode = $failedValidationCode;
154 73
        $this->emptyContentCode = $emptyContentCode;
155 73
        $this->serializeNull = $serializeNull;
156 73
        $this->forceRedirects = (array) $forceRedirects;
157 73
        $this->defaultEngine = $defaultEngine;
158 73
    }
159
160
    /**
161
     * Sets the default serialization groups.
162
     *
163
     * @param array|string $groups
164
     */
165 1
    public function setExclusionStrategyGroups($groups)
166
    {
167 1
        $this->exclusionStrategyGroups = $groups;
0 ignored issues
show
Documentation Bug introduced by
It seems like $groups can also be of type string. However, the property $exclusionStrategyGroups is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
168 1
    }
169
170
    /**
171
     * Sets the default serialization version.
172
     *
173
     * @param string $version
174
     */
175 5
    public function setExclusionStrategyVersion($version)
176
    {
177 5
        $this->exclusionStrategyVersion = $version;
178 5
    }
179
180
    /**
181
     * If nulls should be serialized.
182
     *
183
     * @param bool $isEnabled
184
     */
185 16
    public function setSerializeNullStrategy($isEnabled)
186
    {
187 16
        $this->serializeNullStrategy = $isEnabled;
188 16
    }
189
190
    /**
191
     * Sets context adapter.
192
     *
193
     * @param SerializationContextAdapterInterface $contextAdapter
194
     */
195 44
    public function setSerializationContextAdapter(SerializationContextAdapterInterface $contextAdapter)
196
    {
197 44
        $this->contextAdapter = $contextAdapter;
198 44
    }
199
200
    /**
201
     * {@inheritdoc}
202
     */
203 36
    public function supports($format)
204
    {
205 36
        return isset($this->customHandlers[$format]) || isset($this->formats[$format]);
206
    }
207
208
    /**
209
     * Registers a custom handler.
210
     *
211
     * The handler must have the following signature: handler(ViewHandler $viewHandler, View $view, Request $request, $format)
212
     * It can use the public methods of this class to retrieve the needed data and return a
213
     * Response object ready to be sent.
214
     *
215
     * @param string   $format
216
     * @param callable $callable
217
     *
218
     * @throws \InvalidArgumentException
219
     */
220 16
    public function registerHandler($format, $callable)
221
    {
222 16
        if (!is_callable($callable)) {
223 1
            throw new \InvalidArgumentException('Registered view callback must be callable.');
224
        }
225
226 15
        $this->customHandlers[$format] = $callable;
227 15
    }
228
229
    /**
230
     * Gets a response HTTP status code from a View instance.
231
     *
232
     * By default it will return 200. However if there is a FormInterface stored for
233
     * the key 'form' in the View's data it will return the failed_validation
234
     * configuration if the form instance has errors.
235
     *
236
     * @param View  $view
237
     * @param mixed $content
238
     *
239
     * @return int HTTP status code
240
     */
241 48
    protected function getStatusCode(View $view, $content = null)
242
    {
243 48
        $form = $this->getFormFromView($view);
244
245 48
        if ($form && $form->isSubmitted() && !$form->isValid()) {
246 7
            return $this->failedValidationCode;
247
        }
248
249 41
        $statusCode = $view->getStatusCode();
250 41
        if (null !== $statusCode) {
251 10
            return $statusCode;
252
        }
253
254 31
        return null !== $content ? Response::HTTP_OK : $this->emptyContentCode;
255
    }
256
257
    /**
258
     * If the given format uses the templating system for rendering.
259
     *
260
     * @param string $format
261
     *
262
     * @return bool
263
     */
264 38
    public function isFormatTemplating($format)
265
    {
266 38
        return !empty($this->formats[$format]);
267
    }
268
269
    /**
270
     * Gets or creates a JMS\Serializer\SerializationContext and initializes it with
271
     * the view exclusion strategies, groups & versions if a new context is created.
272
     *
273
     * @param View $view
274
     *
275
     * @return ContextInterface
276
     */
277 27
    protected function getSerializationContext(View $view)
278
    {
279 27
        $context = $view->getSerializationContext();
280
281 27
        if ($context instanceof GroupableContextInterface) {
282 27
            $groups = $context->getGroups();
283 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...
284 1
                $context->addGroups((array) $this->exclusionStrategyGroups);
285 1
            }
286 27
        }
287
288 27
        if ($context instanceof VersionableContextInterface && null === $context->getVersion() && $this->exclusionStrategyVersion) {
289 4
            $context->setVersion($this->exclusionStrategyVersion);
290 4
        }
291
292 27
        if ($context instanceof SerializeNullContextInterface && null === $context->getSerializeNull()) {
293 27
            $context->setSerializeNull($this->serializeNullStrategy);
294 27
        }
295
296 27
        return $context;
297
    }
298
299
    /**
300
     * Handles a request with the proper handler.
301
     *
302
     * Decides on which handler to use based on the request format.
303
     *
304
     * @param View    $view
305
     * @param Request $request
306
     *
307
     * @throws UnsupportedMediaTypeHttpException
308
     *
309
     * @return Response
310
     */
311 32
    public function handle(View $view, Request $request = null)
312
    {
313 32
        if (null === $request) {
314 7
            $request = $this->requestStack->getCurrentRequest();
315 7
        }
316
317 32
        $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...
318
319 32
        if (!$this->supports($format)) {
320 1
            $msg = "Format '$format' not supported, handler must be implemented";
321 1
            throw new UnsupportedMediaTypeHttpException($msg);
322
        }
323
324 31
        if (isset($this->customHandlers[$format])) {
325 10
            return call_user_func($this->customHandlers[$format], $this, $view, $request, $format);
326
        }
327
328 21
        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...
329
    }
330
331
    /**
332
     * Creates the Response from the view.
333
     *
334
     * @param View   $view
335
     * @param string $location
336
     * @param string $format
337
     *
338
     * @return Response
339
     */
340 7
    public function createRedirectResponse(View $view, $location, $format)
341
    {
342 7
        $content = null;
343 7
        if (($view->getStatusCode() == Response::HTTP_CREATED || $view->getStatusCode() == Response::HTTP_ACCEPTED) && $view->getData() != null) {
344 1
            $response = $this->initResponse($view, $format);
345 1
        } else {
346 6
            $response = $view->getResponse();
347 6
            if ('html' === $format && isset($this->forceRedirects[$format])) {
348 1
                $redirect = new RedirectResponse($location);
349 1
                $content = $redirect->getContent();
350 1
                $response->setContent($content);
351 1
            }
352
        }
353
354 7
        $code = isset($this->forceRedirects[$format])
355 7
            ? $this->forceRedirects[$format] : $this->getStatusCode($view, $content);
356
357 7
        $response->setStatusCode($code);
358 7
        $response->headers->set('Location', $location);
359
360 7
        return $response;
361
    }
362
363
    /**
364
     * Renders the view data with the given template.
365
     *
366
     * @param View   $view
367
     * @param string $format
368
     *
369
     * @return string
370
     */
371 12
    public function renderTemplate(View $view, $format)
372
    {
373 12
        $data = $this->prepareTemplateParameters($view);
374
375 12
        $template = $view->getTemplate();
376 12
        if ($template instanceof TemplateReference) {
377
            if (null === $template->get('format')) {
378
                $template->set('format', $format);
379
            }
380
381
            if (null === $template->get('engine')) {
382
                $engine = $view->getEngine() ?: $this->defaultEngine;
383
                $template->set('engine', $engine);
384
            }
385
        }
386
387 12
        return $this->templating->render($template, $data);
0 ignored issues
show
Bug introduced by
It seems like $template defined by $view->getTemplate() on line 375 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...
388
    }
389
390
    /**
391
     * Prepares view data for use by templating engine.
392
     *
393
     * @param View $view
394
     *
395
     * @return array
396
     */
397 19
    public function prepareTemplateParameters(View $view)
398
    {
399 19
        $data = $view->getData();
400
401 19
        if ($data instanceof FormInterface) {
402 2
            $data = [$view->getTemplateVar() => $data->getData(), 'form' => $data];
403 19
        } elseif (empty($data) || !is_array($data) || is_numeric((key($data)))) {
404 10
            $data = [$view->getTemplateVar() => $data];
405 10
        }
406
407 19
        if (isset($data['form']) && $data['form'] instanceof FormInterface) {
408 2
            $data['form'] = $data['form']->createView();
409 2
        }
410
411 19
        $templateData = $view->getTemplateData();
412 19
        if (is_callable($templateData)) {
413 2
            $templateData = call_user_func($templateData, $this, $view);
414 2
        }
415
416 19
        return array_merge($data, $templateData);
417
    }
418
419
    /**
420
     * Handles creation of a Response using either redirection or the templating/serializer service.
421
     *
422
     * @param View    $view
423
     * @param Request $request
424
     * @param string  $format
425
     *
426
     * @return Response
427
     */
428 43
    public function createResponse(View $view, Request $request, $format)
429
    {
430 43
        $route = $view->getRoute();
431
        $location = $route
432 43
            ? $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
433 43
            : $view->getLocation();
434
435 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...
436 7
            return $this->createRedirectResponse($view, $location, $format);
437
        }
438
439 36
        $response = $this->initResponse($view, $format);
440
441 36
        if (!$response->headers->has('Content-Type')) {
442 36
            $response->headers->set('Content-Type', $request->getMimeType($format));
443 36
        }
444
445 36
        return $response;
446
    }
447
448
    /**
449
     * Initializes a response object that represents the view and holds the view's status code.
450
     *
451
     * @param View   $view
452
     * @param string $format
453
     *
454
     * @return Response
455
     */
456 37
    private function initResponse(View $view, $format)
457
    {
458 37
        $content = null;
459 37
        if ($this->isFormatTemplating($format)) {
460 12
            $content = $this->renderTemplate($view, $format);
461 37
        } elseif ($this->serializeNull || null !== $view->getData()) {
462 24
            $data = $this->getDataFromView($view);
463
464 24
            $standardContext = $this->getSerializationContext($view);
465 24
            if ($this->contextAdapter instanceof SerializerAwareInterface) {
466 11
                $this->contextAdapter->setSerializer($this->serializer);
467 11
            }
468 24
            $context = $this->contextAdapter->convertSerializationContext($standardContext);
469 24
            $content = $this->serializer->serialize($data, $format, $context);
470 24
        }
471
472 37
        $response = $view->getResponse();
473 37
        $response->setStatusCode($this->getStatusCode($view, $content));
474
475 37
        if (null !== $content) {
476 31
            $response->setContent($content);
477 31
        }
478
479 37
        return $response;
480
    }
481
482
    /**
483
     * Returns the form from the given view if present, false otherwise.
484
     *
485
     * @param View $view
486
     *
487
     * @return bool|FormInterface
488
     */
489 48
    protected function getFormFromView(View $view)
490
    {
491 48
        $data = $view->getData();
492
493 48
        if ($data instanceof FormInterface) {
494 7
            return $data;
495
        }
496
497 41
        if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
498 4
            return $data['form'];
499
        }
500
501 37
        return false;
502
    }
503
504
    /**
505
     * Returns the data from a view. If the data is form with errors, it will return it wrapped in an ExceptionWrapper.
506
     *
507
     * @param View $view
508
     *
509
     * @return mixed|null
510
     */
511 24
    private function getDataFromView(View $view)
512
    {
513 24
        $form = $this->getFormFromView($view);
514
515 24
        if (false === $form) {
516 18
            return $view->getData();
517
        }
518
519 6
        if ($form->isValid() || !$form->isSubmitted()) {
520
            return $form;
521
        }
522
523 6
        return $this->exceptionWrapperHandler->wrap(
524
            [
525 6
                 'status_code' => $this->failedValidationCode,
526 6
                 'message' => 'Validation Failed',
527 6
                 'errors' => $form,
528
            ]
529 6
        );
530
    }
531
}
532