Completed
Push — master ( 933a2a...d1c0c8 )
by Lukas Kahwe
05:29
created

ViewHandler::initResponse()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 26
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6
Metric Value
dl 0
loc 26
ccs 20
cts 20
cp 1
rs 8.439
cc 6
eloc 17
nc 8
nop 2
crap 6
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 75
    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 75
        if (!method_exists($serializer, 'serialize')) {
144
            throw new \InvalidArgumentException('The $serializer argument must implement a serialize() method.');
145
        }
146
147 75
        $this->urlGenerator = $urlGenerator;
148 75
        $this->serializer = $serializer;
149 75
        $this->templating = $templating;
150 75
        $this->requestStack = $requestStack;
151 75
        $this->exceptionWrapperHandler = $exceptionWrapperHandler;
152 75
        $this->formats = (array) $formats;
153 75
        $this->failedValidationCode = $failedValidationCode;
154 75
        $this->emptyContentCode = $emptyContentCode;
155 75
        $this->serializeNull = $serializeNull;
156 75
        $this->forceRedirects = (array) $forceRedirects;
157 75
        $this->defaultEngine = $defaultEngine;
158 75
    }
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 32
    public function supports($format)
204
    {
205 32
        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 43
    protected function getStatusCode(View $view, $content = null)
242
    {
243 43
        $form = $this->getFormFromView($view);
244
245 43
        if ($form && $form->isSubmitted() && !$form->isValid()) {
246 7
            return $this->failedValidationCode;
247
        }
248
249 36
        if (200 !== ($code = $view->getStatusCode())) {
250 9
            return $code;
251
        }
252
253 27
        return null !== $content ? Response::HTTP_OK : $this->emptyContentCode;
254
    }
255
256
    /**
257
     * If the given format uses the templating system for rendering.
258
     *
259
     * @param string $format
260
     *
261
     * @return bool
262
     */
263 34
    public function isFormatTemplating($format)
264
    {
265 34
        return !empty($this->formats[$format]);
266
    }
267
268
    /**
269
     * Gets the serializer service.
270
     *
271
     * @return object that must provide a "serialize()" method
272
     */
273 21
    protected function getSerializer()
274
    {
275 21
        return $this->serializer;
276
    }
277
278
    /**
279
     * Gets or creates a JMS\Serializer\SerializationContext and initializes it with
280
     * the view exclusion strategies, groups & versions if a new context is created.
281
     *
282
     * @param View $view
283
     *
284
     * @return ContextInterface
285
     */
286 24
    protected function getSerializationContext(View $view)
287
    {
288 24
        $context = $view->getSerializationContext();
289
290 24
        if ($context instanceof GroupableContextInterface) {
291 24
            $groups = $context->getGroups();
292 24
            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...
293 1
                $context->addGroups((array) $this->exclusionStrategyGroups);
294 1
            }
295 24
        }
296
297 24
        if ($context instanceof VersionableContextInterface && null === $context->getVersion() && $this->exclusionStrategyVersion) {
298 1
            $context->setVersion($this->exclusionStrategyVersion);
299 1
        }
300
301 24
        if ($context instanceof SerializeNullContextInterface && null === $context->getSerializeNull()) {
302 24
            $context->setSerializeNull($this->serializeNullStrategy);
303 24
        }
304
305 24
        return $context;
306
    }
307
308
    /**
309
     * Gets the templating service.
310
     *
311
     * @return EngineInterface
312
     */
313 11
    protected function getTemplating()
314
    {
315 11
        return $this->templating;
316
    }
317
318
    /**
319
     * Handles a request with the proper handler.
320
     *
321
     * Decides on which handler to use based on the request format.
322
     *
323
     * @param View    $view
324
     * @param Request $request
325
     *
326
     * @throws UnsupportedMediaTypeHttpException
327
     *
328
     * @return Response
329
     */
330 28
    public function handle(View $view, Request $request = null)
331
    {
332 28
        if (null === $request) {
333 7
            $request = $this->requestStack->getCurrentRequest();
334 7
        }
335
336 28
        $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...
337
338 28
        if (!$this->supports($format)) {
339 1
            $msg = "Format '$format' not supported, handler must be implemented";
340 1
            throw new UnsupportedMediaTypeHttpException($msg);
341
        }
342
343 27
        if (isset($this->customHandlers[$format])) {
344 10
            return call_user_func($this->customHandlers[$format], $this, $view, $request, $format);
345
        }
346
347 17
        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...
348
    }
349
350
    /**
351
     * Creates the Response from the view.
352
     *
353
     * @param View   $view
354
     * @param string $location
355
     * @param string $format
356
     *
357
     * @return Response
358
     */
359 7
    public function createRedirectResponse(View $view, $location, $format)
360
    {
361 7
        $content = null;
362 7
        if (($view->getStatusCode() == Response::HTTP_CREATED || $view->getStatusCode() == Response::HTTP_ACCEPTED) && $view->getData() != null) {
363 1
            $response = $this->initResponse($view, $format);
364 1
        } else {
365 6
            $response = $view->getResponse();
366 6
            if ('html' === $format && isset($this->forceRedirects[$format])) {
367 1
                $redirect = new RedirectResponse($location);
368 1
                $content = $redirect->getContent();
369 1
                $response->setContent($content);
370 1
            }
371
        }
372
373 7
        $code = isset($this->forceRedirects[$format])
374 7
            ? $this->forceRedirects[$format] : $this->getStatusCode($view, $content);
375
376 7
        $response->setStatusCode($code);
377 7
        $response->headers->set('Location', $location);
378
379 7
        return $response;
380
    }
381
382
    /**
383
     * Renders the view data with the given template.
384
     *
385
     * @param View   $view
386
     * @param string $format
387
     *
388
     * @return string
389
     */
390 11
    public function renderTemplate(View $view, $format)
391
    {
392 11
        $data = $this->prepareTemplateParameters($view);
393
394 11
        $template = $view->getTemplate();
395 11
        if ($template instanceof TemplateReference) {
396
            if (null === $template->get('format')) {
397
                $template->set('format', $format);
398
            }
399
400
            if (null === $template->get('engine')) {
401
                $engine = $view->getEngine() ?: $this->defaultEngine;
402
                $template->set('engine', $engine);
403
            }
404
        }
405
406 11
        return $this->getTemplating()->render($template, $data);
407
    }
408
409
    /**
410
     * Prepares view data for use by templating engine.
411
     *
412
     * @param View $view
413
     *
414
     * @return array
415
     */
416 18
    public function prepareTemplateParameters(View $view)
417
    {
418 18
        $data = $view->getData();
419
420 18
        if ($data instanceof FormInterface) {
421 2
            $data = [$view->getTemplateVar() => $data->getData(), 'form' => $data];
422 18
        } elseif (empty($data) || !is_array($data) || is_numeric((key($data)))) {
423 10
            $data = [$view->getTemplateVar() => $data];
424 10
        }
425
426 18
        if (isset($data['form']) && $data['form'] instanceof FormInterface) {
427 2
            $data['form'] = $data['form']->createView();
428 2
        }
429
430 18
        $templateData = $view->getTemplateData();
431 18
        if (is_callable($templateData)) {
432 2
            $templateData = call_user_func($templateData, $this, $view);
433 2
        }
434
435 18
        return array_merge($data, $templateData);
436
    }
437
438
    /**
439
     * Handles creation of a Response using either redirection or the templating/serializer service.
440
     *
441
     * @param View    $view
442
     * @param Request $request
443
     * @param string  $format
444
     *
445
     * @return Response
446
     */
447 39
    public function createResponse(View $view, Request $request, $format)
448
    {
449 39
        $route = $view->getRoute();
450
        $location = $route
451 39
            ? $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
452 39
            : $view->getLocation();
453
454 39
        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...
455 7
            return $this->createRedirectResponse($view, $location, $format);
456
        }
457
458 32
        $response = $this->initResponse($view, $format);
459
460 32
        if (!$response->headers->has('Content-Type')) {
461 32
            $response->headers->set('Content-Type', $request->getMimeType($format));
462 32
        }
463
464 32
        return $response;
465
    }
466
467
    /**
468
     * Initializes a response object that represents the view and holds the view's status code.
469
     *
470
     * @param View   $view
471
     * @param string $format
472
     *
473
     * @return Response
474
     */
475 33
    private function initResponse(View $view, $format)
476
    {
477 33
        $content = null;
478 33
        if ($this->isFormatTemplating($format)) {
479 11
            $content = $this->renderTemplate($view, $format);
480 33
        } elseif ($this->serializeNull || null !== $view->getData()) {
481 21
            $data = $this->getDataFromView($view);
482 21
            $serializer = $this->getSerializer($view);
0 ignored issues
show
Unused Code introduced by
The call to ViewHandler::getSerializer() has too many arguments starting with $view.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
483
484 21
            $standardContext = $this->getSerializationContext($view);
485 21
            if ($this->contextAdapter instanceof SerializerAwareInterface) {
486 8
                $this->contextAdapter->setSerializer($serializer);
487 8
            }
488 21
            $context = $this->contextAdapter->convertSerializationContext($standardContext);
489 21
            $content = $serializer->serialize($data, $format, $context);
490 21
        }
491
492 33
        $response = $view->getResponse();
493 33
        $response->setStatusCode($this->getStatusCode($view, $content));
494
495 33
        if (null !== $content) {
496 27
            $response->setContent($content);
497 27
        }
498
499 33
        return $response;
500
    }
501
502
    /**
503
     * Returns the form from the given view if present, false otherwise.
504
     *
505
     * @param View $view
506
     *
507
     * @return bool|FormInterface
508
     */
509 43
    protected function getFormFromView(View $view)
510
    {
511 43
        $data = $view->getData();
512
513 43
        if ($data instanceof FormInterface) {
514 7
            return $data;
515
        }
516
517 36
        if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
518 4
            return $data['form'];
519
        }
520
521 32
        return false;
522
    }
523
524
    /**
525
     * Returns the data from a view. If the data is form with errors, it will return it wrapped in an ExceptionWrapper.
526
     *
527
     * @param View $view
528
     *
529
     * @return mixed|null
530
     */
531 21
    private function getDataFromView(View $view)
532
    {
533 21
        $form = $this->getFormFromView($view);
534
535 21
        if (false === $form) {
536 15
            return $view->getData();
537
        }
538
539 6
        if ($form->isValid() || !$form->isSubmitted()) {
540
            return $form;
541
        }
542
543 6
        return $this->exceptionWrapperHandler->wrap(
544
            [
545 6
                 'status_code' => $this->failedValidationCode,
546 6
                 'message' => 'Validation Failed',
547 6
                 'errors' => $form,
548
            ]
549 6
        );
550
    }
551
}
552