Completed
Push — master ( adc96f...622b7c )
by
unknown
59:44 queued 04:43
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\Http\Request;
7
use Illuminate\Http\Response;
8
use Illuminate\Routing\Route;
9
10
/**
11
 * Make a call to the route and retrieve its response.
12
 */
13
class ResponseCallStrategy
14
{
15
    public function __invoke(Route $route, array $tags, array $rulesToApply)
16
    {
17
        $rulesToApply = $rulesToApply['response_calls'] ?? [];
18
        if (! $this->shouldMakeApiCall($route, $rulesToApply)) {
19
            return;
20
        }
21
22
        $this->configureEnvironment($rulesToApply);
23
        $request = $this->prepareRequest($route, $rulesToApply);
24
        try {
25
            $response = $this->makeApiCall($request);
26
        } catch (\Exception $e) {
27
            $response = null;
28
        } finally {
29
            $this->finish();
30
        }
31
32
        return $response;
33
    }
34
35
    private function configureEnvironment(array $rulesToApply)
36
    {
37
        $this->startDbTransaction();
38
        $this->setEnvironmentVariables($rulesToApply['env'] ?? []);
39
    }
40
41
    private function prepareRequest(Route $route, array $rulesToApply)
42
    {
43
        $uri = $this->replaceUrlParameterBindings($route, $rulesToApply['bindings'] ?? []);
44
        $routeMethods = $this->getMethods($route);
45
        $method = array_shift($routeMethods);
46
        $request = Request::create($uri, $method, [], [], [], $this->transformHeadersToServerVars($rulesToApply['headers'] ?? []));
47
        $request = $this->addHeaders($request, $route, $rulesToApply['headers'] ?? []);
48
        $request = $this->addQueryParameters($request, $rulesToApply['query'] ?? []);
49
        $request = $this->addBodyParameters($request, $rulesToApply['body'] ?? []);
50
51
        return $request;
52
    }
53
54
    /**
55
     * Transform parameters in URLs into real values (/users/{user} -> /users/2).
56
     * Uses bindings specified by caller, otherwise just uses '1'.
57
     *
58
     * @param Route $route
59
     * @param array $bindings
60
     *
61
     * @return mixed
62
     */
63
    protected function replaceUrlParameterBindings(Route $route, $bindings)
64
    {
65
        $uri = $route->uri();
66
        foreach ($bindings as $parameter => $binding) {
67
            $uri = str_replace($parameter, $binding, $uri);
68
        }
69
        // Replace any unbound parameters with '1'
70
        $uri = preg_replace('/{(.*?)}/', 1, $uri);
71
72
        return $uri;
73
    }
74
75
    private function setEnvironmentVariables(array $env)
76
    {
77
        foreach ($env as $name => $value) {
78
            putenv("$name=$value");
79
80
            $_ENV[$name] = $value;
81
            $_SERVER[$name] = $value;
82
        }
83
    }
84
85
    private function startDbTransaction()
86
    {
87
        try {
88
            app('db')->beginTransaction();
89
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
90
        }
91
    }
92
93
    private function endDbTransaction()
94
    {
95
        try {
96
            app('db')->rollBack();
97
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
98
        }
99
    }
100
101
    private function finish()
102
    {
103
        $this->endDbTransaction();
104
    }
105
106
    public function callDingoRoute(Request $request)
107
    {
108
        /** @var Dispatcher $dispatcher */
109
        $dispatcher = app(\Dingo\Api\Dispatcher::class);
110
111
        foreach ($request->headers as $header => $value) {
112
            $dispatcher->header($header, $value);
113
        }
114
115
        // set domain and body parameters
116
        $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...
117
            ->with($request->request->all());
118
119
        // set URL and query parameters
120
        $uri = $request->getRequestUri();
121
        $query = $request->getQueryString();
122
        if (! empty($query)) {
123
            $uri .= "?$query";
124
        }
125
        $response = call_user_func_array([$dispatcher, strtolower($request->method())], [$uri]);
126
127
        // the response from the Dingo dispatcher is the 'raw' response from the controller,
128
        // so we have to ensure it's JSON first
129
        if (! $response instanceof Response) {
130
            $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...
131
        }
132
133
        return $response;
134
    }
135
136
    public function getMethods(Route $route)
137
    {
138
        return array_diff($route->methods(), ['HEAD']);
139
    }
140
141
    private function addHeaders(Request $request, Route $route, $headers)
142
    {
143
        // set the proper domain
144
        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...
145
            $request->server->add([
146
                'HTTP_HOST' => $route->getDomain(),
147
                'SERVER_NAME' => $route->getDomain(),
148
            ]);
149
        }
150
151
        $headers = collect($headers);
152
153
        if (($headers->get('Accept') ?: $headers->get('accept')) === 'application/json') {
154
            $request->setRequestFormat('json');
155
        }
156
157
        return $request;
158
    }
159
160
    private function addQueryParameters(Request $request, array $query)
161
    {
162
        $request->query->add($query);
163
        $request->server->add(['QUERY_STRING' => http_build_query($query)]);
164
165
        return $request;
166
    }
167
168
    private function addBodyParameters(Request $request, array $body)
169
    {
170
        $request->request->add($body);
171
172
        return $request;
173
    }
174
175
    private function makeApiCall(Request $request)
176
    {
177
        if (config('apidoc.router') == 'dingo') {
178
            $response = $this->callDingoRoute($request);
179
        } else {
180
            $response = $this->callLaravelRoute($request);
181
        }
182
183
        return $response;
184
    }
185
186
    /**
187
     * @param $request
188
     *
189
     * @return \Symfony\Component\HttpFoundation\Response
190
     */
191
    private function callLaravelRoute($request): \Symfony\Component\HttpFoundation\Response
192
    {
193
        $kernel = app(\Illuminate\Contracts\Http\Kernel::class);
194
        $response = $kernel->handle($request);
195
        $kernel->terminate($request, $response);
196
197
        return $response;
198
    }
199
200
    private function shouldMakeApiCall(Route $route, array $rulesToApply): bool
201
    {
202
        $allowedMethods = $rulesToApply['methods'] ?? [];
203
        if (empty($allowedMethods)) {
204
            return false;
205
        }
206
207
        if (is_string($allowedMethods) && $allowedMethods == '*') {
208
            return true;
209
        }
210
211
        if (array_search('*', $allowedMethods) !== false) {
212
            return true;
213
        }
214
215
        $routeMethods = $this->getMethods($route);
216
        if (in_array(array_shift($routeMethods), $allowedMethods)) {
217
            return true;
218
        }
219
220
        return false;
221
    }
222
223
    /**
224
     * Transform headers array to array of $_SERVER vars with HTTP_* format.
225
     *
226
     * @param  array  $headers
227
     *
228
     * @return array
229
     */
230
    protected function transformHeadersToServerVars(array $headers)
231
    {
232
        $server = [];
233
        $prefix = 'HTTP_';
234
        foreach ($headers as $name => $value) {
235
            $name = strtr(strtoupper($name), '-', '_');
236
            if (! starts_with($name, $prefix) && $name !== 'CONTENT_TYPE') {
237
                $name = $prefix.$name;
238
            }
239
            $server[$name] = $value;
240
        }
241
242
        return $server;
243
    }
244
}
245