Completed
Pull Request — master (#500)
by Marcel
03:07 queued 01:17
created

ResponseCallStrategy::prepareRequest()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
cc 2
nc 2
nop 4
1
<?php
2
3
namespace Mpociot\ApiDoc\Tools\ResponseStrategies;
4
5
use Dingo\Api\Dispatcher;
6
use Illuminate\Http\Request;
7
use Illuminate\Http\Response;
8
use Illuminate\Routing\Route;
9
use Mpociot\ApiDoc\Tools\Utils;
10
use Mpociot\ApiDoc\Tools\Traits\ParamHelpers;
11
12
/**
13
 * Make a call to the route and retrieve its response.
14
 */
15
class ResponseCallStrategy
16
{
17
    use ParamHelpers;
18
19
    /**
20
     * @param Route $route
21
     * @param array $tags
22
     * @param array $routeProps
23
     *
24
     * @return array|null
25
     */
26
    public function __invoke(Route $route, array $tags, array $routeProps)
27
    {
28
        $rulesToApply = $routeProps['rules']['response_calls'] ?? [];
29
        if (! $this->shouldMakeApiCall($route, $rulesToApply)) {
30
            return;
31
        }
32
33
        $this->configureEnvironment($rulesToApply);
34
        $request = $this->prepareRequest($route, $rulesToApply, $routeProps['body'], $routeProps['query']);
35
36
        try {
37
            $response = [$this->makeApiCall($request)];
38
        } catch (\Exception $e) {
39
            $response = null;
40
        } finally {
41
            $this->finish();
42
        }
43
44
        return $response;
45
    }
46
47
    /**
48
     * @param array $rulesToApply
49
     *
50
     * @return void
51
     */
52
    private function configureEnvironment(array $rulesToApply)
53
    {
54
        $this->startDbTransaction();
55
        $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...
56
        $this->setLaravelConfigs($rulesToApply['config'] ?? []);
57
    }
58
59
    /**
60
     * @param Route $route
61
     * @param array $rulesToApply
62
     * @param array $bodyParams
63
     * @param array $queryParams
64
     *
65
     * @return Request
66
     */
67
    private function prepareRequest(Route $route, array $rulesToApply, array $bodyParams, array $queryParams)
68
    {
69
        $uri = Utils::getFullUrl($route, $rulesToApply['bindings'] ?? []);
70
        $routeMethods = $this->getMethods($route);
71
        $method = array_shift($routeMethods);
72
        $cookies = isset($rulesToApply['cookies']) ? $rulesToApply['cookies'] : [];
73
        $request = Request::create($uri, $method, [], $cookies, [], $this->transformHeadersToServerVars($rulesToApply['headers'] ?? []));
74
        $request = $this->addHeaders($request, $route, $rulesToApply['headers'] ?? []);
75
76
        // Mix in parsed parameters with manually specified parameters.
77
        $queryParams = collect($this->cleanParams($queryParams))->merge($rulesToApply['query'] ?? [])->toArray();
78
        $bodyParams = collect($this->cleanParams($bodyParams))->merge($rulesToApply['body'] ?? [])->toArray();
79
80
        $request = $this->addQueryParameters($request, $queryParams);
81
        $request = $this->addBodyParameters($request, $bodyParams);
82
83
        return $request;
84
    }
85
86
    /**
87
     * @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...
88
     *
89
     * @return void
90
     *
91
     * @deprecated in favour of Laravel config variables
92
     */
93
    private function setEnvironmentVariables(array $env)
94
    {
95
        foreach ($env as $name => $value) {
96
            putenv("$name=$value");
97
98
            $_ENV[$name] = $value;
99
            $_SERVER[$name] = $value;
100
        }
101
    }
102
103
    /**
104
     * @param array $config
105
     *
106
     * @return void
107
     */
108
    private function setLaravelConfigs(array $config)
109
    {
110
        if (empty($config)) {
111
            return;
112
        }
113
114
        foreach ($config as $name => $value) {
115
            config([$name => $value]);
116
        }
117
    }
118
119
    /**
120
     * @return void
121
     */
122
    private function startDbTransaction()
123
    {
124
        try {
125
            app('db')->beginTransaction();
126
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
127
        }
128
    }
129
130
    /**
131
     * @return void
132
     */
133
    private function endDbTransaction()
134
    {
135
        try {
136
            app('db')->rollBack();
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 finish()
145
    {
146
        $this->endDbTransaction();
147
    }
148
149
    /**
150
     * @param Request $request
151
     *
152
     * @return \Illuminate\Http\JsonResponse|mixed
153
     */
154
    public function callDingoRoute(Request $request)
155
    {
156
        /** @var Dispatcher $dispatcher */
157
        $dispatcher = app(\Dingo\Api\Dispatcher::class);
158
159
        foreach ($request->headers as $header => $value) {
160
            $dispatcher->header($header, $value);
161
        }
162
163
        // set domain and body parameters
164
        $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...
165
            ->with($request->request->all());
166
167
        // set URL and query parameters
168
        $uri = $request->getRequestUri();
169
        $query = $request->getQueryString();
170
        if (! empty($query)) {
171
            $uri .= "?$query";
172
        }
173
        $response = call_user_func_array([$dispatcher, strtolower($request->method())], [$uri]);
174
175
        // the response from the Dingo dispatcher is the 'raw' response from the controller,
176
        // so we have to ensure it's JSON first
177
        if (! $response instanceof Response) {
178
            $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...
179
        }
180
181
        return $response;
182
    }
183
184
    /**
185
     * @param Route $route
186
     *
187
     * @return array
188
     */
189
    public function getMethods(Route $route)
190
    {
191
        return array_diff($route->methods(), ['HEAD']);
192
    }
193
194
    /**
195
     * @param Request $request
196
     * @param Route $route
197
     * @param array|null $headers
198
     *
199
     * @return Request
200
     */
201
    private function addHeaders(Request $request, Route $route, $headers)
202
    {
203
        // set the proper domain
204
        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...
205
            $request->headers->add([
206
                'HOST' => $route->getDomain(),
207
            ]);
208
            $request->server->add([
209
                'HTTP_HOST' => $route->getDomain(),
210
                'SERVER_NAME' => $route->getDomain(),
211
            ]);
212
        }
213
214
        $headers = collect($headers);
215
216
        if (($headers->get('Accept') ?: $headers->get('accept')) === 'application/json') {
217
            $request->setRequestFormat('json');
218
        }
219
220
        return $request;
221
    }
222
223
    /**
224
     * @param Request $request
225
     * @param array $query
226
     *
227
     * @return Request
228
     */
229
    private function addQueryParameters(Request $request, array $query)
230
    {
231
        $request->query->add($query);
232
        $request->server->add(['QUERY_STRING' => http_build_query($query)]);
233
234
        return $request;
235
    }
236
237
    /**
238
     * @param Request $request
239
     * @param array $body
240
     *
241
     * @return Request
242
     */
243
    private function addBodyParameters(Request $request, array $body)
244
    {
245
        $request->request->add($body);
246
247
        return $request;
248
    }
249
250
    /**
251
     * @param Request $request
252
     *
253
     * @throws \Exception
254
     *
255
     * @return \Illuminate\Http\JsonResponse|mixed|\Symfony\Component\HttpFoundation\Response
256
     */
257
    private function makeApiCall(Request $request)
258
    {
259
        if (config('apidoc.router') == 'dingo') {
260
            $response = $this->callDingoRoute($request);
261
        } else {
262
            $response = $this->callLaravelRoute($request);
263
        }
264
265
        return $response;
266
    }
267
268
    /**
269
     * @param Request $request
270
     *
271
     * @throws \Exception
272
     *
273
     * @return \Symfony\Component\HttpFoundation\Response
274
     */
275
    private function callLaravelRoute(Request $request): \Symfony\Component\HttpFoundation\Response
276
    {
277
        $kernel = app(\Illuminate\Contracts\Http\Kernel::class);
278
        $response = $kernel->handle($request);
279
        $kernel->terminate($request, $response);
280
281
        return $response;
282
    }
283
284
    /**
285
     * @param Route $route
286
     * @param array $rulesToApply
287
     *
288
     * @return bool
289
     */
290
    private function shouldMakeApiCall(Route $route, array $rulesToApply): bool
291
    {
292
        $allowedMethods = $rulesToApply['methods'] ?? [];
293
        if (empty($allowedMethods)) {
294
            return false;
295
        }
296
297
        if (is_string($allowedMethods) && $allowedMethods == '*') {
298
            return true;
299
        }
300
301
        if (array_search('*', $allowedMethods) !== false) {
302
            return true;
303
        }
304
305
        $routeMethods = $this->getMethods($route);
306
        if (in_array(array_shift($routeMethods), $allowedMethods)) {
307
            return true;
308
        }
309
310
        return false;
311
    }
312
313
    /**
314
     * Transform headers array to array of $_SERVER vars with HTTP_* format.
315
     *
316
     * @param  array  $headers
317
     *
318
     * @return array
319
     */
320
    protected function transformHeadersToServerVars(array $headers)
321
    {
322
        $server = [];
323
        $prefix = 'HTTP_';
324
        foreach ($headers as $name => $value) {
325
            $name = strtr(strtoupper($name), '-', '_');
326
            if (! starts_with($name, $prefix) && $name !== 'CONTENT_TYPE') {
327
                $name = $prefix.$name;
328
            }
329
            $server[$name] = $value;
330
        }
331
332
        return $server;
333
    }
334
}
335