Completed
Push — master ( 1319dc...6e6413 )
by Lukas Kahwe
05:12
created

ViewHandler::getDataFromView()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4.0119

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 20
ccs 10
cts 11
cp 0.9091
rs 9.2
cc 4
eloc 10
nc 3
nop 1
crap 4.0119
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\Context;
15
use FOS\RestBundle\Serializer\Serializer;
16
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
17
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReferenceInterface;
18
use Symfony\Component\Form\FormInterface;
19
use Symfony\Component\HttpFoundation\RedirectResponse;
20
use Symfony\Component\HttpFoundation\Request;
21
use Symfony\Component\HttpFoundation\RequestStack;
22
use Symfony\Component\HttpFoundation\Response;
23
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
24
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
25
26
/**
27
 * View may be used in controllers to build up a response in a format agnostic way
28
 * The View class takes care of encoding your data in json, xml, or renders a
29
 * template for html via the Serializer component.
30
 *
31
 * @author Jordi Boggiano <[email protected]>
32
 * @author Lukas K. Smith <[email protected]>
33
 */
34
class ViewHandler implements ConfigurableViewHandlerInterface
35
{
36
    /**
37
     * Key format, value a callable that returns a Response instance.
38
     *
39
     * @var array
40
     */
41
    protected $customHandlers = [];
42
43
    /**
44
     * The supported formats as keys and if the given formats
45
     * uses templating is denoted by a true value.
46
     *
47
     * @var array
48
     */
49
    protected $formats;
50
51
    /**
52
     *  HTTP response status code for a failed validation.
53
     *
54
     * @var int
55
     */
56
    protected $failedValidationCode;
57
58
    /**
59
     * HTTP response status code when the view data is null.
60
     *
61
     * @var int
62
     */
63
    protected $emptyContentCode;
64
65
    /**
66
     * Whether or not to serialize null view data.
67
     *
68
     * @var bool
69
     */
70
    protected $serializeNull;
71
72
    /**
73
     * If to force a redirect for the given key format,
74
     * with value being the status code to use.
75
     *
76
     * @var array
77
     */
78
    protected $forceRedirects;
79
80
    /**
81
     * @var string
82
     */
83
    protected $defaultEngine;
84
85
    /**
86
     * @var array
87
     */
88
    protected $exclusionStrategyGroups = [];
89
90
    /**
91
     * @var string
92
     */
93
    protected $exclusionStrategyVersion;
94
95
    /**
96
     * @var bool
97
     */
98
    protected $serializeNullStrategy;
99
100
    private $urlGenerator;
101
    private $serializer;
102
    private $templating;
103
    private $requestStack;
104
    private $exceptionWrapperHandler;
105
106
    /**
107
     * Constructor.
108
     *
109
     * @param UrlGeneratorInterface            $urlGenerator            The URL generator
110
     * @param Serializer                       $serializer
111
     * @param EngineInterface                  $templating              The configured templating engine
112
     * @param RequestStack                     $requestStack            The request stack
113
     * @param ExceptionWrapperHandlerInterface $exceptionWrapperHandler An exception wrapper handler
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 75
    public function __construct(
122
        UrlGeneratorInterface $urlGenerator,
123
        Serializer $serializer,
124
        EngineInterface $templating,
125
        RequestStack $requestStack,
126
        ExceptionWrapperHandlerInterface $exceptionWrapperHandler,
127
        array $formats = null,
128
        $failedValidationCode = Response::HTTP_BAD_REQUEST,
129
        $emptyContentCode = Response::HTTP_NO_CONTENT,
130
        $serializeNull = false,
131
        array $forceRedirects = null,
132
        $defaultEngine = 'twig'
133
    ) {
134 75
        $this->urlGenerator = $urlGenerator;
135 75
        $this->serializer = $serializer;
136 75
        $this->templating = $templating;
137 75
        $this->requestStack = $requestStack;
138 75
        $this->exceptionWrapperHandler = $exceptionWrapperHandler;
139 75
        $this->formats = (array) $formats;
140 75
        $this->failedValidationCode = $failedValidationCode;
141 75
        $this->emptyContentCode = $emptyContentCode;
142 75
        $this->serializeNull = $serializeNull;
143 75
        $this->forceRedirects = (array) $forceRedirects;
144 75
        $this->defaultEngine = $defaultEngine;
145 75
    }
146
147
    /**
148
     * Sets the default serialization groups.
149
     *
150
     * @param array|string $groups
151
     */
152 1
    public function setExclusionStrategyGroups($groups)
153
    {
154 1
        $this->exclusionStrategyGroups = (array) $groups;
155 1
    }
156
157
    /**
158
     * Sets the default serialization version.
159
     *
160
     * @param string $version
161
     */
162 5
    public function setExclusionStrategyVersion($version)
163
    {
164 5
        $this->exclusionStrategyVersion = $version;
165 5
    }
166
167
    /**
168
     * If nulls should be serialized.
169
     *
170
     * @param bool $isEnabled
171
     */
172 18
    public function setSerializeNullStrategy($isEnabled)
173
    {
174 18
        $this->serializeNullStrategy = $isEnabled;
175 18
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180 38
    public function supports($format)
181
    {
182 38
        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 49
    protected function getStatusCode(View $view, $content = null)
219
    {
220 49
        $form = $this->getFormFromView($view);
221
222 49
        if ($form && $form->isSubmitted() && !$form->isValid()) {
223 7
            return $this->failedValidationCode;
224
        }
225
226 42
        $statusCode = $view->getStatusCode();
227 42
        if (null !== $statusCode) {
228 10
            return $statusCode;
229
        }
230
231 32
        return null !== $content ? Response::HTTP_OK : $this->emptyContentCode;
232
    }
233
234
    /**
235
     * If the given format uses the templating system for rendering.
236
     *
237
     * @param string $format
238
     *
239
     * @return bool
240
     */
241 40
    public function isFormatTemplating($format)
242
    {
243 40
        return !empty($this->formats[$format]);
244
    }
245
246
    /**
247
     * Gets or creates a JMS\Serializer\SerializationContext and initializes it with
248
     * the view exclusion strategies, groups & versions if a new context is created.
249
     *
250
     * @param View $view
251
     *
252
     * @return Context
253
     */
254 27
    protected function getSerializationContext(View $view)
255
    {
256 27
        $context = $view->getContext();
257
258 27
        $groups = $context->getGroups();
259 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...
260 1
            $context->addGroups($this->exclusionStrategyGroups);
261 1
        }
262
263 27
        if (null === $context->getVersion() && $this->exclusionStrategyVersion) {
264 4
            $context->setVersion($this->exclusionStrategyVersion);
265 4
        }
266
267 27
        if (null === $context->getSerializeNull() && null !== $this->serializeNullStrategy) {
268 14
            $context->setSerializeNull($this->serializeNullStrategy);
269 14
        }
270
271 27
        return $context;
272
    }
273
274
    /**
275
     * Handles a request with the proper handler.
276
     *
277
     * Decides on which handler to use based on the request format.
278
     *
279
     * @param View    $view
280
     * @param Request $request
281
     *
282
     * @throws UnsupportedMediaTypeHttpException
283
     *
284
     * @return Response
285
     */
286 34
    public function handle(View $view, Request $request = null)
287
    {
288 34
        if (null === $request) {
289 7
            $request = $this->requestStack->getCurrentRequest();
290 7
        }
291
292 34
        $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...
293
294 34
        if (!$this->supports($format)) {
295 1
            $msg = "Format '$format' not supported, handler must be implemented";
296 1
            throw new UnsupportedMediaTypeHttpException($msg);
297
        }
298
299 33
        if (isset($this->customHandlers[$format])) {
300 10
            return call_user_func($this->customHandlers[$format], $this, $view, $request, $format);
301
        }
302
303 23
        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...
304
    }
305
306
    /**
307
     * Creates the Response from the view.
308
     *
309
     * @param View   $view
310
     * @param string $location
311
     * @param string $format
312
     *
313
     * @return Response
314
     */
315 8
    public function createRedirectResponse(View $view, $location, $format)
316
    {
317 8
        $content = null;
318 8
        if (($view->getStatusCode() == Response::HTTP_CREATED || $view->getStatusCode() == Response::HTTP_ACCEPTED) && $view->getData() != null) {
319 1
            $response = $this->initResponse($view, $format);
320 1
        } else {
321 7
            $response = $view->getResponse();
322 7
            if ('html' === $format && isset($this->forceRedirects[$format])) {
323 2
                $redirect = new RedirectResponse($location);
324 2
                $content = $redirect->getContent();
325 2
                $response->setContent($content);
326 2
            }
327
        }
328
329 8
        $code = isset($this->forceRedirects[$format])
330 8
            ? $this->forceRedirects[$format] : $this->getStatusCode($view, $content);
331
332 8
        $response->setStatusCode($code);
333 8
        $response->headers->set('Location', $location);
334
335 8
        return $response;
336
    }
337
338
    /**
339
     * Renders the view data with the given template.
340
     *
341
     * @param View   $view
342
     * @param string $format
343
     *
344
     * @return string
345
     */
346 13
    public function renderTemplate(View $view, $format)
347
    {
348 13
        $data = $this->prepareTemplateParameters($view);
349
350 13
        $template = $view->getTemplate();
351 13
        if ($template instanceof TemplateReferenceInterface) {
0 ignored issues
show
Bug introduced by
The class Symfony\Bundle\Framework...plateReferenceInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
352
            if (null === $template->get('format')) {
353
                $template->set('format', $format);
354
            }
355
356
            if (null === $template->get('engine')) {
357
                $engine = $view->getEngine() ?: $this->defaultEngine;
358
                $template->set('engine', $engine);
359
            }
360
        }
361
362 13
        return $this->templating->render($template, $data);
0 ignored issues
show
Bug introduced by
It seems like $template defined by $view->getTemplate() on line 350 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...
363
    }
364
365
    /**
366
     * Prepares view data for use by templating engine.
367
     *
368
     * @param View $view
369
     *
370
     * @return array
371
     */
372 20
    public function prepareTemplateParameters(View $view)
373
    {
374 20
        $data = $view->getData();
375
376 20
        if ($data instanceof FormInterface) {
377 2
            $data = [$view->getTemplateVar() => $data->getData(), 'form' => $data];
378 20
        } elseif (empty($data) || !is_array($data) || is_numeric((key($data)))) {
379 11
            $data = [$view->getTemplateVar() => $data];
380 11
        }
381
382 20
        if (isset($data['form']) && $data['form'] instanceof FormInterface) {
383 2
            $data['form'] = $data['form']->createView();
384 2
        }
385
386 20
        $templateData = $view->getTemplateData();
387 20
        if (is_callable($templateData)) {
388 2
            $templateData = call_user_func($templateData, $this, $view);
389 2
        }
390
391 20
        return array_merge($data, $templateData);
392
    }
393
394
    /**
395
     * Handles creation of a Response using either redirection or the templating/serializer service.
396
     *
397
     * @param View    $view
398
     * @param Request $request
399
     * @param string  $format
400
     *
401
     * @return Response
402
     */
403 45
    public function createResponse(View $view, Request $request, $format)
404
    {
405 45
        $route = $view->getRoute();
406
407
        $location = $route
408 45
            ? $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
409 45
            : $view->getLocation();
410
411 45
        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...
412 8
            return $this->createRedirectResponse($view, $location, $format);
413
        }
414
415 37
        $response = $this->initResponse($view, $format);
416
417 37
        if (!$response->headers->has('Content-Type')) {
418 37
            $response->headers->set('Content-Type', $request->getMimeType($format));
419 37
        }
420
421 37
        return $response;
422
    }
423
424
    /**
425
     * Initializes a response object that represents the view and holds the view's status code.
426
     *
427
     * @param View   $view
428
     * @param string $format
429
     *
430
     * @return Response
431
     */
432 38
    private function initResponse(View $view, $format)
433
    {
434 38
        $content = null;
435 38
        if ($this->isFormatTemplating($format)) {
436 13
            $content = $this->renderTemplate($view, $format);
437 38
        } elseif ($this->serializeNull || null !== $view->getData()) {
438 24
            $data = $this->getDataFromView($view);
439
440 24
            $context = $this->getSerializationContext($view);
441 24
            $content = $this->serializer->serialize($data, $format, $context);
442 24
        }
443
444 38
        $response = $view->getResponse();
445 38
        $response->setStatusCode($this->getStatusCode($view, $content));
446
447 38
        if (null !== $content) {
448 32
            $response->setContent($content);
449 32
        }
450
451 38
        return $response;
452
    }
453
454
    /**
455
     * Returns the form from the given view if present, false otherwise.
456
     *
457
     * @param View $view
458
     *
459
     * @return bool|FormInterface
460
     */
461 49
    protected function getFormFromView(View $view)
462
    {
463 49
        $data = $view->getData();
464
465 49
        if ($data instanceof FormInterface) {
466 7
            return $data;
467
        }
468
469 42
        if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
470 4
            return $data['form'];
471
        }
472
473 38
        return false;
474
    }
475
476
    /**
477
     * Returns the data from a view. If the data is form with errors, it will return it wrapped in an ExceptionWrapper.
478
     *
479
     * @param View $view
480
     *
481
     * @return mixed|null
482
     */
483 24
    private function getDataFromView(View $view)
484
    {
485 24
        $form = $this->getFormFromView($view);
486
487 24
        if (false === $form) {
488 18
            return $view->getData();
489
        }
490
491 6
        if ($form->isValid() || !$form->isSubmitted()) {
492
            return $form;
493
        }
494
495 6
        return $this->exceptionWrapperHandler->wrap(
496
            [
497 6
                 'status_code' => $this->failedValidationCode,
498 6
                 'message' => 'Validation Failed',
499 6
                 'errors' => $form,
500
            ]
501 6
        );
502
    }
503
}
504