GenerateDocumentation.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. <?php
  2. namespace Mpociot\ApiDoc\Commands;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Routing\Route;
  5. use Illuminate\Support\Collection;
  6. use Illuminate\Support\Facades\URL;
  7. use Mpociot\ApiDoc\Extracting\Generator;
  8. use Mpociot\ApiDoc\Matching\RouteMatcher\Match;
  9. use Mpociot\ApiDoc\Matching\RouteMatcherInterface;
  10. use Mpociot\ApiDoc\Tools\DocumentationConfig;
  11. use Mpociot\ApiDoc\Tools\Flags;
  12. use Mpociot\ApiDoc\Tools\Utils;
  13. use Mpociot\ApiDoc\Writing\Writer;
  14. use Mpociot\Reflection\DocBlock;
  15. use ReflectionClass;
  16. use ReflectionException;
  17. class GenerateDocumentation extends Command
  18. {
  19. /**
  20. * The name and signature of the console command.
  21. *
  22. * @var string
  23. */
  24. protected $signature = 'apidoc:generate
  25. {--force : Force rewriting of existing routes}
  26. ';
  27. /**
  28. * The console command description.
  29. *
  30. * @var string
  31. */
  32. protected $description = 'Generate your API documentation from existing Laravel routes.';
  33. /**
  34. * @var DocumentationConfig
  35. */
  36. private $docConfig;
  37. /**
  38. * @var string
  39. */
  40. private $baseUrl;
  41. /**
  42. * Execute the console command.
  43. *
  44. * @param RouteMatcherInterface $routeMatcher
  45. *
  46. * @return void
  47. */
  48. public function handle(RouteMatcherInterface $routeMatcher)
  49. {
  50. // Using a global static variable here, so fuck off if you don't like it.
  51. // Also, the --verbose option is included with all Artisan commands.
  52. Flags::$shouldBeVerbose = $this->option('verbose');
  53. $this->docConfig = new DocumentationConfig(config('apidoc'));
  54. $this->baseUrl = $this->docConfig->get('base_url') ?? config('app.url');
  55. URL::forceRootUrl($this->baseUrl);
  56. $routes = $routeMatcher->getRoutes($this->docConfig->get('routes'), $this->docConfig->get('router'));
  57. $generator = new Generator($this->docConfig);
  58. $parsedRoutes = $this->processRoutes($generator, $routes);
  59. $groupedRoutes = collect($parsedRoutes)
  60. ->groupBy('metadata.groupName')
  61. ->sortBy(static function ($group) {
  62. /* @var $group Collection */
  63. return $group->first()['metadata']['groupName'];
  64. }, SORT_NATURAL);
  65. $writer = new Writer(
  66. $this,
  67. $this->docConfig,
  68. $this->option('force')
  69. );
  70. $writer->writeDocs($groupedRoutes);
  71. }
  72. /**
  73. * @param \Mpociot\ApiDoc\Extracting\Generator $generator
  74. * @param Match[] $routes
  75. *
  76. * @throws \ReflectionException
  77. *
  78. * @return array
  79. */
  80. private function processRoutes(Generator $generator, array $routes)
  81. {
  82. $parsedRoutes = [];
  83. foreach ($routes as $routeItem) {
  84. $route = $routeItem->getRoute();
  85. /** @var Route $route */
  86. $messageFormat = '%s route: [%s] %s';
  87. $routeMethods = implode(',', $generator->getMethods($route));
  88. $routePath = $generator->getUri($route);
  89. $routeControllerAndMethod = Utils::getRouteClassAndMethodNames($route->getAction());
  90. if (! $this->isValidRoute($routeControllerAndMethod)) {
  91. $this->warn(sprintf($messageFormat, 'Skipping invalid', $routeMethods, $routePath));
  92. continue;
  93. }
  94. if (! $this->doesControllerMethodExist($routeControllerAndMethod)) {
  95. $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath) . ': Controller method does not exist.');
  96. continue;
  97. }
  98. if (! $this->isRouteVisibleForDocumentation($routeControllerAndMethod)) {
  99. $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath) . ': @hideFromAPIDocumentation was specified.');
  100. continue;
  101. }
  102. try {
  103. $parsedRoutes[] = $generator->processRoute($route, $routeItem->getRules());
  104. $this->info(sprintf($messageFormat, 'Processed', $routeMethods, $routePath));
  105. } catch (\Exception $exception) {
  106. $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath) . '- Exception ' . get_class($exception) . ' encountered : ' . $exception->getMessage());
  107. }
  108. }
  109. return $parsedRoutes;
  110. }
  111. /**
  112. * @param array $routeControllerAndMethod
  113. *
  114. * @return bool
  115. */
  116. private function isValidRoute(array $routeControllerAndMethod = null)
  117. {
  118. if (is_array($routeControllerAndMethod)) {
  119. [$classOrObject, $method] = $routeControllerAndMethod;
  120. if (Utils::isInvokableObject($classOrObject)) {
  121. return true;
  122. }
  123. $routeControllerAndMethod = $classOrObject . '@' . $method;
  124. }
  125. return ! is_callable($routeControllerAndMethod) && ! is_null($routeControllerAndMethod);
  126. }
  127. /**
  128. * @param array $routeControllerAndMethod
  129. *
  130. * @throws ReflectionException
  131. *
  132. * @return bool
  133. */
  134. private function doesControllerMethodExist(array $routeControllerAndMethod)
  135. {
  136. [$class, $method] = $routeControllerAndMethod;
  137. $reflection = new ReflectionClass($class);
  138. if (! $reflection->hasMethod($method)) {
  139. return false;
  140. }
  141. return true;
  142. }
  143. /**
  144. * @param array $routeControllerAndMethod
  145. *
  146. * @throws ReflectionException
  147. *
  148. * @return bool
  149. */
  150. private function isRouteVisibleForDocumentation(array $routeControllerAndMethod)
  151. {
  152. $comment = Utils::reflectRouteMethod($routeControllerAndMethod)->getDocComment();
  153. if ($comment) {
  154. $phpdoc = new DocBlock($comment);
  155. return collect($phpdoc->getTags())
  156. ->filter(function ($tag) {
  157. return $tag->getName() === 'hideFromAPIDocumentation';
  158. })
  159. ->isEmpty();
  160. }
  161. return true;
  162. }
  163. }