ResponseCallStrategy.php 8.7 KB

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