Completed
Push — master ( 4b87d9...f22622 )
by Lukas Kahwe
05:36
created

ViewHandler::getSerializer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
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 or creates a JMS\Serializer\SerializationContext and initializes it with
270
     * the view exclusion strategies, groups & versions if a new context is created.
271
     *
272
     * @param View $view
273
     *
274
     * @return ContextInterface
275
     */
276 24
    protected function getSerializationContext(View $view)
277
    {
278 24
        $context = $view->getSerializationContext();
279
280 24
        if ($context instanceof GroupableContextInterface) {
281 24
            $groups = $context->getGroups();
282 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...
283 1
                $context->addGroups((array) $this->exclusionStrategyGroups);
284 1
            }
285 24
        }
286
287 24
        if ($context instanceof VersionableContextInterface && null === $context->getVersion() && $this->exclusionStrategyVersion) {
288 1
            $context->setVersion($this->exclusionStrategyVersion);
289 1
        }
290
291 24
        if ($context instanceof SerializeNullContextInterface && null === $context->getSerializeNull()) {
292 24
            $context->setSerializeNull($this->serializeNullStrategy);
293 24
        }
294
295 24
        return $context;
296
    }
297
298
    /**
299
     * Handles a request with the proper handler.
300
     *
301
     * Decides on which handler to use based on the request format.
302
     *
303
     * @param View    $view
304
     * @param Request $request
305
     *
306
     * @throws UnsupportedMediaTypeHttpException
307
     *
308
     * @return Response
309
     */
310 28
    public function handle(View $view, Request $request = null)
311
    {
312 28
        if (null === $request) {
313 7
            $request = $this->requestStack->getCurrentRequest();
314 7
        }
315
316 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...
317
318 28
        if (!$this->supports($format)) {
319 1
            $msg = "Format '$format' not supported, handler must be implemented";
320 1
            throw new UnsupportedMediaTypeHttpException($msg);
321
        }
322
323 27
        if (isset($this->customHandlers[$format])) {
324 10
            return call_user_func($this->customHandlers[$format], $this, $view, $request, $format);
325
        }
326
327 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...
328
    }
329
330
    /**
331
     * Creates the Response from the view.
332
     *
333
     * @param View   $view
334
     * @param string $location
335
     * @param string $format
336
     *
337
     * @return Response
338
     */
339 7
    public function createRedirectResponse(View $view, $location, $format)
340
    {
341 7
        $content = null;
342 7
        if (($view->getStatusCode() == Response::HTTP_CREATED || $view->getStatusCode() == Response::HTTP_ACCEPTED) && $view->getData() != null) {
343 1
            $response = $this->initResponse($view, $format);
344 1
        } else {
345 6
            $response = $view->getResponse();
346 6
            if ('html' === $format && isset($this->forceRedirects[$format])) {
347 1
                $redirect = new RedirectResponse($location);
348 1
                $content = $redirect->getContent();
349 1
                $response->setContent($content);
350 1
            }
351
        }
352
353 7
        $code = isset($this->forceRedirects[$format])
354 7
            ? $this->forceRedirects[$format] : $this->getStatusCode($view, $content);
355
356 7
        $response->setStatusCode($code);
357 7
        $response->headers->set('Location', $location);
358
359 7
        return $response;
360
    }
361
362
    /**
363
     * Renders the view data with the given template.
364
     *
365
     * @param View   $view
366
     * @param string $format
367
     *
368
     * @return string
369
     */
370 11
    public function renderTemplate(View $view, $format)
371
    {
372 11
        $data = $this->prepareTemplateParameters($view);
373
374 11
        $template = $view->getTemplate();
375 11
        if ($template instanceof TemplateReference) {
376
            if (null === $template->get('format')) {
377
                $template->set('format', $format);
378
            }
379
380
            if (null === $template->get('engine')) {
381
                $engine = $view->getEngine() ?: $this->defaultEngine;
382
                $template->set('engine', $engine);
383
            }
384
        }
385
386 11
        return $this->templating->render($template, $data);
387
    }
388
389
    /**
390
     * Prepares view data for use by templating engine.
391
     *
392
     * @param View $view
393
     *
394
     * @return array
395
     */
396 18
    public function prepareTemplateParameters(View $view)
397
    {
398 18
        $data = $view->getData();
399
400 18
        if ($data instanceof FormInterface) {
401 2
            $data = [$view->getTemplateVar() => $data->getData(), 'form' => $data];
402 18
        } elseif (empty($data) || !is_array($data) || is_numeric((key($data)))) {
403 10
            $data = [$view->getTemplateVar() => $data];
404 10
        }
405
406 18
        if (isset($data['form']) && $data['form'] instanceof FormInterface) {
407 2
            $data['form'] = $data['form']->createView();
408 2
        }
409
410 18
        $templateData = $view->getTemplateData();
411 18
        if (is_callable($templateData)) {
412 2
            $templateData = call_user_func($templateData, $this, $view);
413 2
        }
414
415 18
        return array_merge($data, $templateData);
416
    }
417
418
    /**
419
     * Handles creation of a Response using either redirection or the templating/serializer service.
420
     *
421
     * @param View    $view
422
     * @param Request $request
423
     * @param string  $format
424
     *
425
     * @return Response
426
     */
427 39
    public function createResponse(View $view, Request $request, $format)
428
    {
429 39
        $route = $view->getRoute();
430
        $location = $route
431 39
            ? $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
432 39
            : $view->getLocation();
433
434 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...
435 7
            return $this->createRedirectResponse($view, $location, $format);
436
        }
437
438 32
        $response = $this->initResponse($view, $format);
439
440 32
        if (!$response->headers->has('Content-Type')) {
441 32
            $response->headers->set('Content-Type', $request->getMimeType($format));
442 32
        }
443
444 32
        return $response;
445
    }
446
447
    /**
448
     * Initializes a response object that represents the view and holds the view's status code.
449
     *
450
     * @param View   $view
451
     * @param string $format
452
     *
453
     * @return Response
454
     */
455 33
    private function initResponse(View $view, $format)
456
    {
457 33
        $content = null;
458 33
        if ($this->isFormatTemplating($format)) {
459 11
            $content = $this->renderTemplate($view, $format);
460 33
        } elseif ($this->serializeNull || null !== $view->getData()) {
461 21
            $data = $this->getDataFromView($view);
462
463 21
            $standardContext = $this->getSerializationContext($view);
464 21
            if ($this->contextAdapter instanceof SerializerAwareInterface) {
465 8
                $this->contextAdapter->setSerializer($this->serializer);
466 8
            }
467 21
            $context = $this->contextAdapter->convertSerializationContext($standardContext);
468 21
            $content = $this->serializer->serialize($data, $format, $context);
469 21
        }
470
471 33
        $response = $view->getResponse();
472 33
        $response->setStatusCode($this->getStatusCode($view, $content));
473
474 33
        if (null !== $content) {
475 27
            $response->setContent($content);
476 27
        }
477
478 33
        return $response;
479
    }
480
481
    /**
482
     * Returns the form from the given view if present, false otherwise.
483
     *
484
     * @param View $view
485
     *
486
     * @return bool|FormInterface
487
     */
488 43
    protected function getFormFromView(View $view)
489
    {
490 43
        $data = $view->getData();
491
492 43
        if ($data instanceof FormInterface) {
493 7
            return $data;
494
        }
495
496 36
        if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
497 4
            return $data['form'];
498
        }
499
500 32
        return false;
501
    }
502
503
    /**
504
     * Returns the data from a view. If the data is form with errors, it will return it wrapped in an ExceptionWrapper.
505
     *
506
     * @param View $view
507
     *
508
     * @return mixed|null
509
     */
510 21
    private function getDataFromView(View $view)
511
    {
512 21
        $form = $this->getFormFromView($view);
513
514 21
        if (false === $form) {
515 15
            return $view->getData();
516
        }
517
518 6
        if ($form->isValid() || !$form->isSubmitted()) {
519
            return $form;
520
        }
521
522 6
        return $this->exceptionWrapperHandler->wrap(
523
            [
524 6
                 'status_code' => $this->failedValidationCode,
525 6
                 'message' => 'Validation Failed',
526 6
                 'errors' => $form,
527
            ]
528 6
        );
529
    }
530
}
531