ResponseCalls   B
last analyzed

Complexity

Total Complexity 43

Size/Duplication

Total Lines 359
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Importance

Changes 0
Metric Value
wmc 43
lcom 2
cbo 4
dl 0
loc 359
rs 8.96
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __invoke() 0 37 4
A configureEnvironment() 0 6 1
A prepareRequest() 0 23 2
A setEnvironmentVariables() 0 9 2
A setLaravelConfigs() 0 10 3
A startDbTransaction() 0 7 2
A endDbTransaction() 0 7 2
A finish() 0 4 1
A callDingoRoute() 0 29 4
A getMethods() 0 4 1
A addHeaders() 0 21 4
A addQueryParameters() 0 7 1
A addBodyParameters() 0 6 1
A makeApiCall() 0 10 2
A callLaravelRoute() 0 15 2
B shouldMakeApiCall() 0 30 7
A transformHeadersToServerVars() 0 14 4

How to fix   Complexity   

Complex Class

Complex classes like ResponseCalls 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 ResponseCalls, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Mpociot\ApiDoc\Extracting\Strategies\Responses;
4
5
use Dingo\Api\Dispatcher;
6
use Illuminate\Http\Request;
7
use Illuminate\Http\Response;
8
use Illuminate\Routing\Route;
9
use Illuminate\Support\Str;
10
use Mpociot\ApiDoc\Extracting\ParamHelpers;
11
use Mpociot\ApiDoc\Extracting\Strategies\Strategy;
12
use Mpociot\ApiDoc\Tools\Flags;
13
use Mpociot\ApiDoc\Tools\Utils;
14
15
/**
16
 * Make a call to the route and retrieve its response.
17
 */
18
class ResponseCalls extends Strategy
19
{
20
    use ParamHelpers;
21
22
    /**
23
     * @param Route $route
24
     * @param \ReflectionClass $controller
25
     * @param \ReflectionMethod $method
26
     * @param array $routeRules
27
     * @param array $context
28
     *
29
     * @return array|null
30
     */
31
    public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $routeRules, array $context = [])
32
    {
33
        $rulesToApply = $routeRules['response_calls'] ?? [];
34
        if (! $this->shouldMakeApiCall($route, $rulesToApply, $context)) {
35
            return null;
36
        }
37
38
        $this->configureEnvironment($rulesToApply);
39
40
        // Mix in parsed parameters with manually specified parameters.
41
        $bodyParameters = array_merge($context['cleanBodyParameters'], $rulesToApply['bodyParams'] ?? []);
42
        $queryParameters = array_merge($context['cleanQueryParameters'], $rulesToApply['queryParams'] ?? []);
43
        $urlParameters = $context['cleanUrlParameters'];
44
        $request = $this->prepareRequest($route, $rulesToApply, $urlParameters, $bodyParameters, $queryParameters, $context['headers'] ?? []);
45
46
        try {
47
            $response = $this->makeApiCall($request);
48
            $response = [
49
                [
50
                    'status' => $response->getStatusCode(),
51
                    'content' => $response->getContent(),
52
                ],
53
            ];
54
        } catch (\Exception $e) {
55
            echo 'Exception thrown during response call for [' . implode(',', $route->methods) . "] {$route->uri}.\n";
56
            if (Flags::$shouldBeVerbose) {
57
                Utils::dumpException($e);
58
            } else {
59
                echo "Run this again with the --verbose flag to see the exception.\n";
60
            }
61
            $response = null;
62
        } finally {
63
            $this->finish();
64
        }
65
66
        return $response;
67
    }
68
69
    /**
70
     * @param array $rulesToApply
71
     *
72
     * @return void
73
     */
74
    private function configureEnvironment(array $rulesToApply)
75
    {
76
        $this->startDbTransaction();
77
        $this->setEnvironmentVariables($rulesToApply['env'] ?? []);
0 ignored issues
show
Deprecated Code introduced by
The method Mpociot\ApiDoc\Extractin...tEnvironmentVariables() has been deprecated with message: Not guaranteed to overwrite application's env. Use Laravel config variables instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
78
        $this->setLaravelConfigs($rulesToApply['config'] ?? []);
79
    }
80
81
    /**
82
     * @param Route $route
83
     * @param array $rulesToApply
84
     * @param array $bodyParams
85
     * @param array $queryParams
86
     *
87
     * @return Request
88
     */
89
    protected function prepareRequest(Route $route, array $rulesToApply, array $urlParams, array $bodyParams, array $queryParams, array $headers)
90
    {
91
        $uri = Utils::getFullUrl($route, $urlParams);
92
        $routeMethods = $this->getMethods($route);
93
        $method = array_shift($routeMethods);
94
        $cookies = isset($rulesToApply['cookies']) ? $rulesToApply['cookies'] : [];
95
96
        // Note that we initialise the request with the bodyPatams here
97
        // and later still add them to the ParameterBag (`addBodyParameters`)
98
        // The first is so the body params get added to the request content
99
        // (where Laravel reads body from)
100
        // The second is so they get added to the request bag
101
        // (where Symfony usually reads from and Laravel sometimes does)
102
        // Adding to both ensures consistency
103
        $request = Request::create($uri, $method, [], $cookies, [], $this->transformHeadersToServerVars($headers), json_encode($bodyParams));
104
        // Doing it again to catch any ones we didn't transform properly.
105
        $request = $this->addHeaders($request, $route, $headers);
106
107
        $request = $this->addQueryParameters($request, $queryParams);
108
        $request = $this->addBodyParameters($request, $bodyParams);
109
110
        return $request;
111
    }
112
113
    /**
114
     * @param array $env
115
     *
116
     * @return void
117
     *
118
     * @deprecated Not guaranteed to overwrite application's env. Use Laravel config variables instead.
119
     */
120
    private function setEnvironmentVariables(array $env)
121
    {
122
        foreach ($env as $name => $value) {
123
            putenv("$name=$value");
124
125
            $_ENV[$name] = $value;
126
            $_SERVER[$name] = $value;
127
        }
128
    }
129
130
    /**
131
     * @param array $config
132
     *
133
     * @return void
134
     */
135
    private function setLaravelConfigs(array $config)
136
    {
137
        if (empty($config)) {
138
            return;
139
        }
140
141
        foreach ($config as $name => $value) {
142
            config([$name => $value]);
143
        }
144
    }
145
146
    /**
147
     * @return void
148
     */
149
    private function startDbTransaction()
150
    {
151
        try {
152
            app('db')->beginTransaction();
153
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
154
        }
155
    }
156
157
    /**
158
     * @return void
159
     */
160
    private function endDbTransaction()
161
    {
162
        try {
163
            app('db')->rollBack();
164
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
165
        }
166
    }
167
168
    /**
169
     * @return void
170
     */
171
    private function finish()
172
    {
173
        $this->endDbTransaction();
174
    }
175
176
    /**
177
     * @param Request $request
178
     *
179
     * @return \Illuminate\Http\JsonResponse|mixed
180
     */
181
    public function callDingoRoute(Request $request)
182
    {
183
        /** @var Dispatcher $dispatcher */
184
        $dispatcher = app(\Dingo\Api\Dispatcher::class);
185
186
        foreach ($request->headers as $header => $value) {
187
            $dispatcher->header($header, $value);
188
        }
189
190
        // set domain and body parameters
191
        $dispatcher->on($request->header('SERVER_NAME'))
192
            ->with($request->request->all());
193
194
        // set URL and query parameters
195
        $uri = $request->getRequestUri();
196
        $query = $request->getQueryString();
197
        if (! empty($query)) {
198
            $uri .= "?$query";
199
        }
200
        $response = call_user_func_array([$dispatcher, strtolower($request->method())], [$uri]);
201
202
        // the response from the Dingo dispatcher is the 'raw' response from the controller,
203
        // so we have to ensure it's JSON first
204
        if (! $response instanceof Response) {
0 ignored issues
show
Bug introduced by
The class Illuminate\Http\Response 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...
205
            $response = response()->json($response);
206
        }
207
208
        return $response;
209
    }
210
211
    /**
212
     * @param Route $route
213
     *
214
     * @return array
215
     */
216
    public function getMethods(Route $route)
217
    {
218
        return array_diff($route->methods(), ['HEAD']);
219
    }
220
221
    /**
222
     * @param Request $request
223
     * @param Route $route
224
     * @param array|null $headers
225
     *
226
     * @return Request
227
     */
228
    private function addHeaders(Request $request, Route $route, $headers)
229
    {
230
        // set the proper domain
231
        if ($route->getDomain()) {
232
            $request->headers->add([
233
                'HOST' => $route->getDomain(),
234
            ]);
235
            $request->server->add([
236
                'HTTP_HOST' => $route->getDomain(),
237
                'SERVER_NAME' => $route->getDomain(),
238
            ]);
239
        }
240
241
        $headers = collect($headers);
242
243
        if (($headers->get('Accept') ?: $headers->get('accept')) === 'application/json') {
244
            $request->setRequestFormat('json');
245
        }
246
247
        return $request;
248
    }
249
250
    /**
251
     * @param Request $request
252
     * @param array $query
253
     *
254
     * @return Request
255
     */
256
    private function addQueryParameters(Request $request, array $query)
257
    {
258
        $request->query->add($query);
259
        $request->server->add(['QUERY_STRING' => http_build_query($query)]);
260
261
        return $request;
262
    }
263
264
    /**
265
     * @param Request $request
266
     * @param array $body
267
     *
268
     * @return Request
269
     */
270
    private function addBodyParameters(Request $request, array $body)
271
    {
272
        $request->request->add($body);
273
274
        return $request;
275
    }
276
277
    /**
278
     * @param Request $request
279
     *
280
     * @throws \Exception
281
     *
282
     * @return \Illuminate\Http\JsonResponse|mixed|\Symfony\Component\HttpFoundation\Response
283
     */
284
    protected function makeApiCall(Request $request)
285
    {
286
        if (config('apidoc.router') == 'dingo') {
287
            $response = $this->callDingoRoute($request);
288
        } else {
289
            $response = $this->callLaravelRoute($request);
290
        }
291
292
        return $response;
293
    }
294
295
    /**
296
     * @param Request $request
297
     *
298
     * @throws \Exception
299
     *
300
     * @return \Symfony\Component\HttpFoundation\Response
301
     */
302
    protected function callLaravelRoute(Request $request): \Symfony\Component\HttpFoundation\Response
303
    {
304
        // Confirm we're running in Laravel, not Lumen
305
        if (app()->bound(\Illuminate\Contracts\Http\Kernel::class)) {
306
            $kernel = app(\Illuminate\Contracts\Http\Kernel::class);
307
            $response = $kernel->handle($request);
308
            $kernel->terminate($request, $response);
309
        } else {
310
            // Handle the request using the Lumen application.
311
            $kernel = app();
312
            $response = $kernel->handle($request);
313
        }
314
315
        return $response;
316
    }
317
318
    /**
319
     * @param Route $route
320
     * @param array $rulesToApply
321
     *
322
     * @return bool
323
     */
324
    protected function shouldMakeApiCall(Route $route, array $rulesToApply, array $context): bool
325
    {
326
        $allowedMethods = $rulesToApply['methods'] ?? [];
327
        if (empty($allowedMethods)) {
328
            return false;
329
        }
330
331
        // Don't attempt a response call if there are already successful responses
332
        $successResponses = collect($context['responses'])->filter(function ($response) {
333
            return ((string) $response['status'])[0] == '2';
334
        })->count();
335
        if ($successResponses) {
336
            return false;
337
        }
338
339
        if (is_string($allowedMethods) && $allowedMethods == '*') {
340
            return true;
341
        }
342
343
        if (array_search('*', $allowedMethods) !== false) {
344
            return true;
345
        }
346
347
        $routeMethods = $this->getMethods($route);
348
        if (in_array(array_shift($routeMethods), $allowedMethods)) {
349
            return true;
350
        }
351
352
        return false;
353
    }
354
355
    /**
356
     * Transform headers array to array of $_SERVER vars with HTTP_* format.
357
     *
358
     * @param  array  $headers
359
     *
360
     * @return array
361
     */
362
    protected function transformHeadersToServerVars(array $headers)
363
    {
364
        $server = [];
365
        $prefix = 'HTTP_';
366
        foreach ($headers as $name => $value) {
367
            $name = strtr(strtoupper($name), '-', '_');
368
            if (! Str::startsWith($name, $prefix) && $name !== 'CONTENT_TYPE') {
369
                $name = $prefix . $name;
370
            }
371
            $server[$name] = $value;
372
        }
373
374
        return $server;
375
    }
376
}
377