]> BookStack Code Mirror - bookstack/blob - app/Api/ApiDocsGenerator.php
Merge branch 'feature/sort-shelf-books' of git://github.com/guillaumehanotel/BookStac...
[bookstack] / app / Api / ApiDocsGenerator.php
1 <?php namespace BookStack\Api;
2
3 use BookStack\Http\Controllers\Api\ApiController;
4 use Illuminate\Contracts\Container\BindingResolutionException;
5 use Illuminate\Support\Collection;
6 use Illuminate\Support\Facades\Cache;
7 use Illuminate\Support\Facades\Route;
8 use Illuminate\Support\Str;
9 use ReflectionClass;
10 use ReflectionException;
11 use ReflectionMethod;
12
13 class ApiDocsGenerator
14 {
15
16     protected $reflectionClasses = [];
17     protected $controllerClasses = [];
18
19     /**
20      * Load the docs form the cache if existing
21      * otherwise generate and store in the cache.
22      */
23     public static function generateConsideringCache(): Collection
24     {
25         $appVersion = trim(file_get_contents(base_path('version')));
26         $cacheKey = 'api-docs::' . $appVersion;
27         if (Cache::has($cacheKey) && config('app.env') === 'production') {
28             $docs = Cache::get($cacheKey);
29         } else {
30             $docs = (new static())->generate();
31             Cache::put($cacheKey, $docs, 60 * 24);
32         }
33         return $docs;
34     }
35
36     /**
37      * Generate API documentation.
38      */
39     protected function generate(): Collection
40     {
41         $apiRoutes = $this->getFlatApiRoutes();
42         $apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
43         $apiRoutes = $this->loadDetailsFromFiles($apiRoutes);
44         $apiRoutes = $apiRoutes->groupBy('base_model');
45         return $apiRoutes;
46     }
47
48     /**
49      * Load any API details stored in static files.
50      */
51     protected function loadDetailsFromFiles(Collection $routes): Collection
52     {
53         return $routes->map(function (array $route) {
54             $exampleTypes = ['request', 'response'];
55             foreach ($exampleTypes as $exampleType) {
56                 $exampleFile = base_path("dev/api/{$exampleType}s/{$route['name']}.json");
57                 $exampleContent = file_exists($exampleFile) ? file_get_contents($exampleFile) : null;
58                 $route["example_{$exampleType}"] = $exampleContent;
59             }
60             return $route;
61         });
62     }
63
64     /**
65      * Load any details we can fetch from the controller and its methods.
66      */
67     protected function loadDetailsFromControllers(Collection $routes): Collection
68     {
69         return $routes->map(function (array $route) {
70             $method = $this->getReflectionMethod($route['controller'], $route['controller_method']);
71             $comment = $method->getDocComment();
72             $route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null;
73             $route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']);
74             return $route;
75         });
76     }
77
78     /**
79      * Load body params and their rules by inspecting the given class and method name.
80      * @throws BindingResolutionException
81      */
82     protected function getBodyParamsFromClass(string $className, string $methodName): ?array
83     {
84         /** @var ApiController $class */
85         $class = $this->controllerClasses[$className] ?? null;
86         if ($class === null) {
87             $class = app()->make($className);
88             $this->controllerClasses[$className] = $class;
89         }
90
91         $rules = $class->getValdationRules()[$methodName] ?? [];
92         foreach ($rules as $param => $ruleString) {
93             $rules[$param] = explode('|', $ruleString);
94         }
95         return count($rules) > 0 ? $rules : null;
96     }
97
98     /**
99      * Parse out the description text from a class method comment.
100      */
101     protected function parseDescriptionFromMethodComment(string $comment)
102     {
103         $matches = [];
104         preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches);
105         return implode(' ', $matches[1] ?? []);
106     }
107
108     /**
109      * Get a reflection method from the given class name and method name.
110      * @throws ReflectionException
111      */
112     protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod
113     {
114         $class = $this->reflectionClasses[$className] ?? null;
115         if ($class === null) {
116             $class = new ReflectionClass($className);
117             $this->reflectionClasses[$className] = $class;
118         }
119
120         return $class->getMethod($methodName);
121     }
122
123     /**
124      * Get the system API routes, formatted into a flat collection.
125      */
126     protected function getFlatApiRoutes(): Collection
127     {
128         return collect(Route::getRoutes()->getRoutes())->filter(function ($route) {
129             return strpos($route->uri, 'api/') === 0;
130         })->map(function ($route) {
131             [$controller, $controllerMethod] = explode('@', $route->action['uses']);
132             $baseModelName = explode('.', explode('/', $route->uri)[1])[0];
133             $shortName = $baseModelName . '-' . $controllerMethod;
134             return [
135                 'name' => $shortName,
136                 'uri' => $route->uri,
137                 'method' => $route->methods[0],
138                 'controller' => $controller,
139                 'controller_method' => $controllerMethod,
140                 'controller_method_kebab' => Str::kebab($controllerMethod),
141                 'base_model' => $baseModelName,
142             ];
143         });
144     }
145 }