|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Knuckles\Scribe\Docblock2Attributes\Commands; |
| 4 | + |
| 5 | +use Illuminate\Console\Command; |
| 6 | +use Illuminate\Support\Str; |
| 7 | +use Knuckles\Scribe\Docblock2Attributes\Extractor; |
| 8 | +use Knuckles\Scribe\GroupedEndpoints\GroupedEndpointsFactory; |
| 9 | +use Knuckles\Scribe\Matching\RouteMatcherInterface; |
| 10 | +use Knuckles\Scribe\Tools\ConsoleOutputUtils as c; |
| 11 | +use Knuckles\Scribe\Tools\DocumentationConfig; |
| 12 | +use Knuckles\Scribe\Tools\ErrorHandlingUtils as e; |
| 13 | +use Knuckles\Scribe\Tools\Globals; |
| 14 | +use Knuckles\Scribe\Tools\Utils as u; |
| 15 | +use Mpociot\Reflection\DocBlock; |
| 16 | +use Mpociot\Reflection\DocBlock\Tag; |
| 17 | +use ReflectionClass; |
| 18 | + |
| 19 | +class Process extends Command |
| 20 | +{ |
| 21 | + protected $signature = "scribe:docblock2attributes |
| 22 | + {--config=scribe : choose which config file to use} |
| 23 | + "; |
| 24 | + |
| 25 | + protected $description = 'Convert docblock tags to PHP attributes.'; |
| 26 | + |
| 27 | + protected DocumentationConfig $docConfig; |
| 28 | + |
| 29 | + public function handle(RouteMatcherInterface $routeMatcher, GroupedEndpointsFactory $groupedEndpointsFactory): void |
| 30 | + { |
| 31 | + $this->bootstrap(); |
| 32 | + |
| 33 | + if (!empty($this->docConfig->get("default_group"))) { |
| 34 | + $this->warn("It looks like you just upgraded to Scribe v4."); |
| 35 | + $this->warn("Please run the upgrade command first: `php artisan scribe:upgrade`."); |
| 36 | + exit(1); |
| 37 | + } |
| 38 | + |
| 39 | + $routes = $routeMatcher->getRoutes($this->docConfig->get('routes', []), $this->docConfig->get('router')); |
| 40 | + $extracted = $this->extract($routes); |
| 41 | + // $this->transform($routes); |
| 42 | + } |
| 43 | + |
| 44 | + protected function extract(array $matches): array |
| 45 | + { |
| 46 | + $extractor = new Extractor($this->docConfig); |
| 47 | + $parsedEndpoints = []; |
| 48 | + |
| 49 | + foreach ($matches as $routeItem) { |
| 50 | + $route = $routeItem->getRoute(); |
| 51 | + |
| 52 | + $routeControllerAndMethod = u::getRouteClassAndMethodNames($route); |
| 53 | + if (!$this->isValidRoute($routeControllerAndMethod)) { |
| 54 | + c::warn('Skipping invalid route: ' . c::getRouteRepresentation($route)); |
| 55 | + continue; |
| 56 | + } |
| 57 | + |
| 58 | + if (!$this->doesControllerMethodExist($routeControllerAndMethod)) { |
| 59 | + c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ' - Controller method does not exist.'); |
| 60 | + continue; |
| 61 | + } |
| 62 | + |
| 63 | + if ($this->isRouteHiddenFromDocumentation($routeControllerAndMethod)) { |
| 64 | + c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ': @hideFromAPIDocumentation was specified.'); |
| 65 | + continue; |
| 66 | + } |
| 67 | + |
| 68 | + try { |
| 69 | + c::info('Processing route: ' . c::getRouteRepresentation($route)); |
| 70 | + [$controllerName, $methodName] = u::getRouteClassAndMethodNames($route); |
| 71 | + $controller = new ReflectionClass($controllerName); |
| 72 | + $method = u::getReflectedRouteMethod([$controllerName, $methodName]); |
| 73 | + $endpointData = $extractor->processRoute($route, $method, $controller); |
| 74 | + ray($endpointData); |
| 75 | + c::success('Processed route: ' . c::getRouteRepresentation($route)); |
| 76 | + } catch (\Exception $exception) { |
| 77 | + $this->encounteredErrors = true; |
| 78 | + c::error('Failed processing route: ' . c::getRouteRepresentation($route) . ' - Exception encountered.'); |
| 79 | + e::dumpExceptionIfVerbose($exception); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + return $parsedEndpoints; |
| 84 | + } |
| 85 | + |
| 86 | + protected function transform(array $matches): array |
| 87 | + { |
| 88 | + $extractor = new Transformer($this->docConfig); |
| 89 | + $parsedEndpoints = []; |
| 90 | + |
| 91 | + foreach ($matches as $routeItem) { |
| 92 | + $route = $routeItem->getRoute(); |
| 93 | + |
| 94 | + $routeControllerAndMethod = u::getRouteClassAndMethodNames($route); |
| 95 | + if (!$this->isValidRoute($routeControllerAndMethod)) { |
| 96 | + c::warn('Skipping invalid route: ' . c::getRouteRepresentation($route)); |
| 97 | + continue; |
| 98 | + } |
| 99 | + |
| 100 | + if (!$this->doesControllerMethodExist($routeControllerAndMethod)) { |
| 101 | + c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ' - Controller method does not exist.'); |
| 102 | + continue; |
| 103 | + } |
| 104 | + |
| 105 | + if ($this->isRouteHiddenFromDocumentation($routeControllerAndMethod)) { |
| 106 | + c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ': @hideFromAPIDocumentation was specified.'); |
| 107 | + continue; |
| 108 | + } |
| 109 | + |
| 110 | + try { |
| 111 | + c::info('Processing route: ' . c::getRouteRepresentation($route)); |
| 112 | + $endpointData = $extractor->processRoute($route, $routeItem->getRules()); |
| 113 | + c::success('Processed route: ' . c::getRouteRepresentation($route)); |
| 114 | + } catch (\Exception $exception) { |
| 115 | + $this->encounteredErrors = true; |
| 116 | + c::error('Failed processing route: ' . c::getRouteRepresentation($route) . ' - Exception encountered.'); |
| 117 | + e::dumpExceptionIfVerbose($exception); |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + return $parsedEndpoints; |
| 122 | + } |
| 123 | + |
| 124 | + |
| 125 | + private function isValidRoute(array $routeControllerAndMethod = null): bool |
| 126 | + { |
| 127 | + if (is_array($routeControllerAndMethod)) { |
| 128 | + [$classOrObject, $method] = $routeControllerAndMethod; |
| 129 | + if (u::isInvokableObject($classOrObject)) { |
| 130 | + return true; |
| 131 | + } |
| 132 | + $routeControllerAndMethod = $classOrObject . '@' . $method; |
| 133 | + } |
| 134 | + |
| 135 | + return !is_callable($routeControllerAndMethod) && !is_null($routeControllerAndMethod); |
| 136 | + } |
| 137 | + |
| 138 | + private function doesControllerMethodExist(array $routeControllerAndMethod): bool |
| 139 | + { |
| 140 | + [$class, $method] = $routeControllerAndMethod; |
| 141 | + $reflection = new ReflectionClass($class); |
| 142 | + |
| 143 | + if ($reflection->hasMethod($method)) { |
| 144 | + return true; |
| 145 | + } |
| 146 | + |
| 147 | + return false; |
| 148 | + } |
| 149 | + |
| 150 | + private function isRouteHiddenFromDocumentation(array $routeControllerAndMethod): bool |
| 151 | + { |
| 152 | + if (!($class = $routeControllerAndMethod[0]) instanceof \Closure) { |
| 153 | + $classDocBlock = new DocBlock((new ReflectionClass($class))->getDocComment() ?: ''); |
| 154 | + $shouldIgnoreClass = collect($classDocBlock->getTags()) |
| 155 | + ->filter(function (Tag $tag) { |
| 156 | + return Str::lower($tag->getName()) === 'hidefromapidocumentation'; |
| 157 | + })->isNotEmpty(); |
| 158 | + |
| 159 | + if ($shouldIgnoreClass) { |
| 160 | + return true; |
| 161 | + } |
| 162 | + } |
| 163 | + |
| 164 | + $methodDocBlock = new DocBlock(u::getReflectedRouteMethod($routeControllerAndMethod)->getDocComment() ?: ''); |
| 165 | + $shouldIgnoreMethod = collect($methodDocBlock->getTags()) |
| 166 | + ->filter(function (Tag $tag) { |
| 167 | + return Str::lower($tag->getName()) === 'hidefromapidocumentation'; |
| 168 | + })->isNotEmpty(); |
| 169 | + |
| 170 | + return $shouldIgnoreMethod; |
| 171 | + } |
| 172 | + |
| 173 | + |
| 174 | + public function bootstrap(): void |
| 175 | + { |
| 176 | + // The --verbose option is included with all Artisan commands. |
| 177 | + Globals::$shouldBeVerbose = $this->option('verbose'); |
| 178 | + |
| 179 | + c::bootstrapOutput($this->output); |
| 180 | + |
| 181 | + $this->configName = $this->option('config'); |
| 182 | + if (!config($this->configName)) { |
| 183 | + throw new \InvalidArgumentException("The specified config (config/{$this->configName}.php) doesn't exist."); |
| 184 | + } |
| 185 | + |
| 186 | + $this->docConfig = new DocumentationConfig(config($this->configName)); |
| 187 | + } |
| 188 | + |
| 189 | +} |
0 commit comments