1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Yaro\ApiDocs; |
4
|
|
|
|
5
|
|
|
use ReflectionClass; |
6
|
|
|
use Illuminate\Routing\Router; |
7
|
|
|
|
8
|
|
|
class ApiDocs |
9
|
|
|
{ |
10
|
|
|
|
11
|
|
|
private $router; |
12
|
|
|
|
13
|
|
|
public function __construct(Router $router) |
14
|
|
|
{ |
15
|
|
|
$this->router = $router; |
16
|
|
|
} // end __construct |
17
|
|
|
|
18
|
|
|
public function show() |
19
|
|
|
{ |
20
|
|
|
$endpoints = $this->getEndpoints(); |
21
|
|
|
|
22
|
|
|
return view('apidocs::docs', compact('endpoints')); |
23
|
|
|
} // end show |
24
|
|
|
|
25
|
|
|
private function getEndpoints() |
26
|
|
|
{ |
27
|
|
|
$endpoints = []; |
28
|
|
|
|
29
|
|
|
foreach ($this->router->getRoutes() as $route) { |
30
|
|
|
if (!$this->isPrefixedRoute($route) || $this->isClosureRoute($route)) { |
31
|
|
|
continue; |
32
|
|
|
} |
33
|
|
|
|
34
|
|
|
$actionController = explode("@", $this->getRouteParam($route, 'action.controller')); |
35
|
|
|
$class = $actionController[0]; |
36
|
|
|
$method = $actionController[1]; |
37
|
|
|
|
38
|
|
|
if (!class_exists($class) || !method_exists($class, $method)) { |
39
|
|
|
continue; |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
list($title, $description, $params) = $this->getRouteDocBlock($class, $method); |
43
|
|
|
$key = $this->generateEndpointKey($class); |
44
|
|
|
|
45
|
|
|
$endpoints[$key][] = [ |
46
|
|
|
'hash' => $this->generateHashForUrl($key, $route, $method), |
47
|
|
|
'uri' => $this->getRouteParam($route, 'uri'), |
48
|
|
|
'name' => $method, |
49
|
|
|
'methods' => $this->getRouteParam($route, 'methods'), |
50
|
|
|
'docs' => [ |
51
|
|
|
'title' => $title, |
52
|
|
|
'description' => trim($description), |
53
|
|
|
'params' => $params, |
54
|
|
|
'uri_params' => $this->getUriParams($route), |
55
|
|
|
], |
56
|
|
|
]; |
57
|
|
|
} |
58
|
|
|
|
59
|
|
|
return $this->getSortedEndpoints($endpoints); |
60
|
|
|
} // end getEndpoints |
61
|
|
|
|
62
|
|
|
private function isPrefixedRoute($route) |
63
|
|
|
{ |
64
|
|
|
$prefix = config('yaro.apidocs.prefix', 'api'); |
65
|
|
|
$regexp = '~^'. preg_quote($prefix) .'~'; |
66
|
|
|
|
67
|
|
|
return preg_match($regexp, $this->getRouteParam($route, 'uri')); |
68
|
|
|
} // end isPrefixedRoute |
69
|
|
|
|
70
|
|
|
private function isClosureRoute($route) |
71
|
|
|
{ |
72
|
|
|
$action = $this->getRouteParam($route, 'action.uses'); |
73
|
|
|
|
74
|
|
|
return is_object($action); |
75
|
|
|
} // end isClosureRoute |
76
|
|
|
|
77
|
|
|
private function getRouteDocBlock($class, $method) |
78
|
|
|
{ |
79
|
|
|
$reflector = new ReflectionClass($class); |
80
|
|
|
|
81
|
|
|
$title = implode(' ', $this->splitCamelCaseToWords($method)); |
82
|
|
|
$title = ucfirst(strtolower($title)); |
83
|
|
|
$description = ''; |
84
|
|
|
$params = []; |
85
|
|
|
|
86
|
|
|
$docs = explode("\n", $reflector->getMethod($method)->getDocComment()); |
87
|
|
|
$docs = array_filter($docs); |
88
|
|
|
if (!$docs) { |
|
|
|
|
89
|
|
|
return [$title, $description, $params]; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
$docs = $this->filterDocBlock($docs); |
93
|
|
|
|
94
|
|
|
$title = array_shift($docs); |
95
|
|
|
|
96
|
|
|
$checkForLongDescription = true; |
97
|
|
|
foreach ($docs as $line) { |
98
|
|
|
if ($checkForLongDescription && !preg_match('~^@\w+~', $line)) { |
99
|
|
|
$description .= trim($line) .' '; |
100
|
|
|
} elseif (preg_match('~^@\w+~', $line)) { |
101
|
|
|
$checkForLongDescription = false; |
102
|
|
|
if (preg_match('~^@param~', $line)) { |
103
|
|
|
$paramChunks = $this->getParamChunksFromLine($line); |
104
|
|
|
|
105
|
|
|
$paramType = array_shift($paramChunks); |
106
|
|
|
$paramName = substr(array_shift($paramChunks), 1); |
107
|
|
|
$params[$paramName] = [ |
108
|
|
|
'type' => $paramType, |
109
|
|
|
'name' => $paramName, |
110
|
|
|
'description' => implode(' ', $paramChunks), |
111
|
|
|
]; |
112
|
|
|
} |
113
|
|
|
} |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
return [$title, $description, $params]; |
117
|
|
|
} // end getRouteDocBlock |
118
|
|
|
|
119
|
|
|
private function getParamChunksFromLine($line) |
120
|
|
|
{ |
121
|
|
|
$paramChunks = explode(' ', $line); |
122
|
|
|
$paramChunks = array_filter($paramChunks, function($val) { |
123
|
|
|
return $val !== ''; |
124
|
|
|
}); |
125
|
|
|
unset($paramChunks[0]); |
126
|
|
|
|
127
|
|
|
return $paramChunks; |
128
|
|
|
} // end getParamChunksFromLine |
129
|
|
|
|
130
|
|
|
private function filterDocBlock($docs) |
131
|
|
|
{ |
132
|
|
|
foreach ($docs as &$line) { |
133
|
|
|
$line = preg_replace('~\s*\*\s*~', '', $line); |
134
|
|
|
$line = preg_replace('~^/$~', '', $line); |
135
|
|
|
} |
136
|
|
|
$docs = array_values(array_filter($docs)); |
137
|
|
|
|
138
|
|
|
return $docs; |
139
|
|
|
} // end filterDocBlock |
140
|
|
|
|
141
|
|
|
private function generateEndpointKey($class) |
142
|
|
|
{ |
143
|
|
|
$disabledNamespaces = config('yaro.apidocs.disabled_namespaces', []); |
144
|
|
|
|
145
|
|
|
$chunks = explode('\\', $class); |
146
|
|
|
foreach ($chunks as $index => $chunk) { |
147
|
|
|
if (in_array($chunk, $disabledNamespaces)) { |
148
|
|
|
unset($chunks[$index]); |
149
|
|
|
continue; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
$chunk = preg_replace('~Controller$~', '', $chunk); |
153
|
|
|
if ($chunk) { |
154
|
|
|
$chunk = $this->splitCamelCaseToWords($chunk); |
155
|
|
|
$chunks[$index] = implode(' ', $chunk); |
156
|
|
|
} |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
return implode('.', $chunks); |
160
|
|
|
} // end generateEndpointKey |
161
|
|
|
|
162
|
|
|
private function getSortedEndpoints($endpoints) |
163
|
|
|
{ |
164
|
|
|
ksort($endpoints); |
165
|
|
|
|
166
|
|
|
$sorted = array(); |
167
|
|
|
foreach($endpoints as $key => $val) { |
168
|
|
|
$this->ins($sorted, explode('.', $key), $val); |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
return $sorted; |
172
|
|
|
} // end getSortedEndpoints |
173
|
|
|
|
174
|
|
|
private function getUriParams($route) |
175
|
|
|
{ |
176
|
|
|
preg_match_all('~{(\w+)}~', $this->getRouteParam($route, 'uri'), $matches); |
177
|
|
|
|
178
|
|
|
return isset($matches[1]) ? $matches[1] : []; |
179
|
|
|
} // end getUriParams |
180
|
|
|
|
181
|
|
|
private function generateHashForUrl($key, $route, $method) |
182
|
|
|
{ |
183
|
|
|
$path = preg_replace('~\s+~', '-', $key); |
184
|
|
|
$httpMethod = $this->getRouteParam($route, 'methods.0'); |
185
|
|
|
$classMethod = implode('-', $this->splitCamelCaseToWords($method)); |
186
|
|
|
|
187
|
|
|
$hash = $path .'::'. $httpMethod .'::'. $classMethod; |
188
|
|
|
|
189
|
|
|
return strtolower($hash); |
190
|
|
|
} // end generateHashForUrl |
191
|
|
|
|
192
|
|
|
private function splitCamelCaseToWords($chunk) |
193
|
|
|
{ |
194
|
|
|
$splitCamelCaseRegexp = '/(?#! splitCamelCase Rev:20140412) |
195
|
|
|
# Split camelCase "words". Two global alternatives. Either g1of2: |
196
|
|
|
(?<=[a-z]) # Position is after a lowercase, |
197
|
|
|
(?=[A-Z]) # and before an uppercase letter. |
198
|
|
|
| (?<=[A-Z]) # Or g2of2; Position is after uppercase, |
199
|
|
|
(?=[A-Z][a-z]) # and before upper-then-lower case. |
200
|
|
|
/x'; |
201
|
|
|
|
202
|
|
|
return preg_split($splitCamelCaseRegexp, $chunk); |
203
|
|
|
} // end splitCamelCaseToWords |
204
|
|
|
|
205
|
|
|
private function getRouteParam($route, $param) |
206
|
|
|
{ |
207
|
|
|
$route = (array) $route; |
208
|
|
|
$prefix = chr(0).'*'.chr(0); |
209
|
|
|
|
210
|
|
|
return array_get( |
211
|
|
|
$route, |
212
|
|
|
$prefix.$param, |
213
|
|
|
array_get($route, $param) |
214
|
|
|
); |
215
|
|
|
} // end getRouteParam |
216
|
|
|
|
217
|
|
|
private function ins(&$ary, $keys, $val) |
218
|
|
|
{ |
219
|
|
|
$keys ? |
220
|
|
|
$this->ins($ary[array_shift($keys)], $keys, $val) : |
221
|
|
|
$ary = $val; |
222
|
|
|
} // end ins |
223
|
|
|
|
224
|
|
|
} |
225
|
|
|
|
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.