| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace BookStack\Api; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | use BookStack\Http\Controllers\Api\ApiController; | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  | use Illuminate\Contracts\Container\BindingResolutionException; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  | use Illuminate\Support\Collection; | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  | use Illuminate\Support\Facades\Cache; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  | use Illuminate\Support\Facades\Route; | 
					
						
							| 
									
										
										
										
											2020-04-10 23:05:17 +08:00
										 |  |  | use Illuminate\Support\Str; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  | use ReflectionClass; | 
					
						
							|  |  |  | use ReflectionException; | 
					
						
							|  |  |  | use ReflectionMethod; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ApiDocsGenerator | 
					
						
							|  |  |  | { | 
					
						
							|  |  |  |     protected $reflectionClasses = []; | 
					
						
							|  |  |  |     protected $controllerClasses = []; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Load the docs form the cache if existing | 
					
						
							|  |  |  |      * otherwise generate and store in the cache. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     public static function generateConsideringCache(): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $appVersion = trim(file_get_contents(base_path('version'))); | 
					
						
							|  |  |  |         $cacheKey = 'api-docs::' . $appVersion; | 
					
						
							|  |  |  |         if (Cache::has($cacheKey) && config('app.env') === 'production') { | 
					
						
							|  |  |  |             $docs = Cache::get($cacheKey); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |             $docs = (new static())->generate(); | 
					
						
							|  |  |  |             Cache::put($cacheKey, $docs, 60 * 24); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  |         return $docs; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * Generate API documentation. | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  |     protected function generate(): Collection | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |     { | 
					
						
							|  |  |  |         $apiRoutes = $this->getFlatApiRoutes(); | 
					
						
							|  |  |  |         $apiRoutes = $this->loadDetailsFromControllers($apiRoutes); | 
					
						
							|  |  |  |         $apiRoutes = $this->loadDetailsFromFiles($apiRoutes); | 
					
						
							|  |  |  |         $apiRoutes = $apiRoutes->groupBy('base_model'); | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |         return $apiRoutes; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Load any API details stored in static files. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function loadDetailsFromFiles(Collection $routes): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         return $routes->map(function (array $route) { | 
					
						
							| 
									
										
										
										
											2020-01-18 17:48:30 +08:00
										 |  |  |             $exampleTypes = ['request', 'response']; | 
					
						
							|  |  |  |             foreach ($exampleTypes as $exampleType) { | 
					
						
							|  |  |  |                 $exampleFile = base_path("dev/api/{$exampleType}s/{$route['name']}.json"); | 
					
						
							|  |  |  |                 $exampleContent = file_exists($exampleFile) ? file_get_contents($exampleFile) : null; | 
					
						
							|  |  |  |                 $route["example_{$exampleType}"] = $exampleContent; | 
					
						
							|  |  |  |             } | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |             return $route; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Load any details we can fetch from the controller and its methods. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function loadDetailsFromControllers(Collection $routes): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         return $routes->map(function (array $route) { | 
					
						
							|  |  |  |             $method = $this->getReflectionMethod($route['controller'], $route['controller_method']); | 
					
						
							|  |  |  |             $comment = $method->getDocComment(); | 
					
						
							|  |  |  |             $route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null; | 
					
						
							|  |  |  |             $route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']); | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |             return $route; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Load body params and their rules by inspecting the given class and method name. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2020-11-22 01:03:24 +08:00
										 |  |  |      * @throws BindingResolutionException | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |      */ | 
					
						
							|  |  |  |     protected function getBodyParamsFromClass(string $className, string $methodName): ?array | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         /** @var ApiController $class */ | 
					
						
							|  |  |  |         $class = $this->controllerClasses[$className] ?? null; | 
					
						
							|  |  |  |         if ($class === null) { | 
					
						
							|  |  |  |             $class = app()->make($className); | 
					
						
							|  |  |  |             $this->controllerClasses[$className] = $class; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         $rules = $class->getValdationRules()[$methodName] ?? []; | 
					
						
							|  |  |  |         foreach ($rules as $param => $ruleString) { | 
					
						
							|  |  |  |             $rules[$param] = explode('|', $ruleString); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |         return count($rules) > 0 ? $rules : null; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Parse out the description text from a class method comment. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function parseDescriptionFromMethodComment(string $comment) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $matches = []; | 
					
						
							|  |  |  |         preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches); | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |         return implode(' ', $matches[1] ?? []); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get a reflection method from the given class name and method name. | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |      * | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |      * @throws ReflectionException | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         $class = $this->reflectionClasses[$className] ?? null; | 
					
						
							|  |  |  |         if ($class === null) { | 
					
						
							|  |  |  |             $class = new ReflectionClass($className); | 
					
						
							|  |  |  |             $this->reflectionClasses[$className] = $class; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         return $class->getMethod($methodName); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Get the system API routes, formatted into a flat collection. | 
					
						
							|  |  |  |      */ | 
					
						
							|  |  |  |     protected function getFlatApiRoutes(): Collection | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         return collect(Route::getRoutes()->getRoutes())->filter(function ($route) { | 
					
						
							|  |  |  |             return strpos($route->uri, 'api/') === 0; | 
					
						
							|  |  |  |         })->map(function ($route) { | 
					
						
							|  |  |  |             [$controller, $controllerMethod] = explode('@', $route->action['uses']); | 
					
						
							| 
									
										
										
										
											2020-01-16 04:18:02 +08:00
										 |  |  |             $baseModelName = explode('.', explode('/', $route->uri)[1])[0]; | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |             $shortName = $baseModelName . '-' . $controllerMethod; | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |             return [ | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |                 'name'                    => $shortName, | 
					
						
							|  |  |  |                 'uri'                     => $route->uri, | 
					
						
							|  |  |  |                 'method'                  => $route->methods[0], | 
					
						
							|  |  |  |                 'controller'              => $controller, | 
					
						
							|  |  |  |                 'controller_method'       => $controllerMethod, | 
					
						
							| 
									
										
										
										
											2020-04-10 23:05:17 +08:00
										 |  |  |                 'controller_method_kebab' => Str::kebab($controllerMethod), | 
					
						
							| 
									
										
										
										
											2021-06-26 23:23:15 +08:00
										 |  |  |                 'base_model'              => $baseModelName, | 
					
						
							| 
									
										
										
										
											2020-01-13 00:25:14 +08:00
										 |  |  |             ]; | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-03-08 06:24:05 +08:00
										 |  |  | } |