GenerateDocumentation.php 5.8 KB

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