Completed
Pull Request — 2.x (#2288)
by Guilhem
17:20
created

ViewHandler   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 99.1%

Importance

Changes 0
Metric Value
wmc 52
lcom 1
cbo 11
dl 0
loc 280
ccs 110
cts 111
cp 0.991
rs 7.44
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 24 1
A create() 0 12 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
A createRedirectResponse() 0 16 4
A createResponse() 0 25 5
A getStatusCode() 0 15 6
B getSerializationContext() 0 23 9
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\Request;
18
use Symfony\Component\HttpFoundation\RequestStack;
19
use Symfony\Component\HttpFoundation\Response;
20
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
21
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
22
23
/**
24
 * View may be used in controllers to build up a response in a format agnostic way
25
 * The View class takes care of encoding your data in json, xml via the Serializer
26
 * component.
27
 *
28
 * @author Jordi Boggiano <[email protected]>
29
 * @author Lukas K. Smith <[email protected]>
30
 */
31
final class ViewHandler implements ConfigurableViewHandlerInterface
32
{
33
    /**
34
     * Key format, value a callable that returns a Response instance.
35
     *
36
     * @var array
37
     */
38
    private $customHandlers = [];
39
40
    /**
41
     * The supported formats as keys.
42
     *
43
     * @var array
44
     */
45
    private $formats;
46
    private $failedValidationCode;
47
    private $emptyContentCode;
48
    private $serializeNull;
49
    private $exclusionStrategyGroups = [];
50
    private $exclusionStrategyVersion;
51
    private $serializeNullStrategy;
52
    private $urlGenerator;
53
    private $serializer;
54
    private $requestStack;
55
    private $options;
56
57 54
    private function __construct(
58
        UrlGeneratorInterface $urlGenerator,
59
        Serializer $serializer,
60
        RequestStack $requestStack,
61
        array $formats = null,
62
        int $failedValidationCode = Response::HTTP_BAD_REQUEST,
63
        int $emptyContentCode = Response::HTTP_NO_CONTENT,
64
        bool $serializeNull = false,
65
        array $options = []
66
    ) {
67 54
        $this->urlGenerator = $urlGenerator;
68 54
        $this->serializer = $serializer;
69 54
        $this->requestStack = $requestStack;
70 54
        $this->formats = (array) $formats;
71 54
        $this->failedValidationCode = $failedValidationCode;
72 54
        $this->emptyContentCode = $emptyContentCode;
73 54
        $this->serializeNull = $serializeNull;
74
        $this->options = $options + [
75 54
            'exclusionStrategyGroups' => [],
76
            'exclusionStrategyVersion' => null,
77
            'serializeNullStrategy' => null,
78
            ];
79 54
        $this->reset();
80 54
    }
81
82 54
    public static function create(
83
        UrlGeneratorInterface $urlGenerator,
84
        Serializer $serializer,
85
        RequestStack $requestStack,
86
        array $formats = null,
87
        int $failedValidationCode = Response::HTTP_BAD_REQUEST,
88
        int $emptyContentCode = Response::HTTP_NO_CONTENT,
89
        bool $serializeNull = false,
90
        array $options = []
91
    ): self {
92 54
        return new self($urlGenerator, $serializer, $requestStack, $formats, $failedValidationCode, $emptyContentCode, $serializeNull, $options);
93
    }
94
95
    /**
96
     * @param string[]|string $groups
97
     */
98 1
    public function setExclusionStrategyGroups($groups): void
99
    {
100 1
        $this->exclusionStrategyGroups = (array) $groups;
101 1
    }
102
103 8
    public function setExclusionStrategyVersion(string $version): void
104
    {
105 8
        $this->exclusionStrategyVersion = $version;
106 8
    }
107
108 3
    public function setSerializeNullStrategy(bool $isEnabled): void
109
    {
110 3
        $this->serializeNullStrategy = $isEnabled;
111 3
    }
112
113
    /**
114
     * {@inheritdoc}
115
     */
116 32
    public function supports(string $format): bool
117
    {
118 32
        return isset($this->customHandlers[$format]) || isset($this->formats[$format]);
119
    }
120
121
    /**
122
     * Registers a custom handler.
123
     *
124
     * The handler must have the following signature: handler(ViewHandler $viewHandler, View $view, Request $request, $format)
125
     * It can use the public methods of this class to retrieve the needed data and return a
126
     * Response object ready to be sent.
127
     */
128 14
    public function registerHandler(string $format, callable $callable): void
129
    {
130 14
        $this->customHandlers[$format] = $callable;
131 14
    }
132
133
    /**
134
     * Handles a request with the proper handler.
135
     *
136
     * Decides on which handler to use based on the request format.
137
     *
138
     * @throws UnsupportedMediaTypeHttpException
139
     */
140 28
    public function handle(View $view, Request $request = null): Response
141
    {
142 28
        if (null === $request) {
143 2
            $request = $this->requestStack->getCurrentRequest();
144
        }
145
146 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...
147
148 28
        if (!$this->supports($format)) {
149 1
            $msg = "Format '$format' not supported, handler must be implemented";
150
151 1
            throw new UnsupportedMediaTypeHttpException($msg);
152
        }
153
154 27
        if (isset($this->customHandlers[$format])) {
155 10
            return call_user_func($this->customHandlers[$format], $this, $view, $request, $format);
156
        }
157
158 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...
159
    }
160
161 4
    public function createRedirectResponse(View $view, string $location, string $format): Response
162
    {
163 4
        $content = null;
164 4
        if ((Response::HTTP_CREATED === $view->getStatusCode() || Response::HTTP_ACCEPTED === $view->getStatusCode()) && null !== $view->getData()) {
165 1
            $response = $this->initResponse($view, $format);
166
        } else {
167 3
            $response = $view->getResponse();
168
        }
169
170 4
        $code = $this->getStatusCode($view, $content);
171
172 4
        $response->setStatusCode($code);
173 4
        $response->headers->set('Location', $location);
174
175 4
        return $response;
176
    }
177
178 41
    public function createResponse(View $view, Request $request, string $format): Response
179
    {
180 41
        $route = $view->getRoute();
181
182 41
        $location = $route
183 2
            ? $this->urlGenerator->generate($route, (array) $view->getRouteParameters(), UrlGeneratorInterface::ABSOLUTE_URL)
184 41
            : $view->getLocation();
185
186 41
        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...
187 4
            return $this->createRedirectResponse($view, $location, $format);
188
        }
189
190 37
        $response = $this->initResponse($view, $format);
191
192 37
        if (!$response->headers->has('Content-Type')) {
193 37
            $mimeType = $request->attributes->get('media_type');
194 37
            if (null === $mimeType) {
195 30
                $mimeType = $request->getMimeType($format);
196
            }
197
198 37
            $response->headers->set('Content-Type', $mimeType);
199
        }
200
201 37
        return $response;
202
    }
203
204
    /**
205
     * Gets a response HTTP status code from a View instance.
206
     *
207
     * By default it will return 200. However if there is a FormInterface stored for
208
     * the key 'form' in the View's data it will return the failed_validation
209
     * configuration if the form instance has errors.
210
     *
211
     * @param string|false|null
212
     */
213 41
    private function getStatusCode(View $view, $content = null): int
214
    {
215 41
        $form = $this->getFormFromView($view);
216
217 41
        if (null !== $form && $form->isSubmitted() && !$form->isValid()) {
218 8
            return $this->failedValidationCode;
219
        }
220
221 33
        $statusCode = $view->getStatusCode();
222 33
        if (null !== $statusCode) {
223 7
            return $statusCode;
224
        }
225
226 26
        return null !== $content ? Response::HTTP_OK : $this->emptyContentCode;
227
    }
228
229 35
    private function getSerializationContext(View $view): Context
230
    {
231 35
        $context = $view->getContext();
232
233 35
        $groups = $context->getGroups();
234 35
        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...
235 1
            $context->setGroups($this->exclusionStrategyGroups);
236
        }
237
238 35
        if (null === $context->getVersion() && $this->exclusionStrategyVersion) {
239 8
            $context->setVersion($this->exclusionStrategyVersion);
240
        }
241
242 35
        if (null === $context->getSerializeNull() && null !== $this->serializeNullStrategy) {
243 14
            $context->setSerializeNull($this->serializeNullStrategy);
244
        }
245
246 35
        if (null !== $view->getStatusCode() && !$context->hasAttribute('status_code')) {
247 4
            $context->setAttribute('status_code', $view->getStatusCode());
248
        }
249
250 35
        return $context;
251
    }
252
253 38
    private function initResponse(View $view, string $format): Response
254
    {
255 38
        $content = null;
256 38
        if ($this->serializeNull || null !== $view->getData()) {
257 32
            $data = $this->getDataFromView($view);
258
259 32
            if ($data instanceof FormInterface && $data->isSubmitted() && !$data->isValid()) {
260 8
                $view->getContext()->setAttribute('status_code', $this->failedValidationCode);
261
            }
262
263 32
            $context = $this->getSerializationContext($view);
264
265 32
            $content = $this->serializer->serialize($data, $format, $context);
266
        }
267
268 38
        $response = $view->getResponse();
269 38
        $response->setStatusCode($this->getStatusCode($view, $content));
270
271 38
        if (null !== $content) {
272 27
            $response->setContent($content);
273
        }
274
275 38
        return $response;
276
    }
277
278 41
    private function getFormFromView(View $view): ?FormInterface
279
    {
280 41
        $data = $view->getData();
281
282 41
        if ($data instanceof FormInterface) {
283 6
            return $data;
284
        }
285
286 35
        if (is_array($data) && isset($data['form']) && $data['form'] instanceof FormInterface) {
287 5
            return $data['form'];
288
        }
289
290 30
        return null;
291
    }
292
293 32
    private function getDataFromView(View $view)
294
    {
295 32
        $form = $this->getFormFromView($view);
296
297 32
        if (null === $form) {
298 21
            return $view->getData();
299
        }
300
301 11
        return $form;
302
    }
303
304 54
    public function reset(): void
305
    {
306 54
        $this->exclusionStrategyGroups = $this->options['exclusionStrategyGroups'];
307 54
        $this->exclusionStrategyVersion = $this->options['exclusionStrategyVersion'];
308 54
        $this->serializeNullStrategy = $this->options['serializeNullStrategy'];
309 54
    }
310
}
311