ResponseCallStrategy.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <?php
  2. namespace Mpociot\ApiDoc\Tools\ResponseStrategies;
  3. use Dingo\Api\Dispatcher;
  4. use Illuminate\Http\Request;
  5. use Illuminate\Http\Response;
  6. use Illuminate\Routing\Route;
  7. use Mpociot\ApiDoc\Tools\Flags;
  8. use Mpociot\ApiDoc\Tools\Utils;
  9. use Whoops\Exception\Inspector;
  10. use NunoMaduro\Collision\Handler;
  11. use Mpociot\ApiDoc\Tools\Traits\ParamHelpers;
  12. /**
  13. * Make a call to the route and retrieve its response.
  14. */
  15. class ResponseCallStrategy
  16. {
  17. use ParamHelpers;
  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. $this->configureEnvironment($rulesToApply);
  32. $request = $this->prepareRequest($route, $rulesToApply, $routeProps['body'], $routeProps['query']);
  33. try {
  34. $response = [$this->makeApiCall($request)];
  35. } catch (\Exception $e) {
  36. echo 'Exception thrown during response call for ['.implode(',', $route->methods)."] {$route->uri}.\n";
  37. if (Flags::$shouldBeVerbose) {
  38. $handler = new Handler;
  39. $handler->setInspector(new Inspector($e));
  40. $handler->setException($e);
  41. $handler->handle();
  42. } else {
  43. echo "Run this again with the --verbose flag to see the exception.\n";
  44. }
  45. $response = null;
  46. } finally {
  47. $this->finish();
  48. }
  49. return $response;
  50. }
  51. /**
  52. * @param array $rulesToApply
  53. *
  54. * @return void
  55. */
  56. private function configureEnvironment(array $rulesToApply)
  57. {
  58. $this->startDbTransaction();
  59. $this->setEnvironmentVariables($rulesToApply['env'] ?? []);
  60. $this->setLaravelConfigs($rulesToApply['config'] ?? []);
  61. }
  62. /**
  63. * @param Route $route
  64. * @param array $rulesToApply
  65. * @param array $bodyParams
  66. * @param array $queryParams
  67. *
  68. * @return Request
  69. */
  70. private function prepareRequest(Route $route, array $rulesToApply, array $bodyParams, array $queryParams)
  71. {
  72. $uri = Utils::getFullUrl($route, $rulesToApply['bindings'] ?? []);
  73. $routeMethods = $this->getMethods($route);
  74. $method = array_shift($routeMethods);
  75. $cookies = isset($rulesToApply['cookies']) ? $rulesToApply['cookies'] : [];
  76. $request = Request::create($uri, $method, [], $cookies, [], $this->transformHeadersToServerVars($rulesToApply['headers'] ?? []));
  77. $request = $this->addHeaders($request, $route, $rulesToApply['headers'] ?? []);
  78. // Mix in parsed parameters with manually specified parameters.
  79. $queryParams = collect($this->cleanParams($queryParams))->merge($rulesToApply['query'] ?? [])->toArray();
  80. $bodyParams = collect($this->cleanParams($bodyParams))->merge($rulesToApply['body'] ?? [])->toArray();
  81. $request = $this->addQueryParameters($request, $queryParams);
  82. $request = $this->addBodyParameters($request, $bodyParams);
  83. return $request;
  84. }
  85. /**
  86. * @param array $config
  87. *
  88. * @return void
  89. *
  90. * @deprecated in favour of Laravel config variables
  91. */
  92. private function setEnvironmentVariables(array $env)
  93. {
  94. foreach ($env as $name => $value) {
  95. putenv("$name=$value");
  96. $_ENV[$name] = $value;
  97. $_SERVER[$name] = $value;
  98. }
  99. }
  100. /**
  101. * @param array $config
  102. *
  103. * @return void
  104. */
  105. private function setLaravelConfigs(array $config)
  106. {
  107. if (empty($config)) {
  108. return;
  109. }
  110. foreach ($config as $name => $value) {
  111. config([$name => $value]);
  112. }
  113. }
  114. /**
  115. * @return void
  116. */
  117. private function startDbTransaction()
  118. {
  119. try {
  120. app('db')->beginTransaction();
  121. } catch (\Exception $e) {
  122. }
  123. }
  124. /**
  125. * @return void
  126. */
  127. private function endDbTransaction()
  128. {
  129. try {
  130. app('db')->rollBack();
  131. } catch (\Exception $e) {
  132. }
  133. }
  134. /**
  135. * @return void
  136. */
  137. private function finish()
  138. {
  139. $this->endDbTransaction();
  140. }
  141. /**
  142. * @param Request $request
  143. *
  144. * @return \Illuminate\Http\JsonResponse|mixed
  145. */
  146. public function callDingoRoute(Request $request)
  147. {
  148. /** @var Dispatcher $dispatcher */
  149. $dispatcher = app(\Dingo\Api\Dispatcher::class);
  150. foreach ($request->headers as $header => $value) {
  151. $dispatcher->header($header, $value);
  152. }
  153. // set domain and body parameters
  154. $dispatcher->on($request->header('SERVER_NAME'))
  155. ->with($request->request->all());
  156. // set URL and query parameters
  157. $uri = $request->getRequestUri();
  158. $query = $request->getQueryString();
  159. if (! empty($query)) {
  160. $uri .= "?$query";
  161. }
  162. $response = call_user_func_array([$dispatcher, strtolower($request->method())], [$uri]);
  163. // the response from the Dingo dispatcher is the 'raw' response from the controller,
  164. // so we have to ensure it's JSON first
  165. if (! $response instanceof Response) {
  166. $response = response()->json($response);
  167. }
  168. return $response;
  169. }
  170. /**
  171. * @param Route $route
  172. *
  173. * @return array
  174. */
  175. public function getMethods(Route $route)
  176. {
  177. return array_diff($route->methods(), ['HEAD']);
  178. }
  179. /**
  180. * @param Request $request
  181. * @param Route $route
  182. * @param array|null $headers
  183. *
  184. * @return Request
  185. */
  186. private function addHeaders(Request $request, Route $route, $headers)
  187. {
  188. // set the proper domain
  189. if ($route->getDomain()) {
  190. $request->headers->add([
  191. 'HOST' => $route->getDomain(),
  192. ]);
  193. $request->server->add([
  194. 'HTTP_HOST' => $route->getDomain(),
  195. 'SERVER_NAME' => $route->getDomain(),
  196. ]);
  197. }
  198. $headers = collect($headers);
  199. if (($headers->get('Accept') ?: $headers->get('accept')) === 'application/json') {
  200. $request->setRequestFormat('json');
  201. }
  202. return $request;
  203. }
  204. /**
  205. * @param Request $request
  206. * @param array $query
  207. *
  208. * @return Request
  209. */
  210. private function addQueryParameters(Request $request, array $query)
  211. {
  212. $request->query->add($query);
  213. $request->server->add(['QUERY_STRING' => http_build_query($query)]);
  214. return $request;
  215. }
  216. /**
  217. * @param Request $request
  218. * @param array $body
  219. *
  220. * @return Request
  221. */
  222. private function addBodyParameters(Request $request, array $body)
  223. {
  224. $request->request->add($body);
  225. return $request;
  226. }
  227. /**
  228. * @param Request $request
  229. *
  230. * @throws \Exception
  231. *
  232. * @return \Illuminate\Http\JsonResponse|mixed|\Symfony\Component\HttpFoundation\Response
  233. */
  234. private function makeApiCall(Request $request)
  235. {
  236. if (config('apidoc.router') == 'dingo') {
  237. $response = $this->callDingoRoute($request);
  238. } else {
  239. $response = $this->callLaravelRoute($request);
  240. }
  241. return $response;
  242. }
  243. /**
  244. * @param Request $request
  245. *
  246. * @throws \Exception
  247. *
  248. * @return \Symfony\Component\HttpFoundation\Response
  249. */
  250. private function callLaravelRoute(Request $request): \Symfony\Component\HttpFoundation\Response
  251. {
  252. $kernel = app(\Illuminate\Contracts\Http\Kernel::class);
  253. $response = $kernel->handle($request);
  254. $kernel->terminate($request, $response);
  255. return $response;
  256. }
  257. /**
  258. * @param Route $route
  259. * @param array $rulesToApply
  260. *
  261. * @return bool
  262. */
  263. private function shouldMakeApiCall(Route $route, array $rulesToApply): bool
  264. {
  265. $allowedMethods = $rulesToApply['methods'] ?? [];
  266. if (empty($allowedMethods)) {
  267. return false;
  268. }
  269. if (is_string($allowedMethods) && $allowedMethods == '*') {
  270. return true;
  271. }
  272. if (array_search('*', $allowedMethods) !== false) {
  273. return true;
  274. }
  275. $routeMethods = $this->getMethods($route);
  276. if (in_array(array_shift($routeMethods), $allowedMethods)) {
  277. return true;
  278. }
  279. return false;
  280. }
  281. /**
  282. * Transform headers array to array of $_SERVER vars with HTTP_* format.
  283. *
  284. * @param array $headers
  285. *
  286. * @return array
  287. */
  288. protected function transformHeadersToServerVars(array $headers)
  289. {
  290. $server = [];
  291. $prefix = 'HTTP_';
  292. foreach ($headers as $name => $value) {
  293. $name = strtr(strtoupper($name), '-', '_');
  294. if (! starts_with($name, $prefix) && $name !== 'CONTENT_TYPE') {
  295. $name = $prefix.$name;
  296. }
  297. $server[$name] = $value;
  298. }
  299. return $server;
  300. }
  301. }