Completed
Push — master ( 991fcc...fc2666 )
by Christian
17s queued 11s
created

ViewHandler   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 297
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 97.44%

Importance

Changes 0
Metric Value
wmc 54
lcom 1
cbo 12
dl 0
loc 297
ccs 114
cts 117
cp 0.9744
rs 6.4799
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 26 1
A create() 0 13 1
A setExclusionStrategyGroups() 0 4 1
A setExclusionStrategyVersion() 0 4 1
A setSerializeNullStrategy() 0 4 1
A supports() 0 4 2
A registerHandler() 0 4 1
A handle() 0 20 5
B createRedirectResponse() 0 22 7
A createResponse() 0 25 5
A getStatusCode() 0 15 6
B getSerializationContext() 0 23 8
B initResponse() 0 24 7
A getFormFromView() 0 14 5
A getDataFromView() 0 10 2
A reset() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like ViewHandler often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ViewHandler, and based on these observations, apply Extract Interface, too.

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\Component\Form\FormInterface;
17
use Symfony\Component\HttpFoundation\RedirectResponse;
18
use Symfony\Component\HttpFoundation\Request;
19
use Symfony\Component\HttpFoundation\RequestStack;
20
use Symfony\Component\HttpFoundation\Response;
21
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
22
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
23
24
/**
25
 * View may be used in controllers to build up a response in a format agnostic way
26
 * The View class takes care of encoding your data in json, xml via the Serializer
27
 * component.
28
 *
29
 * @author Jordi Boggiano <[email protected]>
30
 * @author Lukas K. Smith <[email protected]>
31
 */
32
final class ViewHandler implements ConfigurableViewHandlerInterface
33
{
34
    /**
35
     * Key format, value a callable that returns a Response instance.
36
     *
37
     * @var array
38
     */
39
    private $customHandlers = [];
40
41
    /**
42
     * The supported formats as keys.
43
     *
44
     * @var array
45
     */
46
    private $formats;
47
    private $failedValidationCode;
48
    private $emptyContentCode;
49
    private $serializeNull;
50
51
    /**
52
     * If to force a redirect for the given key format,
53
     * with value being the status code to use.
54
     *
55
     * @var array<string,int>
56
     */
57
    private $forceRedirects;
58
    private $exclusionStrategyGroups = [];
59
    private $exclusionStrategyVersion;
60
    private $serializeNullStrategy;
61
    private $urlGenerator;
62
    private $serializer;
63
    private $requestStack;
64
    private $options;
65
66 61
    private function __construct(
67
        UrlGeneratorInterface $urlGenerator,
68
        Serializer $serializer,
69
        RequestStack $requestStack,
70
        array $formats = null,
71
        int $failedValidationCode = Response::HTTP_BAD_REQUEST,
72
        int $emptyContentCode = Response::HTTP_NO_CONTENT,
73
        bool $serializeNull = false,
74
        array $forceRedirects = null,
75
        array $options = []
76
    ) {
77 61
        $this->urlGenerator = $urlGenerator;
78 61
        $this->serializer = $serializer;
79 61
        $this->requestStack = $requestStack;
80 61
        $this->formats = (array) $formats;
81 61
        $this->failedValidationCode = $failedValidationCode;
82 61
        $this->emptyContentCode = $emptyContentCode;
83 61
        $this->serializeNull = $serializeNull;
84 61
        $this->forceRedirects = (array) $forceRedirects;
85 61
        $this->options = $options + [
86 61
            'exclusionStrategyGroups' => [],
87
            'exclusionStrategyVersion' => null,
88
            'serializeNullStrategy' => null,
89
            ];
90 61
        $this->reset();
91 61
    }
92
93 61
    public static function create(
94
        UrlGeneratorInterface $urlGenerator,
95
        Serializer $serializer,
96
        RequestStack $requestStack,
97
        array $formats = null,
98
        int $failedValidationCode = Response::HTTP_BAD_REQUEST,
99
        int $emptyContentCode = Response::HTTP_NO_CONTENT,
100
        bool $serializeNull = false,
101
        array $options = []
102
    ): self
103
    {
104 61
        return new self($urlGenerator, $serializer, $requestStack, $formats, $failedValidationCode, $emptyContentCode, $serializeNull, [], $options, false);
0 ignored issues
show
Unused Code introduced by
The call to ViewHandler::__construct() has too many arguments starting with false.

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...
105
    }
106
107
    /**
108
     * @param string[]|string $groups
109
     */
110 1
    public function setExclusionStrategyGroups($groups): void
111
    {
112 1
        $this->exclusionStrategyGroups = (array) $groups;
113 1
    }
114
115 8
    public function setExclusionStrategyVersion(string $version): void
116
    {
117 8
        $this->exclusionStrategyVersion = $version;
118 8
    }
119
120 3
    public function setSerializeNullStrategy(bool $isEnabled): void
121
    {
122 3
        $this->serializeNullStrategy = $isEnabled;
123 3
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 39
    public function supports(string $format): bool
129
    {
130 39
        return isset($this->customHandlers[$format]) || isset($this->formats[$format]);
131
    }
132
133
    /**
134
     * Registers a custom handler.
135
     *
136
     * The handler must have the following signature: handler(ViewHandler $viewHandler, View $view, Request $request, $format)
137
     * It can use the public methods of this class to retrieve the needed data and return a
138
     * Response object ready to be sent.
139
     */
140 15
    public function registerHandler(string $format, callable $callable): void
141
    {
142 15
        $this->customHandlers[$format] = $callable;
143 15
    }
144
145
    /**
146
     * Handles a request with the proper handler.
147
     *
148
     * Decides on which handler to use based on the request format.
149
     *
150
     * @throws UnsupportedMediaTypeHttpException
151
     */
152 35
    public function handle(View $view, Request $request = null): Response
153
    {
154 35
        if (null === $request) {
155 9
            $request = $this->requestStack->getCurrentRequest();
156
        }
157
158 35
        $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...
159
160 35
        if (!$this->supports($format)) {
161 1
            $msg = "Format '$format' not supported, handler must be implemented";
162
163 1
            throw new UnsupportedMediaTypeHttpException($msg);
164
        }
165
166 34
        if (isset($this->customHandlers[$format])) {
167 10
            return call_user_func($this->customHandlers[$format], $this, $view, $request, $format);
168
        }
169
170 24
        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...
171
    }
172
173 4
    public function createRedirectResponse(View $view, string $location, string $format): Response
174
    {
175 4
        $content = null;
176 4
        if ((Response::HTTP_CREATED === $view->getStatusCode() || Response::HTTP_ACCEPTED === $view->getStatusCode()) && null !== $view->getData()) {
177 1
            $response = $this->initResponse($view, $format);
178
        } else {
179 3
            $response = $view->getResponse();
180 3
            if ('html' === $format && isset($this->forceRedirects[$format])) {
181
                $redirect = new RedirectResponse($location);
182
                $content = $redirect->getContent();
183
                $response->setContent($content);
184
            }
185
        }
186
187 4
        $code = isset($this->forceRedirects[$format])
188 4
            ? $this->forceRedirects[$format] : $this->getStatusCode($view, $content);
189
190 4
        $response->setStatusCode($code);
191 4
        $response->headers->set('Location', $location);
192
193 4
        return $response;
194
    }
195
196 40
    public function createResponse(View $view, Request $request, string $format): Response
197
    {
198 40
        $route = $view->getRoute();
199
200 40
        $location = $route
201 2
            ? $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
202 40
            : $view->getLocation();
203
204 40
        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...
205 4
            return $this->createRedirectResponse($view, $location, $format);
206
        }
207
208 36
        $response = $this->initResponse($view, $format);
209
210 36
        if (!$response->headers->has('Content-Type')) {
211 36
            $mimeType = $request->attributes->get('media_type');
212 36
            if (null === $mimeType) {
213 29
                $mimeType = $request->getMimeType($format);
214
            }
215
216 36
            $response->headers->set('Content-Type', $mimeType);
217
        }
218
219 36
        return $response;
220
    }
221
222
    /**
223
     * Gets a response HTTP status code from a View instance.
224
     *
225
     * By default it will return 200. However if there is a FormInterface stored for
226
     * the key 'form' in the View's data it will return the failed_validation
227
     * configuration if the form instance has errors.
228
     *
229
     * @param string|false|null
230
     */
231 47
    private function getStatusCode(View $view, $content = null): int
232
    {
233 47
        $form = $this->getFormFromView($view);
234
235 47
        if (null !== $form && $form->isSubmitted() && !$form->isValid()) {
236 7
            return $this->failedValidationCode;
237
        }
238
239 40
        $statusCode = $view->getStatusCode();
240 40
        if (null !== $statusCode) {
241 14
            return $statusCode;
242
        }
243
244 26
        return null !== $content ? Response::HTTP_OK : $this->emptyContentCode;
245
    }
246
247 37
    private function getSerializationContext(View $view): Context
248
    {
249 37
        $context = $view->getContext();
250
251 37
        $groups = $context->getGroups();
252 37
        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...
253 1
            $context->setGroups($this->exclusionStrategyGroups);
254
        }
255
256 37
        if (null === $context->getVersion() && $this->exclusionStrategyVersion) {
257 8
            $context->setVersion($this->exclusionStrategyVersion);
258
        }
259
260 37
        if (null === $context->getSerializeNull() && null !== $this->serializeNullStrategy) {
261 21
            $context->setSerializeNull($this->serializeNullStrategy);
262
        }
263
264 37
        if (null !== $view->getStatusCode()) {
265 11
            $context->setAttribute('status_code', $view->getStatusCode());
266
        }
267
268 37
        return $context;
269
    }
270
271 37
    private function initResponse(View $view, string $format): Response
272
    {
273 37
        $content = null;
274 37
        if ($this->serializeNull || null !== $view->getData()) {
275 34
            $data = $this->getDataFromView($view);
276
277 34
            if ($data instanceof FormInterface && $data->isSubmitted() && !$data->isValid()) {
278 6
                $view->getContext()->setAttribute('status_code', $this->failedValidationCode);
279
            }
280
281 34
            $context = $this->getSerializationContext($view);
282
283 34
            $content = $this->serializer->serialize($data, $format, $context);
284
        }
285
286 37
        $response = $view->getResponse();
287 37
        $response->setStatusCode($this->getStatusCode($view, $content));
288
289 37
        if (null !== $content) {
290 29
            $response->setContent($content);
291
        }
292
293 37
        return $response;
294
    }
295
296 47
    private function getFormFromView(View $view): ?FormInterface
297
    {
298 47
        $data = $view->getData();
299
300 47
        if ($data instanceof FormInterface) {
301 6
            return $data;
302
        }
303
304 41
        if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
305 4
            return $data['form'];
306
        }
307
308 37
        return null;
309
    }
310
311 34
    private function getDataFromView(View $view)
312
    {
313 34
        $form = $this->getFormFromView($view);
314
315 34
        if (null === $form) {
316 28
            return $view->getData();
317
        }
318
319 6
        return $form;
320
    }
321
322 61
    public function reset(): void
323
    {
324 61
        $this->exclusionStrategyGroups = $this->options['exclusionStrategyGroups'];
325 61
        $this->exclusionStrategyVersion = $this->options['exclusionStrategyVersion'];
326 61
        $this->serializeNullStrategy = $this->options['serializeNullStrategy'];
327 61
    }
328
}
329