Completed
Push — master ( 1af7ca...fe85dd )
by Lukas Kahwe
05:49
created

ViewHandler::isFormatTemplating()   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
Metric Value
dl 0
loc 4
ccs 2
cts 2
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
29
/**
30
 * View may be used in controllers to build up a response in a format agnostic way
31
 * The View class takes care of encoding your data in json, xml, or renders a
32
 * template for html via the Serializer component.
33
 *
34
 * @author Jordi Boggiano <[email protected]>
35
 * @author Lukas K. Smith <[email protected]>
36
 */
37
class ViewHandler implements ConfigurableViewHandlerInterface, ContainerAwareInterface
38
{
39
    use ContainerAwareTrait;
40
41
    /**
42
     * Key format, value a callable that returns a Response instance.
43
     *
44
     * @var array
45
     */
46
    protected $customHandlers = [];
47
48
    /**
49
     * The supported formats as keys and if the given formats
50
     * uses templating is denoted by a true value.
51
     *
52
     * @var array
53
     */
54
    protected $formats;
55
56
    /**
57
     *  HTTP response status code for a failed validation.
58
     *
59
     * @var int
60
     */
61
    protected $failedValidationCode;
62
63
    /**
64
     * HTTP response status code when the view data is null.
65
     *
66
     * @var int
67
     */
68
    protected $emptyContentCode;
69
70
    /**
71
     * Whether or not to serialize null view data.
72
     *
73
     * @var bool
74
     */
75
    protected $serializeNull;
76
77
    /**
78
     * If to force a redirect for the given key format,
79
     * with value being the status code to use.
80
     *
81
     * @var array
82
     */
83
    protected $forceRedirects;
84
85
    /**
86
     * @var string
87
     */
88
    protected $defaultEngine;
89
90
    /**
91
     * @var array
92
     */
93
    protected $exclusionStrategyGroups = [];
94
95
    /**
96
     * @var string
97
     */
98
    protected $exclusionStrategyVersion;
99
100
    /**
101
     * @var bool
102
     */
103
    protected $serializeNullStrategy;
104
105
    /**
106
     * @var SerializationContextAdapterInterface
107
     */
108
    protected $contextAdapter;
109
110
    /**
111
     * Constructor.
112
     *
113
     * @param array  $formats              the supported formats as keys and if the given formats uses templating is denoted by a true value
114
     * @param int    $failedValidationCode The HTTP response status code for a failed validation
115
     * @param int    $emptyContentCode     HTTP response status code when the view data is null
116
     * @param bool   $serializeNull        Whether or not to serialize null view data
117
     * @param array  $forceRedirects       If to force a redirect for the given key format, with value being the status code to use
118
     * @param string $defaultEngine        default engine (twig, php ..)
119
     */
120 71
    public function __construct(
121
        array $formats = null,
122
        $failedValidationCode = Response::HTTP_BAD_REQUEST,
123
        $emptyContentCode = Response::HTTP_NO_CONTENT,
124
        $serializeNull = false,
125
        array $forceRedirects = null,
126
        $defaultEngine = 'twig'
127
    ) {
128 71
        $this->formats = (array) $formats;
129 71
        $this->failedValidationCode = $failedValidationCode;
130 71
        $this->emptyContentCode = $emptyContentCode;
131 71
        $this->serializeNull = $serializeNull;
132 71
        $this->forceRedirects = (array) $forceRedirects;
133 71
        $this->defaultEngine = $defaultEngine;
134 71
    }
135
136
    /**
137
     * Sets the default serialization groups.
138
     *
139
     * @param array|string $groups
140
     */
141 1
    public function setExclusionStrategyGroups($groups)
142
    {
143 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...
144 1
    }
145
146
    /**
147
     * Sets the default serialization version.
148
     *
149
     * @param string $version
150
     */
151 5
    public function setExclusionStrategyVersion($version)
152
    {
153 5
        $this->exclusionStrategyVersion = $version;
154 5
    }
155
156
    /**
157
     * If nulls should be serialized.
158
     *
159
     * @param bool $isEnabled
160
     */
161 16
    public function setSerializeNullStrategy($isEnabled)
162
    {
163 16
        $this->serializeNullStrategy = $isEnabled;
164 16
    }
165
166
    /**
167
     * Sets context adapter.
168
     *
169
     * @param SerializationContextAdapterInterface $contextAdapter
170
     */
171 44
    public function setSerializationContextAdapter(SerializationContextAdapterInterface $contextAdapter)
172
    {
173 44
        $this->contextAdapter = $contextAdapter;
174 44
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 32
    public function supports($format)
180
    {
181 32
        return isset($this->customHandlers[$format]) || isset($this->formats[$format]);
182
    }
183
184
    /**
185
     * Registers a custom handler.
186
     *
187
     * The handler must have the following signature: handler(ViewHandler $viewHandler, View $view, Request $request, $format)
188
     * It can use the public methods of this class to retrieve the needed data and return a
189
     * Response object ready to be sent.
190
     *
191
     * @param string   $format
192
     * @param callable $callable
193
     *
194
     * @throws \InvalidArgumentException
195
     */
196 16
    public function registerHandler($format, $callable)
197
    {
198 16
        if (!is_callable($callable)) {
199 1
            throw new \InvalidArgumentException('Registered view callback must be callable.');
200
        }
201
202 15
        $this->customHandlers[$format] = $callable;
203 15
    }
204
205
    /**
206
     * Gets a response HTTP status code from a View instance.
207
     *
208
     * By default it will return 200. However if there is a FormInterface stored for
209
     * the key 'form' in the View's data it will return the failed_validation
210
     * configuration if the form instance has errors.
211
     *
212
     * @param View  $view
213
     * @param mixed $content
214
     *
215
     * @return int HTTP status code
216
     */
217 43
    protected function getStatusCode(View $view, $content = null)
218
    {
219 43
        $form = $this->getFormFromView($view);
220
221 43
        if ($form && $form->isSubmitted() && !$form->isValid()) {
222 7
            return $this->failedValidationCode;
223
        }
224
225 36
        if (200 !== ($code = $view->getStatusCode())) {
226 9
            return $code;
227
        }
228
229 27
        return null !== $content ? Response::HTTP_OK : $this->emptyContentCode;
230
    }
231
232
    /**
233
     * If the given format uses the templating system for rendering.
234
     *
235
     * @param string $format
236
     *
237
     * @return bool
238
     */
239 34
    public function isFormatTemplating($format)
240
    {
241 34
        return !empty($this->formats[$format]);
242
    }
243
244
    /**
245
     * Gets the router service.
246
     *
247
     * @return \Symfony\Component\Routing\RouterInterface
248
     */
249 1
    protected function getRouter()
250
    {
251 1
        return $this->container->get('fos_rest.router');
252
    }
253
254
    /**
255
     * Gets the serializer service.
256
     *
257
     * @param View $view view instance from which the serializer should be configured
258
     *
259
     * @return object that must provide a "serialize()" method
260
     */
261 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...
262
    {
263 21
        return $this->container->get('fos_rest.serializer');
264
    }
265
266
    /**
267
     * Gets or creates a JMS\Serializer\SerializationContext and initializes it with
268
     * the view exclusion strategies, groups & versions if a new context is created.
269
     *
270
     * @param View $view
271
     *
272
     * @return ContextInterface
273
     */
274 24
    protected function getSerializationContext(View $view)
275
    {
276 24
        $context = $view->getSerializationContext();
277
278 24
        if ($context instanceof GroupableContextInterface) {
279 24
            $groups = $context->getGroups();
280 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...
281 1
                $context->addGroups((array) $this->exclusionStrategyGroups);
282 1
            }
283 24
        }
284
285 24
        if ($context instanceof VersionableContextInterface && null === $context->getVersion() && $this->exclusionStrategyVersion) {
286 1
            $context->setVersion($this->exclusionStrategyVersion);
287 1
        }
288
289 24
        if ($context instanceof SerializeNullContextInterface && null === $context->getSerializeNull()) {
290 24
            $context->setSerializeNull($this->serializeNullStrategy);
291 24
        }
292
293 24
        return $context;
294
    }
295
296
    /**
297
     * Gets the templating service.
298
     *
299
     * @return \Symfony\Bundle\FrameworkBundle\Templating\EngineInterface
300
     */
301 11
    protected function getTemplating()
302
    {
303 11
        return $this->container->get('fos_rest.templating');
304
    }
305
306
    /**
307
     * Handles a request with the proper handler.
308
     *
309
     * Decides on which handler to use based on the request format.
310
     *
311
     * @param View    $view
312
     * @param Request $request
313
     *
314
     * @throws UnsupportedMediaTypeHttpException
315
     *
316
     * @return Response
317
     */
318 28
    public function handle(View $view, Request $request = null)
319
    {
320 28
        if (null === $request) {
321 7
            $request = $this->container->get('request_stack')->getCurrentRequest();
322 7
        }
323
324 28
        $format = $view->getFormat() ?: $request->getRequestFormat();
325
326 28
        if (!$this->supports($format)) {
327 1
            $msg = "Format '$format' not supported, handler must be implemented";
328 1
            throw new UnsupportedMediaTypeHttpException($msg);
329
        }
330
331 27
        if (isset($this->customHandlers[$format])) {
332 10
            return call_user_func($this->customHandlers[$format], $this, $view, $request, $format);
333
        }
334
335 17
        return $this->createResponse($view, $request, $format);
336
    }
337
338
    /**
339
     * Creates the Response from the view.
340
     *
341
     * @param View   $view
342
     * @param string $location
343
     * @param string $format
344
     *
345
     * @return Response
346
     */
347 7
    public function createRedirectResponse(View $view, $location, $format)
348
    {
349 7
        $content = null;
350 7
        if (($view->getStatusCode() == Response::HTTP_CREATED || $view->getStatusCode() == Response::HTTP_ACCEPTED) && $view->getData() != null) {
351 1
            $response = $this->initResponse($view, $format);
352 1
        } else {
353 6
            $response = $view->getResponse();
354 6
            if ('html' === $format && isset($this->forceRedirects[$format])) {
355 1
                $redirect = new RedirectResponse($location);
356 1
                $content = $redirect->getContent();
357 1
                $response->setContent($content);
358 1
            }
359
        }
360
361 7
        $code = isset($this->forceRedirects[$format])
362 7
            ? $this->forceRedirects[$format] : $this->getStatusCode($view, $content);
363
364 7
        $response->setStatusCode($code);
365 7
        $response->headers->set('Location', $location);
366
367 7
        return $response;
368
    }
369
370
    /**
371
     * Renders the view data with the given template.
372
     *
373
     * @param View   $view
374
     * @param string $format
375
     *
376
     * @return string
377
     */
378 11
    public function renderTemplate(View $view, $format)
379
    {
380 11
        $data = $this->prepareTemplateParameters($view);
381
382 11
        $template = $view->getTemplate();
383 11
        if ($template instanceof TemplateReference) {
384
            if (null === $template->get('format')) {
385
                $template->set('format', $format);
386
            }
387
388
            if (null === $template->get('engine')) {
389
                $engine = $view->getEngine() ?: $this->defaultEngine;
390
                $template->set('engine', $engine);
391
            }
392
        }
393
394 11
        return $this->getTemplating()->render($template, $data);
395
    }
396
397
    /**
398
     * Prepares view data for use by templating engine.
399
     *
400
     * @param View $view
401
     *
402
     * @return array
403
     */
404 18
    public function prepareTemplateParameters(View $view)
405
    {
406 18
        $data = $view->getData();
407
408 18
        if ($data instanceof FormInterface) {
409 2
            $data = [$view->getTemplateVar() => $data->getData(), 'form' => $data];
410 18
        } elseif (empty($data) || !is_array($data) || is_numeric((key($data)))) {
411 10
            $data = [$view->getTemplateVar() => $data];
412 10
        }
413
414 18
        if (isset($data['form']) && $data['form'] instanceof FormInterface) {
415 2
            $data['form'] = $data['form']->createView();
416 2
        }
417
418 18
        $templateData = $view->getTemplateData();
419 18
        if (is_callable($templateData)) {
420 2
            $templateData = call_user_func($templateData, $this, $view);
421 2
        }
422
423 18
        return array_merge($data, $templateData);
424
    }
425
426
    /**
427
     * Handles creation of a Response using either redirection or the templating/serializer service.
428
     *
429
     * @param View    $view
430
     * @param Request $request
431
     * @param string  $format
432
     *
433
     * @return Response
434
     */
435 39
    public function createResponse(View $view, Request $request, $format)
436
    {
437 39
        $route = $view->getRoute();
438
        $location = $route
439 39
            ? $this->getRouter()->generate($route, (array) $view->getRouteParameters(), true)
440 39
            : $view->getLocation();
441
442 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...
443 7
            return $this->createRedirectResponse($view, $location, $format);
444
        }
445
446 32
        $response = $this->initResponse($view, $format);
447
448 32
        if (!$response->headers->has('Content-Type')) {
449 32
            $response->headers->set('Content-Type', $request->getMimeType($format));
450 32
        }
451
452 32
        return $response;
453
    }
454
455
    /**
456
     * Initializes a response object that represents the view and holds the view's status code.
457
     *
458
     * @param View   $view
459
     * @param string $format
460
     *
461
     * @return Response
462
     */
463 33
    private function initResponse(View $view, $format)
464
    {
465 33
        $content = null;
466 33
        if ($this->isFormatTemplating($format)) {
467 11
            $content = $this->renderTemplate($view, $format);
468 33
        } elseif ($this->serializeNull || null !== $view->getData()) {
469 21
            $data = $this->getDataFromView($view);
470 21
            $serializer = $this->getSerializer($view);
471
472 21
            $standardContext = $this->getSerializationContext($view);
473 21
            if ($this->contextAdapter instanceof SerializerAwareInterface) {
474 8
                $this->contextAdapter->setSerializer($serializer);
475 8
            }
476 21
            $context = $this->contextAdapter->convertSerializationContext($standardContext);
477 21
            $content = $serializer->serialize($data, $format, $context);
478 21
        }
479
480 33
        $response = $view->getResponse();
481 33
        $response->setStatusCode($this->getStatusCode($view, $content));
482
483 33
        if (null !== $content) {
484 29
            $response->setContent($content);
485 29
        }
486
487 33
        return $response;
488
    }
489
490
    /**
491
     * Returns the form from the given view if present, false otherwise.
492
     *
493
     * @param View $view
494
     *
495
     * @return bool|FormInterface
496
     */
497 43
    protected function getFormFromView(View $view)
498
    {
499 43
        $data = $view->getData();
500
501 43
        if ($data instanceof FormInterface) {
502 7
            return $data;
503
        }
504
505 36
        if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
506 4
            return $data['form'];
507
        }
508
509 32
        return false;
510
    }
511
512
    /**
513
     * Returns the data from a view. If the data is form with errors, it will return it wrapped in an ExceptionWrapper.
514
     *
515
     * @param View $view
516
     *
517
     * @return mixed|null
518
     */
519 21
    private function getDataFromView(View $view)
520
    {
521 21
        $form = $this->getFormFromView($view);
522
523 21
        if (false === $form) {
524 15
            return $view->getData();
525
        }
526
527 6
        if ($form->isValid() || !$form->isSubmitted()) {
528
            return $form;
529
        }
530
531
        /** @var ExceptionWrapperHandlerInterface $exceptionWrapperHandler */
532 6
        $exceptionWrapperHandler = $this->container->get('fos_rest.exception_handler');
533
534 6
        return $exceptionWrapperHandler->wrap(
535
            [
536 6
                 'status_code' => $this->failedValidationCode,
537 6
                 'message' => 'Validation Failed',
538 6
                 'errors' => $form,
539
            ]
540 6
        );
541
    }
542
}
543