ResponseCallStrategy.php 8.5 KB

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