Completed
Pull Request — master (#570)
by Marcel
01:55
created

ResponseCalls   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 333
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Importance

Changes 0
Metric Value
wmc 42
lcom 2
cbo 4
dl 0
loc 333
rs 9.0399
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __invoke() 0 30 4
A configureEnvironment() 0 6 1
A prepareRequest() 0 14 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 8 1
B shouldMakeApiCall() 0 27 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\Strategies\Responses;
4
5
use Dingo\Api\Dispatcher;
6
use Illuminate\Support\Str;
7
use Illuminate\Http\Request;
8
use Illuminate\Http\Response;
9
use Illuminate\Routing\Route;
10
use Mpociot\ApiDoc\Tools\Flags;
11
use Mpociot\ApiDoc\Tools\Utils;
12
use Mpociot\ApiDoc\Strategies\Strategy;
13
use Mpociot\ApiDoc\Tools\Traits\ParamHelpers;
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['body'] ?? []);
42
        $queryParameters = array_merge($context['cleanQueryParameters'], $rulesToApply['query'] ?? []);
43
        $request = $this->prepareRequest($route, $rulesToApply, $bodyParameters, $queryParameters);
44
45
        try {
46
            $response = [200 => $this->makeApiCall($request)->getContent()];
47
        } catch (\Exception $e) {
48
            echo 'Exception thrown during response call for ['.implode(',', $route->methods)."] {$route->uri}.\n";
49
            if (Flags::$shouldBeVerbose) {
50
                Utils::dumpException($e);
51
            } else {
52
                echo "Run this again with the --verbose flag to see the exception.\n";
53
            }
54
            $response = null;
55
        } finally {
56
            $this->finish();
57
        }
58
59
        return $response;
60
    }
61
62
    /**
63
     * @param array $rulesToApply
64
     *
65
     * @return void
66
     */
67
    private function configureEnvironment(array $rulesToApply)
68
    {
69
        $this->startDbTransaction();
70
        $this->setEnvironmentVariables($rulesToApply['env'] ?? []);
0 ignored issues
show
Deprecated Code introduced by
The method Mpociot\ApiDoc\Strategie...tEnvironmentVariables() has been deprecated with message: in favour of Laravel config variables

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