]> BookStack Code Mirror - bookstack/blob - app/Api/ApiDocsGenerator.php
Started work on generating API docs
[bookstack] / app / Api / ApiDocsGenerator.php
1 <?php namespace BookStack\Api;
2
3 use BookStack\Http\Controllers\Api\ApiController;
4 use Illuminate\Support\Collection;
5 use Illuminate\Support\Facades\Route;
6 use ReflectionClass;
7 use ReflectionException;
8 use ReflectionMethod;
9
10 class ApiDocsGenerator
11 {
12
13     protected $reflectionClasses = [];
14     protected $controllerClasses = [];
15
16     /**
17      * Generate API documentation.
18      */
19     public function generate(): Collection
20     {
21         $apiRoutes = $this->getFlatApiRoutes();
22         $apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
23         $apiRoutes = $this->loadDetailsFromFiles($apiRoutes);
24         $apiRoutes = $apiRoutes->groupBy('base_model');
25         return $apiRoutes;
26     }
27
28     /**
29      * Load any API details stored in static files.
30      */
31     protected function loadDetailsFromFiles(Collection $routes): Collection
32     {
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;
37             return $route;
38         });
39     }
40
41     /**
42      * Load any details we can fetch from the controller and its methods.
43      */
44     protected function loadDetailsFromControllers(Collection $routes): Collection
45     {
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']);
51             return $route;
52         });
53     }
54
55     /**
56      * Load body params and their rules by inspecting the given class and method name.
57      * @throws \Illuminate\Contracts\Container\BindingResolutionException
58      */
59     protected function getBodyParamsFromClass(string $className, string $methodName): ?array
60     {
61         /** @var ApiController $class */
62         $class = $this->controllerClasses[$className] ?? null;
63         if ($class === null) {
64             $class = app()->make($className);
65             $this->controllerClasses[$className] = $class;
66         }
67
68         $rules = $class->getValdationRules()[$methodName] ?? [];
69         foreach ($rules as $param => $ruleString) {
70             $rules[$param] = explode('|', $ruleString);
71         }
72         return count($rules) > 0 ? $rules : null;
73     }
74
75     /**
76      * Parse out the description text from a class method comment.
77      */
78     protected function parseDescriptionFromMethodComment(string $comment)
79     {
80         $matches = [];
81         preg_match_all('/^\s*?\*\s((?![@\s]).*?)$/m', $comment, $matches);
82         return implode(' ', $matches[1] ?? []);
83     }
84
85     /**
86      * Get a reflection method from the given class name and method name.
87      * @throws ReflectionException
88      */
89     protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod
90     {
91         $class = $this->reflectionClasses[$className] ?? null;
92         if ($class === null) {
93             $class = new ReflectionClass($className);
94             $this->reflectionClasses[$className] = $class;
95         }
96
97         return $class->getMethod($methodName);
98     }
99
100     /**
101      * Get the system API routes, formatted into a flat collection.
102      */
103     protected function getFlatApiRoutes(): Collection
104     {
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;
111             return [
112                 'name' => $shortName,
113                 'uri' => $route->uri,
114                 'method' => $route->methods[0],
115                 'controller' => $controller,
116                 'controller_method' => $controllerMethod,
117                 'base_model' => $baseModelName,
118             ];
119         });
120     }
121
122 }