Completed
Pull Request — master (#638)
by
unknown
12:52
created

ResponseCalls::prepareRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 2
nc 2
nop 6
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'))
0 ignored issues
show
Bug introduced by
It seems like $request->header('SERVER_NAME') targeting Illuminate\Http\Concerns...actsWithInput::header() can also be of type array or null; however, Dingo\Api\Dispatcher::on() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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) {
197
            $response = response()->json($response);
0 ignored issues
show
Bug introduced by
The method json does only exist in Illuminate\Contracts\Routing\ResponseFactory, but not in Illuminate\Http\Response.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
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()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $route->getDomain() 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...
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