Completed
Push — master ( 0ed20a...e8c824 )
by
unknown
10:05 queued 08:45
created

ResponseCalls::shouldMakeApiCall()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

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