Completed
Pull Request — master (#376)
by
unknown
01:48
created

ResponseCallStrategy::disableDbTransactions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 10
c 0
b 0
f 0
cc 2
nc 2
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->enableDbTransactions();
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 enableDbTransactions()
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
94
    private function disableDbTransactions()
95
    {
96
        try {
97
            app('db')->rollBack();
98
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
99
100
        }
101
    }
102
103
    private function finish()
104
    {
105
        $this->disableDbTransactions();
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     */
111
    public function callDingoRoute(Request $request)
112
    {
113
        /** @var Dispatcher $dispatcher */
114
        $dispatcher = app(\Dingo\Api\Dispatcher::class);
115
116
        foreach ($request->headers as $header => $value) {
117
            $dispatcher->header($header, $value);
118
        }
119
120
        // set domain and body parameters
121
        $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...
122
            ->with($request->request->all());
123
124
        // set URL and query parameters
125
        $uri = $request->getRequestUri();
126
        $query = $request->getQueryString();
127
        if (!empty($query)) {
128
            $uri .= "?$query";
129
        }
130
        $response = call_user_func_array([$dispatcher, strtolower($request->method())], [$uri]);
131
132
        // the response from the Dingo dispatcher is the 'raw' response from the controller,
133
        // so we have to ensure it's JSON first
134
        if (! $response instanceof Response) {
135
            $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...
136
        }
137
138
        return $response;
139
    }
140
141
    public function getMethods(Route $route)
142
    {
143
        return array_diff($route->methods(), ['HEAD']);
144
    }
145
146
    private function addHeaders(Request $request, Route $route, $headers)
147
    {
148
        // set the proper domain
149
        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...
150
            $request->server->add([
151
                'HTTP_HOST' => $route->getDomain(),
152
                'SERVER_NAME' => $route->getDomain(),
153
            ]);
154
        }
155
156
        $headers = collect($headers);
157
158
        if (($headers->get('Accept') ?: $headers->get('accept')) === 'application/json') {
159
            $request->setRequestFormat('json');
160
        }
161
162
        return $request;
163
    }
164
165
    private function addQueryParameters(Request $request, array $query)
166
    {
167
        $request->query->add($query);
168
        $request->server->add(['QUERY_STRING' => http_build_query($query)]);
169
170
        return $request;
171
    }
172
173
    private function addBodyParameters(Request $request, array $body)
174
    {
175
        $request->request->add($body);
176
177
        return $request;
178
    }
179
180
    private function makeApiCall(Request $request)
181
    {
182
        if (config('apidoc.router') == 'dingo') {
183
            $response = $this->callDingoRoute($request);
184
        } else {
185
            $response = $this->callLaravelRoute($request);
186
        }
187
188
        return $response;
189
    }
190
191
    /**
192
     * @param $request
193
     *
194
     * @return \Symfony\Component\HttpFoundation\Response
195
     */
196
    private function callLaravelRoute($request): \Symfony\Component\HttpFoundation\Response
197
    {
198
        $kernel = app(\Illuminate\Contracts\Http\Kernel::class);
199
        $response = $kernel->handle($request);
200
        $kernel->terminate($request, $response);
201
202
        return $response;
203
    }
204
205
    private function shouldMakeApiCall(Route $route, array $rulesToApply): bool
206
    {
207
        $allowedMethods = $rulesToApply['methods'] ?? [];
208
        if (empty($allowedMethods)) {
209
            return false;
210
        }
211
212
        if (array_search('*', $allowedMethods) !== false) {
213
            return true;
214
        }
215
216
        $routeMethods = $this->getMethods($route);
217
        if (in_array(array_shift($routeMethods), $allowedMethods)) {
218
            return true;
219
        }
220
221
        return false;
222
    }
223
224
    /**
225
     * Transform headers array to array of $_SERVER vars with HTTP_* format.
226
     *
227
     * @param  array  $headers
228
     *
229
     * @return array
230
     */
231
    protected function transformHeadersToServerVars(array $headers)
232
    {
233
        $server = [];
234
        $prefix = 'HTTP_';
235
        foreach ($headers as $name => $value) {
236
            $name = strtr(strtoupper($name), '-', '_');
237
            if (! starts_with($name, $prefix) && $name !== 'CONTENT_TYPE') {
238
                $name = $prefix.$name;
239
            }
240
            $server[$name] = $value;
241
        }
242
243
        return $server;
244
    }
245
}
246