1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Yaro\ApiDocs; |
4
|
|
|
|
5
|
|
|
use ReflectionClass; |
6
|
|
|
use Illuminate\Routing\Router; |
7
|
|
|
use Illuminate\Contracts\Config\Repository as Config; |
8
|
|
|
use Illuminate\Http\Request; |
9
|
|
|
use Yaro\ApiDocs\Blueprint; |
10
|
|
|
|
11
|
|
|
class ApiDocs |
12
|
|
|
{ |
13
|
|
|
|
14
|
|
|
private $router; |
15
|
|
|
private $config; |
16
|
|
|
private $request; |
17
|
|
|
|
18
|
|
|
public function __construct(Router $router, Config $config, Request $request) |
19
|
|
|
{ |
20
|
|
|
$this->router = $router; |
21
|
|
|
$this->config = $config; |
22
|
|
|
$this->request = $request; |
23
|
|
|
} // end __construct |
24
|
|
|
|
25
|
|
|
public function show($routePrefix = null) |
26
|
|
|
{ |
27
|
|
|
$currentPrefix = $this->request->get('prefix', $this->getRoutePrefix($routePrefix)); |
28
|
|
|
$endpoints = $this->getEndpoints($currentPrefix); |
29
|
|
|
$endpoints = $this->getSortedEndpoints($endpoints); |
30
|
|
|
$prefixes = $this->getRoutePrefixes(); |
31
|
|
|
|
32
|
|
|
return view('apidocs::docs', compact('endpoints', 'prefixes', 'currentPrefix')); |
33
|
|
|
} // end show |
34
|
|
|
|
35
|
|
|
public function blueprint($routePrefix = null) |
36
|
|
|
{ |
37
|
|
|
$routePrefix = $this->request->get('prefix', $this->getRoutePrefix($routePrefix)); |
38
|
|
|
|
39
|
|
|
$blueprint = app()->make(Blueprint::class); |
40
|
|
|
$blueprint->setRoutePrefix($routePrefix); |
41
|
|
|
$blueprint->setEndpoints($this->getEndpoints($routePrefix)); |
42
|
|
|
|
43
|
|
|
return $blueprint; |
44
|
|
|
} // end blueprint |
45
|
|
|
|
46
|
|
|
private function getEndpoints($routePrefix) |
47
|
|
|
{ |
48
|
|
|
$endpoints = []; |
49
|
|
|
|
50
|
|
|
foreach ($this->router->getRoutes() as $route) { |
51
|
|
|
if (!$this->isPrefixedRoute($route, $routePrefix) || $this->isClosureRoute($route) || $this->isExcluded($route)) { |
52
|
|
|
continue; |
53
|
|
|
} |
54
|
|
|
|
55
|
|
|
$actionController = explode("@", $this->getRouteParam($route, 'action.controller')); |
56
|
|
|
$class = $actionController[0]; |
57
|
|
|
$method = $actionController[1]; |
58
|
|
|
|
59
|
|
|
if (!class_exists($class) || !method_exists($class, $method)) { |
60
|
|
|
continue; |
61
|
|
|
} |
62
|
|
|
|
63
|
|
|
list($title, $description, $params) = $this->getRouteDocBlock($class, $method); |
64
|
|
|
$key = $this->generateEndpointKey($class); |
65
|
|
|
|
66
|
|
|
$endpoints[$key][] = [ |
67
|
|
|
'hash' => $this->generateHashForUrl($key, $route, $method), |
68
|
|
|
'uri' => $this->getRouteParam($route, 'uri'), |
69
|
|
|
'name' => $method, |
70
|
|
|
'methods' => $this->getRouteParam($route, 'methods'), |
71
|
|
|
'docs' => [ |
72
|
|
|
'title' => $title, |
73
|
|
|
'description' => trim($description), |
74
|
|
|
'params' => $params, |
75
|
|
|
'uri_params' => $this->getUriParams($route), |
76
|
|
|
], |
77
|
|
|
]; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
return $endpoints; |
81
|
|
|
} // end getEndpoints |
82
|
|
|
|
83
|
|
|
private function isExcluded($route) |
84
|
|
|
{ |
85
|
|
|
$uri = $this->getRouteParam($route, 'uri'); |
86
|
|
|
$actionController = $this->getRouteParam($route, 'action.controller'); |
87
|
|
|
|
88
|
|
|
return $this->isExcludedClass($actionController) || $this->isExcludedRoute($uri); |
89
|
|
|
} // end isExcluded |
90
|
|
|
|
91
|
|
|
private function isExcludedRoute($uri) |
92
|
|
|
{ |
93
|
|
|
foreach ($this->config->get('yaro.apidocs.exclude.routes', []) as $pattern) { |
94
|
|
|
if (str_is($pattern, $uri)) { |
95
|
|
|
return true; |
96
|
|
|
} |
97
|
|
|
} |
98
|
|
|
|
99
|
|
|
return false; |
100
|
|
|
} // end isExcludedRoute |
101
|
|
|
|
102
|
|
|
private function isExcludedClass($actionController) |
103
|
|
|
{ |
104
|
|
|
foreach ($this->config->get('yaro.apidocs.exclude.classes', []) as $pattern) { |
105
|
|
|
if (str_is($pattern, $actionController)) { |
106
|
|
|
return true; |
107
|
|
|
} |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
return false; |
111
|
|
|
} // end isExcludedClass |
112
|
|
|
|
113
|
|
|
private function isPrefixedRoute($route, $routePrefix) |
114
|
|
|
{ |
115
|
|
|
$regexp = '~^'. preg_quote($routePrefix) .'~'; |
116
|
|
|
|
117
|
|
|
return preg_match($regexp, $this->getRouteParam($route, 'uri')); |
118
|
|
|
} // end isPrefixedRoute |
119
|
|
|
|
120
|
|
|
private function getRoutePrefix($routePrefix) |
121
|
|
|
{ |
122
|
|
|
$prefixes = $this->getRoutePrefixes(); |
123
|
|
|
|
124
|
|
|
return in_array($routePrefix, $prefixes) ? $routePrefix : array_shift($prefixes); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
private function getRoutePrefixes() |
128
|
|
|
{ |
129
|
|
|
$prefixes = $this->config->get('yaro.apidocs.prefix', 'api'); |
130
|
|
|
if (!is_array($prefixes)) { |
131
|
|
|
$prefixes = [$prefixes]; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
return $prefixes; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
private function isClosureRoute($route) |
138
|
|
|
{ |
139
|
|
|
$action = $this->getRouteParam($route, 'action.uses'); |
140
|
|
|
|
141
|
|
|
return is_object($action); |
142
|
|
|
} // end isClosureRoute |
143
|
|
|
|
144
|
|
|
private function getRouteDocBlock($class, $method) |
145
|
|
|
{ |
146
|
|
|
$reflector = new ReflectionClass($class); |
147
|
|
|
|
148
|
|
|
$title = implode(' ', $this->splitCamelCaseToWords($method)); |
149
|
|
|
$title = ucfirst(strtolower($title)); |
150
|
|
|
$description = ''; |
151
|
|
|
$params = []; |
152
|
|
|
|
153
|
|
|
$docs = explode("\n", $reflector->getMethod($method)->getDocComment()); |
154
|
|
|
$docs = array_filter($docs); |
155
|
|
|
if (!$docs) { |
|
|
|
|
156
|
|
|
return [$title, $description, $params]; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
$docs = $this->filterDocBlock($docs); |
160
|
|
|
|
161
|
|
|
$title = array_shift($docs); |
162
|
|
|
|
163
|
|
|
$checkForLongDescription = true; |
164
|
|
|
foreach ($docs as $line) { |
165
|
|
|
if ($checkForLongDescription && !preg_match('~^@\w+~', $line)) { |
166
|
|
|
$description .= trim($line) .' '; |
167
|
|
|
} elseif (preg_match('~^@\w+~', $line)) { |
168
|
|
|
$checkForLongDescription = false; |
169
|
|
|
if (preg_match('~^@param~', $line)) { |
170
|
|
|
$paramChunks = $this->getParamChunksFromLine($line); |
171
|
|
|
|
172
|
|
|
$paramType = array_shift($paramChunks); |
173
|
|
|
$paramName = substr(array_shift($paramChunks), 1); |
174
|
|
|
$params[$paramName] = [ |
175
|
|
|
'type' => $paramType, |
176
|
|
|
'name' => $paramName, |
177
|
|
|
'description' => implode(' ', $paramChunks), |
178
|
|
|
]; |
179
|
|
|
} |
180
|
|
|
} |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
return [$title, $description, $params]; |
184
|
|
|
} // end getRouteDocBlock |
185
|
|
|
|
186
|
|
|
private function getParamChunksFromLine($line) |
187
|
|
|
{ |
188
|
|
|
$paramChunks = explode(' ', $line); |
189
|
|
|
$paramChunks = array_filter($paramChunks, function($val) { |
190
|
|
|
return $val !== ''; |
191
|
|
|
}); |
192
|
|
|
unset($paramChunks[0]); |
193
|
|
|
|
194
|
|
|
return $paramChunks; |
195
|
|
|
} // end getParamChunksFromLine |
196
|
|
|
|
197
|
|
|
private function filterDocBlock($docs) |
198
|
|
|
{ |
199
|
|
|
foreach ($docs as &$line) { |
200
|
|
|
$line = preg_replace('~\s*\*\s*~', '', $line); |
201
|
|
|
$line = preg_replace('~^/$~', '', $line); |
202
|
|
|
} |
203
|
|
|
$docs = array_values(array_filter($docs)); |
204
|
|
|
|
205
|
|
|
return $docs; |
206
|
|
|
} // end filterDocBlock |
207
|
|
|
|
208
|
|
|
private function generateEndpointKey($class) |
209
|
|
|
{ |
210
|
|
|
$disabledNamespaces = $this->config->get('yaro.apidocs.disabled_namespaces', []); |
211
|
|
|
|
212
|
|
|
$chunks = explode('\\', $class); |
213
|
|
|
foreach ($chunks as $index => $chunk) { |
214
|
|
|
if (in_array($chunk, $disabledNamespaces)) { |
215
|
|
|
unset($chunks[$index]); |
216
|
|
|
continue; |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
$chunk = preg_replace('~Controller$~', '', $chunk); |
220
|
|
|
if ($chunk) { |
221
|
|
|
$chunk = $this->splitCamelCaseToWords($chunk); |
222
|
|
|
$chunks[$index] = implode(' ', $chunk); |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
|
226
|
|
|
return implode('.', $chunks); |
227
|
|
|
} // end generateEndpointKey |
228
|
|
|
|
229
|
|
|
private function getSortedEndpoints($endpoints) |
230
|
|
|
{ |
231
|
|
|
ksort($endpoints); |
232
|
|
|
|
233
|
|
|
$sorted = array(); |
234
|
|
|
foreach ($endpoints as $key => $val) { |
235
|
|
|
$this->ins($sorted, explode('.', $key), $val); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
return $sorted; |
239
|
|
|
} // end getSortedEndpoints |
240
|
|
|
|
241
|
|
|
private function getUriParams($route) |
242
|
|
|
{ |
243
|
|
|
preg_match_all('~{(\w+)}~', $this->getRouteParam($route, 'uri'), $matches); |
244
|
|
|
|
245
|
|
|
return isset($matches[1]) ? $matches[1] : []; |
246
|
|
|
} // end getUriParams |
247
|
|
|
|
248
|
|
|
private function generateHashForUrl($key, $route, $method) |
249
|
|
|
{ |
250
|
|
|
$path = preg_replace('~\s+~', '-', $key); |
251
|
|
|
$httpMethod = $this->getRouteParam($route, 'methods.0'); |
252
|
|
|
$classMethod = implode('-', $this->splitCamelCaseToWords($method)); |
253
|
|
|
|
254
|
|
|
$hash = $path .'::'. $httpMethod .'::'. $classMethod; |
255
|
|
|
|
256
|
|
|
return strtolower($hash); |
257
|
|
|
} // end generateHashForUrl |
258
|
|
|
|
259
|
|
|
private function splitCamelCaseToWords($chunk) |
260
|
|
|
{ |
261
|
|
|
$splitCamelCaseRegexp = '/(?#! splitCamelCase Rev:20140412) |
262
|
|
|
# Split camelCase "words". Two global alternatives. Either g1of2: |
263
|
|
|
(?<=[a-z]) # Position is after a lowercase, |
264
|
|
|
(?=[A-Z]) # and before an uppercase letter. |
265
|
|
|
| (?<=[A-Z]) # Or g2of2; Position is after uppercase, |
266
|
|
|
(?=[A-Z][a-z]) # and before upper-then-lower case. |
267
|
|
|
/x'; |
268
|
|
|
|
269
|
|
|
return preg_split($splitCamelCaseRegexp, $chunk); |
270
|
|
|
} // end splitCamelCaseToWords |
271
|
|
|
|
272
|
|
|
private function getRouteParam($route, $param) |
273
|
|
|
{ |
274
|
|
|
$route = (array) $route; |
275
|
|
|
$prefix = chr(0).'*'.chr(0); |
276
|
|
|
|
277
|
|
|
return $this->arrayGet( |
278
|
|
|
$route, |
279
|
|
|
$prefix.$param, |
280
|
|
|
$this->arrayGet($route, $param) |
281
|
|
|
); |
282
|
|
|
} // end getRouteParam |
283
|
|
|
|
284
|
|
|
private function ins(&$ary, $keys, $val) |
285
|
|
|
{ |
286
|
|
|
$keys ? |
287
|
|
|
$this->ins($ary[array_shift($keys)], $keys, $val) : |
288
|
|
|
$ary = $val; |
289
|
|
|
} // end ins |
290
|
|
|
|
291
|
|
|
private function arrayGet($array, $key, $default = null) |
292
|
|
|
{ |
293
|
|
|
if (is_null($key)) { |
294
|
|
|
return $array; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
if (isset($array[$key])) { |
298
|
|
|
return $array[$key]; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
foreach (explode('.', $key) as $segment) { |
302
|
|
|
if (!is_array($array) || ! array_key_exists($segment, $array)) { |
303
|
|
|
return $default; |
304
|
|
|
} |
305
|
|
|
$array = $array[$segment]; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
return $array; |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
} |
312
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.