1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Mpociot\ApiDoc\Extracting\Strategies\Responses; |
4
|
|
|
|
5
|
|
|
use Dingo\Api\Dispatcher; |
6
|
|
|
use Illuminate\Http\Request; |
7
|
|
|
use Illuminate\Http\Response; |
8
|
|
|
use Illuminate\Routing\Route; |
9
|
|
|
use Illuminate\Support\Str; |
10
|
|
|
use Mpociot\ApiDoc\Extracting\ParamHelpers; |
11
|
|
|
use Mpociot\ApiDoc\Extracting\Strategies\Strategy; |
12
|
|
|
use Mpociot\ApiDoc\Tools\Flags; |
13
|
|
|
use Mpociot\ApiDoc\Tools\Utils; |
14
|
|
|
|
15
|
|
|
/** |
16
|
|
|
* Make a call to the route and retrieve its response. |
17
|
|
|
*/ |
18
|
|
|
class ResponseCalls extends Strategy |
19
|
|
|
{ |
20
|
|
|
use ParamHelpers; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @param Route $route |
24
|
|
|
* @param \ReflectionClass $controller |
25
|
|
|
* @param \ReflectionMethod $method |
26
|
|
|
* @param array $routeRules |
27
|
|
|
* @param array $context |
28
|
|
|
* |
29
|
|
|
* @return array|null |
30
|
|
|
*/ |
31
|
|
|
public function __invoke(Route $route, \ReflectionClass $controller, \ReflectionMethod $method, array $routeRules, array $context = []) |
32
|
|
|
{ |
33
|
|
|
$rulesToApply = $routeRules['response_calls'] ?? []; |
34
|
|
|
if (! $this->shouldMakeApiCall($route, $rulesToApply, $context)) { |
35
|
|
|
return null; |
36
|
|
|
} |
37
|
|
|
|
38
|
|
|
$this->configureEnvironment($rulesToApply); |
39
|
|
|
|
40
|
|
|
// Mix in parsed parameters with manually specified parameters. |
41
|
|
|
$bodyParameters = array_merge($context['cleanBodyParameters'], $rulesToApply['bodyParams'] ?? []); |
42
|
|
|
$queryParameters = array_merge($context['cleanQueryParameters'], $rulesToApply['queryParams'] ?? []); |
43
|
|
|
$urlParameters = $context['cleanUrlParameters']; |
44
|
|
|
$request = $this->prepareRequest($route, $rulesToApply, $urlParameters, $bodyParameters, $queryParameters, $context['headers'] ?? []); |
45
|
|
|
|
46
|
|
|
try { |
47
|
|
|
$response = $this->makeApiCall($request); |
48
|
|
|
$response = [ |
49
|
|
|
[ |
50
|
|
|
'status' => $response->getStatusCode(), |
51
|
|
|
'content' => $response->getContent(), |
52
|
|
|
], |
53
|
|
|
]; |
54
|
|
|
} catch (\Exception $e) { |
55
|
|
|
echo 'Exception thrown during response call for [' . implode(',', $route->methods) . "] {$route->uri}.\n"; |
56
|
|
|
if (Flags::$shouldBeVerbose) { |
57
|
|
|
Utils::dumpException($e); |
58
|
|
|
} else { |
59
|
|
|
echo "Run this again with the --verbose flag to see the exception.\n"; |
60
|
|
|
} |
61
|
|
|
$response = null; |
62
|
|
|
} finally { |
63
|
|
|
$this->finish(); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
return $response; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @param array $rulesToApply |
71
|
|
|
* |
72
|
|
|
* @return void |
73
|
|
|
*/ |
74
|
|
|
private function configureEnvironment(array $rulesToApply) |
75
|
|
|
{ |
76
|
|
|
$this->startDbTransaction(); |
77
|
|
|
$this->setEnvironmentVariables($rulesToApply['env'] ?? []); |
|
|
|
|
78
|
|
|
$this->setLaravelConfigs($rulesToApply['config'] ?? []); |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* @param Route $route |
83
|
|
|
* @param array $rulesToApply |
84
|
|
|
* @param array $bodyParams |
85
|
|
|
* @param array $queryParams |
86
|
|
|
* |
87
|
|
|
* @return Request |
88
|
|
|
*/ |
89
|
|
|
protected function prepareRequest(Route $route, array $rulesToApply, array $urlParams, array $bodyParams, array $queryParams, array $headers) |
90
|
|
|
{ |
91
|
|
|
$uri = Utils::getFullUrl($route, $urlParams); |
92
|
|
|
$routeMethods = $this->getMethods($route); |
93
|
|
|
$method = array_shift($routeMethods); |
94
|
|
|
$cookies = isset($rulesToApply['cookies']) ? $rulesToApply['cookies'] : []; |
95
|
|
|
|
96
|
|
|
// Note that we initialise the request with the bodyPatams here |
97
|
|
|
// and later still add them to the ParameterBag (`addBodyParameters`) |
98
|
|
|
// The first is so the body params get added to the request content |
99
|
|
|
// (where Laravel reads body from) |
100
|
|
|
// The second is so they get added to the request bag |
101
|
|
|
// (where Symfony usually reads from and Laravel sometimes does) |
102
|
|
|
// Adding to both ensures consistency |
103
|
|
|
$request = Request::create($uri, $method, [], $cookies, [], $this->transformHeadersToServerVars($headers), json_encode($bodyParams)); |
104
|
|
|
// Doing it again to catch any ones we didn't transform properly. |
105
|
|
|
$request = $this->addHeaders($request, $route, $headers); |
106
|
|
|
|
107
|
|
|
$request = $this->addQueryParameters($request, $queryParams); |
108
|
|
|
$request = $this->addBodyParameters($request, $bodyParams); |
109
|
|
|
|
110
|
|
|
return $request; |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* @param array $env |
115
|
|
|
* |
116
|
|
|
* @return void |
117
|
|
|
* |
118
|
|
|
* @deprecated Not guaranteed to overwrite application's env. Use Laravel config variables instead. |
119
|
|
|
*/ |
120
|
|
|
private function setEnvironmentVariables(array $env) |
121
|
|
|
{ |
122
|
|
|
foreach ($env as $name => $value) { |
123
|
|
|
putenv("$name=$value"); |
124
|
|
|
|
125
|
|
|
$_ENV[$name] = $value; |
126
|
|
|
$_SERVER[$name] = $value; |
127
|
|
|
} |
128
|
|
|
} |
129
|
|
|
|
130
|
|
|
/** |
131
|
|
|
* @param array $config |
132
|
|
|
* |
133
|
|
|
* @return void |
134
|
|
|
*/ |
135
|
|
|
private function setLaravelConfigs(array $config) |
136
|
|
|
{ |
137
|
|
|
if (empty($config)) { |
138
|
|
|
return; |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
foreach ($config as $name => $value) { |
142
|
|
|
config([$name => $value]); |
143
|
|
|
} |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* @return void |
148
|
|
|
*/ |
149
|
|
|
private function startDbTransaction() |
150
|
|
|
{ |
151
|
|
|
try { |
152
|
|
|
app('db')->beginTransaction(); |
153
|
|
|
} catch (\Exception $e) { |
|
|
|
|
154
|
|
|
} |
155
|
|
|
} |
156
|
|
|
|
157
|
|
|
/** |
158
|
|
|
* @return void |
159
|
|
|
*/ |
160
|
|
|
private function endDbTransaction() |
161
|
|
|
{ |
162
|
|
|
try { |
163
|
|
|
app('db')->rollBack(); |
164
|
|
|
} catch (\Exception $e) { |
|
|
|
|
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* @return void |
170
|
|
|
*/ |
171
|
|
|
private function finish() |
172
|
|
|
{ |
173
|
|
|
$this->endDbTransaction(); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* @param Request $request |
178
|
|
|
* |
179
|
|
|
* @return \Illuminate\Http\JsonResponse|mixed |
180
|
|
|
*/ |
181
|
|
|
public function callDingoRoute(Request $request) |
182
|
|
|
{ |
183
|
|
|
/** @var Dispatcher $dispatcher */ |
184
|
|
|
$dispatcher = app(\Dingo\Api\Dispatcher::class); |
185
|
|
|
|
186
|
|
|
foreach ($request->headers as $header => $value) { |
187
|
|
|
$dispatcher->header($header, $value); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
// set domain and body parameters |
191
|
|
|
$dispatcher->on($request->header('SERVER_NAME')) |
192
|
|
|
->with($request->request->all()); |
193
|
|
|
|
194
|
|
|
// set URL and query parameters |
195
|
|
|
$uri = $request->getRequestUri(); |
196
|
|
|
$query = $request->getQueryString(); |
197
|
|
|
if (! empty($query)) { |
198
|
|
|
$uri .= "?$query"; |
199
|
|
|
} |
200
|
|
|
$response = call_user_func_array([$dispatcher, strtolower($request->method())], [$uri]); |
201
|
|
|
|
202
|
|
|
// the response from the Dingo dispatcher is the 'raw' response from the controller, |
203
|
|
|
// so we have to ensure it's JSON first |
204
|
|
|
if (! $response instanceof Response) { |
|
|
|
|
205
|
|
|
$response = response()->json($response); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
return $response; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* @param Route $route |
213
|
|
|
* |
214
|
|
|
* @return array |
215
|
|
|
*/ |
216
|
|
|
public function getMethods(Route $route) |
217
|
|
|
{ |
218
|
|
|
return array_diff($route->methods(), ['HEAD']); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* @param Request $request |
223
|
|
|
* @param Route $route |
224
|
|
|
* @param array|null $headers |
225
|
|
|
* |
226
|
|
|
* @return Request |
227
|
|
|
*/ |
228
|
|
|
private function addHeaders(Request $request, Route $route, $headers) |
229
|
|
|
{ |
230
|
|
|
// set the proper domain |
231
|
|
|
if ($route->getDomain()) { |
232
|
|
|
$request->headers->add([ |
233
|
|
|
'HOST' => $route->getDomain(), |
234
|
|
|
]); |
235
|
|
|
$request->server->add([ |
236
|
|
|
'HTTP_HOST' => $route->getDomain(), |
237
|
|
|
'SERVER_NAME' => $route->getDomain(), |
238
|
|
|
]); |
239
|
|
|
} |
240
|
|
|
|
241
|
|
|
$headers = collect($headers); |
242
|
|
|
|
243
|
|
|
if (($headers->get('Accept') ?: $headers->get('accept')) === 'application/json') { |
244
|
|
|
$request->setRequestFormat('json'); |
245
|
|
|
} |
246
|
|
|
|
247
|
|
|
return $request; |
248
|
|
|
} |
249
|
|
|
|
250
|
|
|
/** |
251
|
|
|
* @param Request $request |
252
|
|
|
* @param array $query |
253
|
|
|
* |
254
|
|
|
* @return Request |
255
|
|
|
*/ |
256
|
|
|
private function addQueryParameters(Request $request, array $query) |
257
|
|
|
{ |
258
|
|
|
$request->query->add($query); |
259
|
|
|
$request->server->add(['QUERY_STRING' => http_build_query($query)]); |
260
|
|
|
|
261
|
|
|
return $request; |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* @param Request $request |
266
|
|
|
* @param array $body |
267
|
|
|
* |
268
|
|
|
* @return Request |
269
|
|
|
*/ |
270
|
|
|
private function addBodyParameters(Request $request, array $body) |
271
|
|
|
{ |
272
|
|
|
$request->request->add($body); |
273
|
|
|
|
274
|
|
|
return $request; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* @param Request $request |
279
|
|
|
* |
280
|
|
|
* @throws \Exception |
281
|
|
|
* |
282
|
|
|
* @return \Illuminate\Http\JsonResponse|mixed|\Symfony\Component\HttpFoundation\Response |
283
|
|
|
*/ |
284
|
|
|
protected function makeApiCall(Request $request) |
285
|
|
|
{ |
286
|
|
|
if (config('apidoc.router') == 'dingo') { |
287
|
|
|
$response = $this->callDingoRoute($request); |
288
|
|
|
} else { |
289
|
|
|
$response = $this->callLaravelRoute($request); |
290
|
|
|
} |
291
|
|
|
|
292
|
|
|
return $response; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* @param Request $request |
297
|
|
|
* |
298
|
|
|
* @throws \Exception |
299
|
|
|
* |
300
|
|
|
* @return \Symfony\Component\HttpFoundation\Response |
301
|
|
|
*/ |
302
|
|
|
protected function callLaravelRoute(Request $request): \Symfony\Component\HttpFoundation\Response |
303
|
|
|
{ |
304
|
|
|
// Confirm we're running in Laravel, not Lumen |
305
|
|
|
if (app()->bound(\Illuminate\Contracts\Http\Kernel::class)) { |
306
|
|
|
$kernel = app(\Illuminate\Contracts\Http\Kernel::class); |
307
|
|
|
$response = $kernel->handle($request); |
308
|
|
|
$kernel->terminate($request, $response); |
309
|
|
|
} else { |
310
|
|
|
// Handle the request using the Lumen application. |
311
|
|
|
$kernel = app(); |
312
|
|
|
$response = $kernel->handle($request); |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
return $response; |
316
|
|
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* @param Route $route |
320
|
|
|
* @param array $rulesToApply |
321
|
|
|
* |
322
|
|
|
* @return bool |
323
|
|
|
*/ |
324
|
|
|
protected function shouldMakeApiCall(Route $route, array $rulesToApply, array $context): bool |
325
|
|
|
{ |
326
|
|
|
$allowedMethods = $rulesToApply['methods'] ?? []; |
327
|
|
|
if (empty($allowedMethods)) { |
328
|
|
|
return false; |
329
|
|
|
} |
330
|
|
|
|
331
|
|
|
// Don't attempt a response call if there are already successful responses |
332
|
|
|
$successResponses = collect($context['responses'])->filter(function ($response) { |
333
|
|
|
return ((string) $response['status'])[0] == '2'; |
334
|
|
|
})->count(); |
335
|
|
|
if ($successResponses) { |
336
|
|
|
return false; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
if (is_string($allowedMethods) && $allowedMethods == '*') { |
340
|
|
|
return true; |
341
|
|
|
} |
342
|
|
|
|
343
|
|
|
if (array_search('*', $allowedMethods) !== false) { |
344
|
|
|
return true; |
345
|
|
|
} |
346
|
|
|
|
347
|
|
|
$routeMethods = $this->getMethods($route); |
348
|
|
|
if (in_array(array_shift($routeMethods), $allowedMethods)) { |
349
|
|
|
return true; |
350
|
|
|
} |
351
|
|
|
|
352
|
|
|
return false; |
353
|
|
|
} |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* Transform headers array to array of $_SERVER vars with HTTP_* format. |
357
|
|
|
* |
358
|
|
|
* @param array $headers |
359
|
|
|
* |
360
|
|
|
* @return array |
361
|
|
|
*/ |
362
|
|
|
protected function transformHeadersToServerVars(array $headers) |
363
|
|
|
{ |
364
|
|
|
$server = []; |
365
|
|
|
$prefix = 'HTTP_'; |
366
|
|
|
foreach ($headers as $name => $value) { |
367
|
|
|
$name = strtr(strtoupper($name), '-', '_'); |
368
|
|
|
if (! Str::startsWith($name, $prefix) && $name !== 'CONTENT_TYPE') { |
369
|
|
|
$name = $prefix . $name; |
370
|
|
|
} |
371
|
|
|
$server[$name] = $value; |
372
|
|
|
} |
373
|
|
|
|
374
|
|
|
return $server; |
375
|
|
|
} |
376
|
|
|
} |
377
|
|
|
|
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.