Completed
Push — master ( fa8a6e...14e95e )
by
unknown
11s queued 10s
created

ResponseCallStrategy::finish()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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