Completed
Pull Request — master (#500)
by
unknown
01:44
created

ResponseCallStrategy::__invoke()   A

Complexity

Conditions 3
Paths 7

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
cc 3
nc 7
nop 3
1
<?php
2
3
namespace Mpociot\ApiDoc\Tools\ResponseStrategies;
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\Traits\ParamHelpers;
11
use Mpociot\ApiDoc\Tools\Utils;
12
13
/**
14
 * Make a call to the route and retrieve its response.
15
 */
16
class ResponseCallStrategy
17
{
18
    use ParamHelpers;
19
20
    /**
21
     * @param Route $route
22
     * @param array $tags
23
     * @param array $routeProps
24
     *
25
     * @return array|null
26
     */
27
    public function __invoke(Route $route, array $tags, array $routeProps)
28
    {
29
        $rulesToApply = $routeProps['rules']['response_calls'] ?? [];
30
        if (! $this->shouldMakeApiCall($route, $rulesToApply)) {
31
            return;
32
        }
33
34
        $this->configureEnvironment($rulesToApply);
35
        $request = $this->prepareRequest($route, $rulesToApply, $routeProps['body'], $routeProps['query']);
36
37
        try {
38
            $response = [$this->makeApiCall($request)];
39
        } catch (\Exception $e) {
40
            $response = null;
41
        } finally {
42
            $this->finish();
43
        }
44
45
        return $response;
46
    }
47
48
    /**
49
     * @param array $rulesToApply
50
     *
51
     * @return void
52
     */
53
    private function configureEnvironment(array $rulesToApply)
54
    {
55
        $this->startDbTransaction();
56
        $this->setEnvironmentVariables($rulesToApply['env'] ?? []);
0 ignored issues
show
Deprecated Code introduced by
The method Mpociot\ApiDoc\Tools\Res...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...
57
        $this->setLaravelConfigs($rulesToApply['config'] ?? []);
58
    }
59
60
    /**
61
     * @param Route $route
62
     * @param array $rulesToApply
63
     * @param array $bodyParams
64
     * @param array $queryParams
65
     *
66
     * @return Request
67
     */
68
    private function prepareRequest(Route $route, array $rulesToApply, array $bodyParams, array $queryParams)
69
    {
70
        $uri = Utils::getFullUrl($route, $rulesToApply['bindings'] ?? []);
71
        $routeMethods = $this->getMethods($route);
72
        $method = array_shift($routeMethods);
73
        $cookies = isset($rulesToApply['cookies']) ? $rulesToApply['cookies'] : [];
74
        $request = Request::create($uri, $method, [], $cookies, [], $this->transformHeadersToServerVars($rulesToApply['headers'] ?? []));
75
        $request = $this->addHeaders($request, $route, $rulesToApply['headers'] ?? []);
76
77
        // Mix in parsed parameters with manually specified parameters.
78
        $queryParams = collect($this->cleanParams($queryParams))->merge($rulesToApply['query'] ?? [])->toArray();
79
        $bodyParams = collect($this->cleanParams($bodyParams))->merge($rulesToApply['body'] ?? [])->toArray();
80
81
        $request = $this->addQueryParameters($request, $queryParams);
82
        $request = $this->addBodyParameters($request, $bodyParams);
83
84
        return $request;
85
    }
86
87
88
    /**
89
     * @param array $config
0 ignored issues
show
Bug introduced by
There is no parameter named $config. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

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