1 <?php namespace BookStack\Api;
3 use BookStack\Http\Controllers\Api\ApiController;
4 use Illuminate\Support\Collection;
5 use Illuminate\Support\Facades\Route;
7 use ReflectionException;
10 class ApiDocsGenerator
13 protected $reflectionClasses = [];
14 protected $controllerClasses = [];
17 * Generate API documentation.
19 public function generate(): Collection
21 $apiRoutes = $this->getFlatApiRoutes();
22 $apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
23 $apiRoutes = $this->loadDetailsFromFiles($apiRoutes);
24 $apiRoutes = $apiRoutes->groupBy('base_model');
29 * Load any API details stored in static files.
31 protected function loadDetailsFromFiles(Collection $routes): Collection
33 return $routes->map(function (array $route) {
34 $exampleResponseFile = base_path('dev/api/responses/' . $route['name'] . '.json');
35 $exampleResponse = file_exists($exampleResponseFile) ? file_get_contents($exampleResponseFile) : null;
36 $route['example_response'] = $exampleResponse;
42 * Load any details we can fetch from the controller and its methods.
44 protected function loadDetailsFromControllers(Collection $routes): Collection
46 return $routes->map(function (array $route) {
47 $method = $this->getReflectionMethod($route['controller'], $route['controller_method']);
48 $comment = $method->getDocComment();
49 $route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null;
50 $route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']);
56 * Load body params and their rules by inspecting the given class and method name.
57 * @throws \Illuminate\Contracts\Container\BindingResolutionException
59 protected function getBodyParamsFromClass(string $className, string $methodName): ?array
61 /** @var ApiController $class */
62 $class = $this->controllerClasses[$className] ?? null;
63 if ($class === null) {
64 $class = app()->make($className);
65 $this->controllerClasses[$className] = $class;
68 $rules = $class->getValdationRules()[$methodName] ?? [];
69 foreach ($rules as $param => $ruleString) {
70 $rules[$param] = explode('|', $ruleString);
72 return count($rules) > 0 ? $rules : null;
76 * Parse out the description text from a class method comment.
78 protected function parseDescriptionFromMethodComment(string $comment)
81 preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches);
82 return implode(' ', $matches[1] ?? []);
86 * Get a reflection method from the given class name and method name.
87 * @throws ReflectionException
89 protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod
91 $class = $this->reflectionClasses[$className] ?? null;
92 if ($class === null) {
93 $class = new ReflectionClass($className);
94 $this->reflectionClasses[$className] = $class;
97 return $class->getMethod($methodName);
101 * Get the system API routes, formatted into a flat collection.
103 protected function getFlatApiRoutes(): Collection
105 return collect(Route::getRoutes()->getRoutes())->filter(function ($route) {
106 return strpos($route->uri, 'api/') === 0;
107 })->map(function ($route) {
108 [$controller, $controllerMethod] = explode('@', $route->action['uses']);
109 $baseModelName = explode('/', $route->uri)[1];
110 $shortName = $baseModelName . '-' . $controllerMethod;
112 'name' => $shortName,
113 'uri' => $route->uri,
114 'method' => $route->methods[0],
115 'controller' => $controller,
116 'controller_method' => $controllerMethod,
117 'base_model' => $baseModelName,