Generator.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <?php
  2. namespace Mpociot\ApiDoc\Tools;
  3. use ReflectionClass;
  4. use ReflectionMethod;
  5. use Illuminate\Support\Arr;
  6. use Illuminate\Support\Str;
  7. use Illuminate\Routing\Route;
  8. class Generator
  9. {
  10. /**
  11. * @var DocumentationConfig
  12. */
  13. private $config;
  14. public function __construct(DocumentationConfig $config = null)
  15. {
  16. // If no config is injected, pull from global
  17. $this->config = $config ?: new DocumentationConfig(config('apidoc'));
  18. }
  19. /**
  20. * @param Route $route
  21. *
  22. * @return mixed
  23. */
  24. public function getUri(Route $route)
  25. {
  26. return $route->uri();
  27. }
  28. /**
  29. * @param Route $route
  30. *
  31. * @return mixed
  32. */
  33. public function getMethods(Route $route)
  34. {
  35. return array_diff($route->methods(), ['HEAD']);
  36. }
  37. /**
  38. * @param \Illuminate\Routing\Route $route
  39. * @param array $routeRules Rules to apply when generating documentation for this route
  40. *
  41. * @return array
  42. */
  43. public function processRoute(Route $route, array $routeRules = [])
  44. {
  45. list($controllerName, $methodName) = Utils::getRouteClassAndMethodNames($route->getAction());
  46. $controller = new ReflectionClass($controllerName);
  47. $method = $controller->getMethod($methodName);
  48. $parsedRoute = [
  49. 'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
  50. 'methods' => $this->getMethods($route),
  51. 'uri' => $this->getUri($route),
  52. ];
  53. $metadata = $this->fetchMetadata($controller, $method, $route, $routeRules, $parsedRoute);
  54. $parsedRoute['metadata'] = $metadata;
  55. $urlParameters = $this->fetchUrlParameters($controller, $method, $route, $routeRules, $parsedRoute);
  56. $parsedRoute['urlParameters'] = $urlParameters;
  57. $parsedRoute['cleanUrlParameters'] = $this->cleanParams($urlParameters);
  58. $parsedRoute['boundUri'] = Utils::getFullUrl($route, $parsedRoute['cleanUrlParameters']);
  59. $queryParameters = $this->fetchQueryParameters($controller, $method, $route, $routeRules, $parsedRoute);
  60. $parsedRoute['queryParameters'] = $queryParameters;
  61. $parsedRoute['cleanQueryParameters'] = $this->cleanParams($queryParameters);
  62. $bodyParameters = $this->fetchBodyParameters($controller, $method, $route, $routeRules, $parsedRoute);
  63. $parsedRoute['bodyParameters'] = $bodyParameters;
  64. $parsedRoute['cleanBodyParameters'] = $this->cleanParams($bodyParameters);
  65. $responses = $this->fetchResponses($controller, $method, $route, $routeRules, $parsedRoute);
  66. $parsedRoute['responses'] = $responses;
  67. $parsedRoute['showresponse'] = ! empty($responses);
  68. $parsedRoute['headers'] = $routeRules['headers'] ?? [];
  69. $parsedRoute += $metadata;
  70. return $parsedRoute;
  71. }
  72. protected function fetchMetadata(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
  73. {
  74. $context['metadata'] = [
  75. 'groupName' => $this->config->get('default_group', ''),
  76. 'groupDescription' => '',
  77. 'title' => '',
  78. 'description' => '',
  79. 'authenticated' => false,
  80. ];
  81. return $this->iterateThroughStrategies('metadata', $context, [$route, $controller, $method, $rulesToApply]);
  82. }
  83. protected function fetchUrlParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
  84. {
  85. return $this->iterateThroughStrategies('urlParameters', $context, [$route, $controller, $method, $rulesToApply]);
  86. }
  87. protected function fetchQueryParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
  88. {
  89. return $this->iterateThroughStrategies('queryParameters', $context, [$route, $controller, $method, $rulesToApply]);
  90. }
  91. protected function fetchBodyParameters(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
  92. {
  93. return $this->iterateThroughStrategies('bodyParameters', $context, [$route, $controller, $method, $rulesToApply]);
  94. }
  95. protected function fetchResponses(ReflectionClass $controller, ReflectionMethod $method, Route $route, array $rulesToApply, array $context = [])
  96. {
  97. $responses = $this->iterateThroughStrategies('responses', $context, [$route, $controller, $method, $rulesToApply]);
  98. if (count($responses)) {
  99. return $responses;
  100. }
  101. return null;
  102. }
  103. protected function iterateThroughStrategies(string $stage, array $context, array $arguments)
  104. {
  105. $defaultStrategies = [
  106. 'metadata' => [
  107. \Mpociot\ApiDoc\Strategies\Metadata\GetFromDocBlocks::class,
  108. ],
  109. 'urlParameters' => [
  110. \Mpociot\ApiDoc\Strategies\UrlParameters\GetFromUrlParamTag::class,
  111. ],
  112. 'queryParameters' => [
  113. \Mpociot\ApiDoc\Strategies\QueryParameters\GetFromQueryParamTag::class,
  114. ],
  115. 'bodyParameters' => [
  116. \Mpociot\ApiDoc\Strategies\BodyParameters\GetFromBodyParamTag::class,
  117. ],
  118. 'responses' => [
  119. \Mpociot\ApiDoc\Strategies\Responses\UseResponseTag::class,
  120. \Mpociot\ApiDoc\Strategies\Responses\UseResponseFileTag::class,
  121. \Mpociot\ApiDoc\Strategies\Responses\UseApiResourceTags::class,
  122. \Mpociot\ApiDoc\Strategies\Responses\UseTransformerTags::class,
  123. \Mpociot\ApiDoc\Strategies\Responses\ResponseCalls::class,
  124. ],
  125. ];
  126. // Use the default strategies for the stage, unless they were explicitly set
  127. $strategies = $this->config->get("strategies.$stage", $defaultStrategies[$stage]);
  128. $context[$stage] = $context[$stage] ?? [];
  129. foreach ($strategies as $strategyClass) {
  130. $strategy = new $strategyClass($stage, $this->config);
  131. $arguments[] = $context;
  132. $results = $strategy(...$arguments);
  133. if (! is_null($results)) {
  134. foreach ($results as $index => $item) {
  135. if ($stage == "responses") {
  136. // Responses are additive
  137. $context[$stage][] = $item;
  138. continue;
  139. }
  140. // Using a for loop rather than array_merge or +=
  141. // so it does not renumber numeric keys
  142. // and also allows values to be overwritten
  143. // Don't allow overwriting if an empty value is trying to replace a set one
  144. if (! in_array($context[$stage], [null, ''], true) && in_array($item, [null, ''], true)) {
  145. continue;
  146. } else {
  147. $context[$stage][$index] = $item;
  148. }
  149. }
  150. }
  151. }
  152. return $context[$stage];
  153. }
  154. /**
  155. * Create samples at index 0 for array parameters.
  156. * Also filter out parameters which were excluded from having examples.
  157. *
  158. * @param array $params
  159. *
  160. * @return array
  161. */
  162. protected function cleanParams(array $params)
  163. {
  164. $values = [];
  165. // Remove params which have no examples.
  166. $params = array_filter($params, function ($details) {
  167. return ! is_null($details['value']);
  168. });
  169. foreach ($params as $paramName => $details) {
  170. $this->generateConcreteSampleForArrayKeys(
  171. $paramName, $details['value'], $values
  172. );
  173. }
  174. return $values;
  175. }
  176. /**
  177. * For each array notation parameter (eg user.*, item.*.name, object.*.*, user[])
  178. * generate concrete sample (user.0, item.0.name, object.0.0, user.0) with example as value.
  179. *
  180. * @param string $paramName
  181. * @param mixed $paramExample
  182. * @param array $values The array that holds the result
  183. *
  184. * @return void
  185. */
  186. protected function generateConcreteSampleForArrayKeys($paramName, $paramExample, array &$values = [])
  187. {
  188. if (Str::contains($paramName, '[')) {
  189. // Replace usages of [] with dot notation
  190. $paramName = str_replace(['][', '[', ']', '..'], ['.', '.', '', '.*.'], $paramName);
  191. }
  192. // Then generate a sample item for the dot notation
  193. Arr::set($values, str_replace('.*', '.0', $paramName), $paramExample);
  194. }
  195. }