Completed
Push — master ( 469e8a...669561 )
by Guilh
05:45
created

ViewHandler::setExclusionStrategyVersion()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
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\TemplateReference;
21
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
22
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
23
use Symfony\Component\Form\FormInterface;
24
use Symfony\Component\HttpFoundation\RedirectResponse;
25
use Symfony\Component\HttpFoundation\Request;
26
use Symfony\Component\HttpFoundation\Response;
27
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
28
use Symfony\Component\Routing\RouterInterface;
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, ContainerAwareInterface
39
{
40
    use ContainerAwareTrait;
41
42
    /**
43
     * Key format, value a callable that returns a Response instance.
44
     *
45
     * @var array
46
     */
47
    protected $customHandlers = [];
48
49
    /**
50
     * The supported formats as keys and if the given formats
51
     * uses templating is denoted by a true value.
52
     *
53
     * @var array
54
     */
55
    protected $formats;
56
57
    /**
58
     *  HTTP response status code for a failed validation.
59
     *
60
     * @var int
61
     */
62
    protected $failedValidationCode;
63
64
    /**
65
     * HTTP response status code when the view data is null.
66
     *
67
     * @var int
68
     */
69
    protected $emptyContentCode;
70
71
    /**
72
     * Whether or not to serialize null view data.
73
     *
74
     * @var bool
75
     */
76
    protected $serializeNull;
77
78
    /**
79
     * If to force a redirect for the given key format,
80
     * with value being the status code to use.
81
     *
82
     * @var array
83
     */
84
    protected $forceRedirects;
85
86
    /**
87
     * @var string
88
     */
89
    protected $defaultEngine;
90
91
    /**
92
     * @var array
93
     */
94
    protected $exclusionStrategyGroups = [];
95
96
    /**
97
     * @var string
98
     */
99
    protected $exclusionStrategyVersion;
100
101
    /**
102
     * @var bool
103
     */
104
    protected $serializeNullStrategy;
105
106
    /**
107
     * @var SerializationContextAdapterInterface
108
     */
109
    protected $contextAdapter;
110
111
    /**
112
     * Constructor.
113
     *
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
     * @param string $defaultEngine        default engine (twig, php ..)
120
     */
121 71
    public function __construct(
122
        array $formats = null,
123
        $failedValidationCode = Response::HTTP_BAD_REQUEST,
124
        $emptyContentCode = Response::HTTP_NO_CONTENT,
125
        $serializeNull = false,
126
        array $forceRedirects = null,
127
        $defaultEngine = 'twig'
128
    ) {
129 71
        $this->formats = (array) $formats;
130 71
        $this->failedValidationCode = $failedValidationCode;
131 71
        $this->emptyContentCode = $emptyContentCode;
132 71
        $this->serializeNull = $serializeNull;
133 71
        $this->forceRedirects = (array) $forceRedirects;
134 71
        $this->defaultEngine = $defaultEngine;
135 71
    }
136
137
    /**
138
     * Sets the default serialization groups.
139
     *
140
     * @param array|string $groups
141
     */
142 1
    public function setExclusionStrategyGroups($groups)
143
    {
144 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...
145 1
    }
146
147
    /**
148
     * Sets the default serialization version.
149
     *
150
     * @param string $version
151
     */
152 5
    public function setExclusionStrategyVersion($version)
153
    {
154 5
        $this->exclusionStrategyVersion = $version;
155 5
    }
156
157
    /**
158
     * If nulls should be serialized.
159
     *
160
     * @param bool $isEnabled
161
     */
162 16
    public function setSerializeNullStrategy($isEnabled)
163
    {
164 16
        $this->serializeNullStrategy = $isEnabled;
165 16
    }
166
167
    /**
168
     * Sets context adapter.
169
     *
170
     * @param SerializationContextAdapterInterface $contextAdapter
171
     */
172 44
    public function setSerializationContextAdapter(SerializationContextAdapterInterface $contextAdapter)
173
    {
174 44
        $this->contextAdapter = $contextAdapter;
175 44
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180 32
    public function supports($format)
181
    {
182 32
        return isset($this->customHandlers[$format]) || isset($this->formats[$format]);
183
    }
184
185
    /**
186
     * Registers a custom handler.
187
     *
188
     * The handler must have the following signature: handler(ViewHandler $viewHandler, View $view, Request $request, $format)
189
     * It can use the public methods of this class to retrieve the needed data and return a
190
     * Response object ready to be sent.
191
     *
192
     * @param string   $format
193
     * @param callable $callable
194
     *
195
     * @throws \InvalidArgumentException
196
     */
197 16
    public function registerHandler($format, $callable)
198
    {
199 16
        if (!is_callable($callable)) {
200 1
            throw new \InvalidArgumentException('Registered view callback must be callable.');
201
        }
202
203 15
        $this->customHandlers[$format] = $callable;
204 15
    }
205
206
    /**
207
     * Gets a response HTTP status code from a View instance.
208
     *
209
     * By default it will return 200. However if there is a FormInterface stored for
210
     * the key 'form' in the View's data it will return the failed_validation
211
     * configuration if the form instance has errors.
212
     *
213
     * @param View  $view
214
     * @param mixed $content
215
     *
216
     * @return int HTTP status code
217
     */
218 43
    protected function getStatusCode(View $view, $content = null)
219
    {
220 43
        $form = $this->getFormFromView($view);
221
222 43
        if ($form && $form->isSubmitted() && !$form->isValid()) {
223 7
            return $this->failedValidationCode;
224
        }
225
226 36
        if (200 !== ($code = $view->getStatusCode())) {
227 9
            return $code;
228
        }
229
230 27
        return null !== $content ? Response::HTTP_OK : $this->emptyContentCode;
231
    }
232
233
    /**
234
     * If the given format uses the templating system for rendering.
235
     *
236
     * @param string $format
237
     *
238
     * @return bool
239
     */
240 34
    public function isFormatTemplating($format)
241
    {
242 34
        return !empty($this->formats[$format]);
243
    }
244
245
    /**
246
     * Gets the router service.
247
     *
248
     * @return RouterInterface
249
     */
250 1
    protected function getRouter()
251
    {
252 1
        return $this->container->get('fos_rest.router');
253
    }
254
255
    /**
256
     * Gets the serializer service.
257
     *
258
     * @param View $view view instance from which the serializer should be configured
259
     *
260
     * @return object that must provide a "serialize()" method
261
     */
262 21
    protected function getSerializer(View $view = null)
0 ignored issues
show
Unused Code introduced by
The parameter $view is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

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