Completed
Push — master ( 99fbbb...fa8a6e )
by
unknown
13s
created

ResponseCallStrategy::callDingoRoute()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

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