|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Mpociot\ApiDoc\Tools; |
|
4
|
|
|
|
|
5
|
|
|
use Faker\Factory; |
|
6
|
|
|
use ReflectionClass; |
|
7
|
|
|
use ReflectionMethod; |
|
8
|
|
|
use Illuminate\Routing\Route; |
|
9
|
|
|
use Mpociot\Reflection\DocBlock; |
|
10
|
|
|
use Mpociot\Reflection\DocBlock\Tag; |
|
11
|
|
|
use Mpociot\ApiDoc\Tools\Traits\ParamHelpers; |
|
12
|
|
|
|
|
13
|
|
|
class Generator |
|
14
|
|
|
{ |
|
15
|
|
|
use ParamHelpers; |
|
16
|
|
|
|
|
17
|
|
|
/** |
|
18
|
|
|
* @var DocumentationConfig |
|
19
|
|
|
*/ |
|
20
|
|
|
private $config; |
|
21
|
|
|
|
|
22
|
|
|
public function __construct(DocumentationConfig $config = null) |
|
23
|
|
|
{ |
|
24
|
|
|
// If no config is injected, pull from global |
|
25
|
|
|
$this->config = $config ?: new DocumentationConfig(config('apidoc')); |
|
26
|
|
|
} |
|
27
|
|
|
|
|
28
|
|
|
/** |
|
29
|
|
|
* @param Route $route |
|
30
|
|
|
* |
|
31
|
|
|
* @return mixed |
|
32
|
|
|
*/ |
|
33
|
|
|
public function getUri(Route $route) |
|
34
|
|
|
{ |
|
35
|
|
|
return $route->uri(); |
|
36
|
|
|
} |
|
37
|
|
|
|
|
38
|
|
|
/** |
|
39
|
|
|
* @param Route $route |
|
40
|
|
|
* |
|
41
|
|
|
* @return mixed |
|
42
|
|
|
*/ |
|
43
|
|
|
public function getMethods(Route $route) |
|
44
|
|
|
{ |
|
45
|
|
|
return array_diff($route->methods(), ['HEAD']); |
|
46
|
|
|
} |
|
47
|
|
|
|
|
48
|
|
|
/** |
|
49
|
|
|
* @param \Illuminate\Routing\Route $route |
|
50
|
|
|
* @param array $rulesToApply Rules to apply when generating documentation for this route |
|
51
|
|
|
* |
|
52
|
|
|
* @return array |
|
53
|
|
|
*/ |
|
54
|
|
|
public function processRoute(Route $route, array $rulesToApply = []) |
|
55
|
|
|
{ |
|
56
|
|
|
list($class, $method) = Utils::getRouteActionUses($route->getAction()); |
|
57
|
|
|
$controller = new ReflectionClass($class); |
|
58
|
|
|
$method = $controller->getMethod($method); |
|
59
|
|
|
|
|
60
|
|
|
list($routeGroupName, $routeGroupDescription) = $this->getRouteGroup($controller, $method); |
|
61
|
|
|
|
|
62
|
|
|
$docBlock = $this->parseDocBlock($method); |
|
63
|
|
|
$bodyParameters = $this->getBodyParameters($method, $docBlock['tags']); |
|
64
|
|
|
$queryParameters = $this->getQueryParameters($method, $docBlock['tags']); |
|
65
|
|
|
$content = ResponseResolver::getResponse($route, $docBlock['tags'], [ |
|
66
|
|
|
'rules' => $rulesToApply, |
|
67
|
|
|
'body' => $bodyParameters, |
|
68
|
|
|
'query' => $queryParameters, |
|
69
|
|
|
]); |
|
70
|
|
|
|
|
71
|
|
|
$parsedRoute = [ |
|
72
|
|
|
'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))), |
|
73
|
|
|
'groupName' => $routeGroupName, |
|
74
|
|
|
'groupDescription' => $routeGroupDescription, |
|
75
|
|
|
'title' => $docBlock['short'], |
|
76
|
|
|
'description' => $docBlock['long'], |
|
77
|
|
|
'methods' => $this->getMethods($route), |
|
78
|
|
|
'uri' => $this->getUri($route), |
|
79
|
|
|
'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])), |
|
80
|
|
|
'queryParameters' => $queryParameters, |
|
81
|
|
|
'bodyParameters' => $bodyParameters, |
|
82
|
|
|
'cleanBodyParameters' => $this->cleanParams($bodyParameters), |
|
83
|
|
|
'cleanQueryParameters' => $this->cleanParams($queryParameters), |
|
84
|
|
|
'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']), |
|
85
|
|
|
'response' => $content, |
|
86
|
|
|
'showresponse' => ! empty($content), |
|
87
|
|
|
]; |
|
88
|
|
|
$parsedRoute['headers'] = $rulesToApply['headers'] ?? []; |
|
89
|
|
|
|
|
90
|
|
|
return $parsedRoute; |
|
91
|
|
|
} |
|
92
|
|
|
|
|
93
|
|
View Code Duplication |
protected function getBodyParameters(ReflectionMethod $method, array $tags) |
|
|
|
|
|
|
94
|
|
|
{ |
|
95
|
|
|
foreach ($method->getParameters() as $param) { |
|
96
|
|
|
$paramType = $param->getType(); |
|
97
|
|
|
if ($paramType === null) { |
|
98
|
|
|
continue; |
|
99
|
|
|
} |
|
100
|
|
|
|
|
101
|
|
|
$parameterClassName = version_compare(phpversion(), '7.1.0', '<') |
|
102
|
|
|
? $paramType->__toString() |
|
103
|
|
|
: $paramType->getName(); |
|
104
|
|
|
|
|
105
|
|
|
try { |
|
106
|
|
|
$parameterClass = new ReflectionClass($parameterClassName); |
|
107
|
|
|
} catch (\ReflectionException $e) { |
|
108
|
|
|
continue; |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
|
|
if (class_exists('\Illuminate\Foundation\Http\FormRequest') && $parameterClass->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class) || class_exists('\Dingo\Api\Http\FormRequest') && $parameterClass->isSubclassOf(\Dingo\Api\Http\FormRequest::class)) { |
|
112
|
|
|
$formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); |
|
113
|
|
|
$bodyParametersFromDocBlock = $this->getBodyParametersFromDocBlock($formRequestDocBlock->getTags()); |
|
114
|
|
|
|
|
115
|
|
|
if (count($bodyParametersFromDocBlock)) { |
|
116
|
|
|
return $bodyParametersFromDocBlock; |
|
117
|
|
|
} |
|
118
|
|
|
} |
|
119
|
|
|
} |
|
120
|
|
|
|
|
121
|
|
|
return $this->getBodyParametersFromDocBlock($tags); |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
/** |
|
125
|
|
|
* @param array $tags |
|
126
|
|
|
* |
|
127
|
|
|
* @return array |
|
128
|
|
|
*/ |
|
129
|
|
|
protected function getBodyParametersFromDocBlock(array $tags) |
|
130
|
|
|
{ |
|
131
|
|
|
$parameters = collect($tags) |
|
132
|
|
|
->filter(function ($tag) { |
|
133
|
|
|
return $tag instanceof Tag && $tag->getName() === 'bodyParam'; |
|
|
|
|
|
|
134
|
|
|
}) |
|
135
|
|
|
->filter(function (Tag $tag) { |
|
136
|
|
|
return ! $this->shouldExcludeExample($tag); |
|
137
|
|
|
}) |
|
138
|
|
|
->mapWithKeys(function ($tag) { |
|
139
|
|
|
preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); |
|
140
|
|
View Code Duplication |
if (empty($content)) { |
|
|
|
|
|
|
141
|
|
|
// this means only name and type were supplied |
|
142
|
|
|
list($name, $type) = preg_split('/\s+/', $tag->getContent()); |
|
143
|
|
|
$required = false; |
|
144
|
|
|
$description = ''; |
|
145
|
|
|
} else { |
|
146
|
|
|
list($_, $name, $type, $required, $description) = $content; |
|
|
|
|
|
|
147
|
|
|
$description = trim($description); |
|
148
|
|
|
if ($description == 'required' && empty(trim($required))) { |
|
149
|
|
|
$required = $description; |
|
150
|
|
|
$description = ''; |
|
151
|
|
|
} |
|
152
|
|
|
$required = trim($required) == 'required' ? true : false; |
|
153
|
|
|
} |
|
154
|
|
|
|
|
155
|
|
|
$type = $this->normalizeParameterType($type); |
|
156
|
|
|
list($description, $example) = $this->parseDescription($description, $type); |
|
157
|
|
|
$value = is_null($example) ? $this->generateDummyValue($type) : $example; |
|
158
|
|
|
|
|
159
|
|
|
return [$name => compact('type', 'description', 'required', 'value')]; |
|
160
|
|
|
})->toArray(); |
|
161
|
|
|
|
|
162
|
|
|
return $parameters; |
|
163
|
|
|
} |
|
164
|
|
|
|
|
165
|
|
|
/** |
|
166
|
|
|
* @param ReflectionMethod $method |
|
167
|
|
|
* @param array $tags |
|
168
|
|
|
* |
|
169
|
|
|
* @return array |
|
170
|
|
|
*/ |
|
171
|
|
View Code Duplication |
protected function getQueryParameters(ReflectionMethod $method, array $tags) |
|
|
|
|
|
|
172
|
|
|
{ |
|
173
|
|
|
foreach ($method->getParameters() as $param) { |
|
174
|
|
|
$paramType = $param->getType(); |
|
175
|
|
|
if ($paramType === null) { |
|
176
|
|
|
continue; |
|
177
|
|
|
} |
|
178
|
|
|
|
|
179
|
|
|
$parameterClassName = version_compare(phpversion(), '7.1.0', '<') |
|
180
|
|
|
? $paramType->__toString() |
|
181
|
|
|
: $paramType->getName(); |
|
182
|
|
|
|
|
183
|
|
|
try { |
|
184
|
|
|
$parameterClass = new ReflectionClass($parameterClassName); |
|
185
|
|
|
} catch (\ReflectionException $e) { |
|
186
|
|
|
continue; |
|
187
|
|
|
} |
|
188
|
|
|
|
|
189
|
|
|
if (class_exists('\Illuminate\Foundation\Http\FormRequest') && $parameterClass->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class) || class_exists('\Dingo\Api\Http\FormRequest') && $parameterClass->isSubclassOf(\Dingo\Api\Http\FormRequest::class)) { |
|
190
|
|
|
$formRequestDocBlock = new DocBlock($parameterClass->getDocComment()); |
|
191
|
|
|
$queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags()); |
|
192
|
|
|
|
|
193
|
|
|
if (count($queryParametersFromDocBlock)) { |
|
194
|
|
|
return $queryParametersFromDocBlock; |
|
195
|
|
|
} |
|
196
|
|
|
} |
|
197
|
|
|
} |
|
198
|
|
|
|
|
199
|
|
|
return $this->getQueryParametersFromDocBlock($tags); |
|
200
|
|
|
} |
|
201
|
|
|
|
|
202
|
|
|
/** |
|
203
|
|
|
* @param array $tags |
|
204
|
|
|
* |
|
205
|
|
|
* @return array |
|
206
|
|
|
*/ |
|
207
|
|
|
protected function getQueryParametersFromDocBlock(array $tags) |
|
208
|
|
|
{ |
|
209
|
|
|
$parameters = collect($tags) |
|
210
|
|
|
->filter(function ($tag) { |
|
211
|
|
|
return $tag instanceof Tag && $tag->getName() === 'queryParam'; |
|
|
|
|
|
|
212
|
|
|
}) |
|
213
|
|
|
->filter(function (Tag $tag) { |
|
214
|
|
|
return ! $this->shouldExcludeExample($tag); |
|
215
|
|
|
}) |
|
216
|
|
|
->mapWithKeys(function ($tag) { |
|
217
|
|
|
preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content); |
|
218
|
|
View Code Duplication |
if (empty($content)) { |
|
|
|
|
|
|
219
|
|
|
// this means only name was supplied |
|
220
|
|
|
list($name) = preg_split('/\s+/', $tag->getContent()); |
|
221
|
|
|
$required = false; |
|
222
|
|
|
$description = ''; |
|
223
|
|
|
} else { |
|
224
|
|
|
list($_, $name, $required, $description) = $content; |
|
|
|
|
|
|
225
|
|
|
$description = trim($description); |
|
226
|
|
|
if ($description == 'required' && empty(trim($required))) { |
|
227
|
|
|
$required = $description; |
|
228
|
|
|
$description = ''; |
|
229
|
|
|
} |
|
230
|
|
|
$required = trim($required) == 'required' ? true : false; |
|
231
|
|
|
} |
|
232
|
|
|
|
|
233
|
|
|
list($description, $value) = $this->parseDescription($description, 'string'); |
|
234
|
|
|
if (is_null($value)) { |
|
235
|
|
|
$value = str_contains($description, ['number', 'count', 'page']) |
|
236
|
|
|
? $this->generateDummyValue('integer') |
|
237
|
|
|
: $this->generateDummyValue('string'); |
|
238
|
|
|
} |
|
239
|
|
|
|
|
240
|
|
|
return [$name => compact('description', 'required', 'value')]; |
|
241
|
|
|
})->toArray(); |
|
242
|
|
|
|
|
243
|
|
|
return $parameters; |
|
244
|
|
|
} |
|
245
|
|
|
|
|
246
|
|
|
/** |
|
247
|
|
|
* @param array $tags |
|
248
|
|
|
* |
|
249
|
|
|
* @return bool |
|
250
|
|
|
*/ |
|
251
|
|
|
protected function getAuthStatusFromDocBlock(array $tags) |
|
252
|
|
|
{ |
|
253
|
|
|
$authTag = collect($tags) |
|
254
|
|
|
->first(function ($tag) { |
|
255
|
|
|
return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated'; |
|
|
|
|
|
|
256
|
|
|
}); |
|
257
|
|
|
|
|
258
|
|
|
return (bool) $authTag; |
|
259
|
|
|
} |
|
260
|
|
|
|
|
261
|
|
|
/** |
|
262
|
|
|
* @param ReflectionMethod $method |
|
263
|
|
|
* |
|
264
|
|
|
* @return array |
|
265
|
|
|
*/ |
|
266
|
|
|
protected function parseDocBlock(ReflectionMethod $method) |
|
267
|
|
|
{ |
|
268
|
|
|
$comment = $method->getDocComment(); |
|
269
|
|
|
$phpdoc = new DocBlock($comment); |
|
270
|
|
|
|
|
271
|
|
|
return [ |
|
272
|
|
|
'short' => $phpdoc->getShortDescription(), |
|
273
|
|
|
'long' => $phpdoc->getLongDescription()->getContents(), |
|
274
|
|
|
'tags' => $phpdoc->getTags(), |
|
275
|
|
|
]; |
|
276
|
|
|
} |
|
277
|
|
|
|
|
278
|
|
|
/** |
|
279
|
|
|
* @param ReflectionClass $controller |
|
280
|
|
|
* @param ReflectionMethod $method |
|
281
|
|
|
* |
|
282
|
|
|
* @return array The route group name and description |
|
283
|
|
|
*/ |
|
284
|
|
|
protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $method) |
|
285
|
|
|
{ |
|
286
|
|
|
// @group tag on the method overrides that on the controller |
|
287
|
|
|
$docBlockComment = $method->getDocComment(); |
|
288
|
|
View Code Duplication |
if ($docBlockComment) { |
|
|
|
|
|
|
289
|
|
|
$phpdoc = new DocBlock($docBlockComment); |
|
290
|
|
|
foreach ($phpdoc->getTags() as $tag) { |
|
291
|
|
|
if ($tag->getName() === 'group') { |
|
292
|
|
|
$routeGroup = trim($tag->getContent()); |
|
|
|
|
|
|
293
|
|
|
$routeGroupParts = explode("\n", $tag->getContent()); |
|
294
|
|
|
$routeGroupName = array_shift($routeGroupParts); |
|
295
|
|
|
$routeGroupDescription = implode("\n", $routeGroupParts); |
|
296
|
|
|
|
|
297
|
|
|
return [$routeGroupName, $routeGroupDescription]; |
|
298
|
|
|
} |
|
299
|
|
|
} |
|
300
|
|
|
} |
|
301
|
|
|
|
|
302
|
|
|
$docBlockComment = $controller->getDocComment(); |
|
303
|
|
View Code Duplication |
if ($docBlockComment) { |
|
|
|
|
|
|
304
|
|
|
$phpdoc = new DocBlock($docBlockComment); |
|
305
|
|
|
foreach ($phpdoc->getTags() as $tag) { |
|
306
|
|
|
if ($tag->getName() === 'group') { |
|
307
|
|
|
$routeGroupParts = explode("\n", $tag->getContent()); |
|
308
|
|
|
$routeGroupName = array_shift($routeGroupParts); |
|
309
|
|
|
$routeGroupDescription = implode("\n", $routeGroupParts); |
|
310
|
|
|
|
|
311
|
|
|
return [$routeGroupName, $routeGroupDescription]; |
|
312
|
|
|
} |
|
313
|
|
|
} |
|
314
|
|
|
} |
|
315
|
|
|
|
|
316
|
|
|
return [$this->config->get(('default_group')), '']; |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
private function normalizeParameterType($type) |
|
320
|
|
|
{ |
|
321
|
|
|
$typeMap = [ |
|
322
|
|
|
'int' => 'integer', |
|
323
|
|
|
'bool' => 'boolean', |
|
324
|
|
|
'double' => 'float', |
|
325
|
|
|
]; |
|
326
|
|
|
|
|
327
|
|
|
return $type ? ($typeMap[$type] ?? $type) : 'string'; |
|
328
|
|
|
} |
|
329
|
|
|
|
|
330
|
|
|
private function generateDummyValue(string $type) |
|
331
|
|
|
{ |
|
332
|
|
|
$faker = Factory::create(); |
|
333
|
|
|
if ($this->config->get('faker_seed')) { |
|
334
|
|
|
$faker->seed($this->config->get('faker_seed')); |
|
335
|
|
|
} |
|
336
|
|
|
$fakeFactories = [ |
|
337
|
|
|
'integer' => function () use ($faker) { |
|
338
|
|
|
return $faker->numberBetween(1, 20); |
|
339
|
|
|
}, |
|
340
|
|
|
'number' => function () use ($faker) { |
|
341
|
|
|
return $faker->randomFloat(); |
|
342
|
|
|
}, |
|
343
|
|
|
'float' => function () use ($faker) { |
|
344
|
|
|
return $faker->randomFloat(); |
|
345
|
|
|
}, |
|
346
|
|
|
'boolean' => function () use ($faker) { |
|
347
|
|
|
return $faker->boolean(); |
|
348
|
|
|
}, |
|
349
|
|
|
'string' => function () use ($faker) { |
|
350
|
|
|
return $faker->word; |
|
351
|
|
|
}, |
|
352
|
|
|
'array' => function () { |
|
353
|
|
|
return []; |
|
354
|
|
|
}, |
|
355
|
|
|
'object' => function () { |
|
356
|
|
|
return new \stdClass; |
|
357
|
|
|
}, |
|
358
|
|
|
]; |
|
359
|
|
|
|
|
360
|
|
|
$fakeFactory = $fakeFactories[$type] ?? $fakeFactories['string']; |
|
361
|
|
|
|
|
362
|
|
|
return $fakeFactory(); |
|
363
|
|
|
} |
|
364
|
|
|
|
|
365
|
|
|
/** |
|
366
|
|
|
* Allows users to specify an example for the parameter by writing 'Example: the-example', |
|
367
|
|
|
* to be used in example requests and response calls. |
|
368
|
|
|
* |
|
369
|
|
|
* @param string $description |
|
370
|
|
|
* @param string $type The type of the parameter. Used to cast the example provided, if any. |
|
371
|
|
|
* |
|
372
|
|
|
* @return array The description and included example. |
|
373
|
|
|
*/ |
|
374
|
|
|
private function parseDescription(string $description, string $type) |
|
375
|
|
|
{ |
|
376
|
|
|
$example = null; |
|
377
|
|
|
if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) { |
|
378
|
|
|
$description = $content[1]; |
|
379
|
|
|
|
|
380
|
|
|
// examples are parsed as strings by default, we need to cast them properly |
|
381
|
|
|
$example = $this->castToType($content[2], $type); |
|
382
|
|
|
} |
|
383
|
|
|
|
|
384
|
|
|
return [$description, $example]; |
|
385
|
|
|
} |
|
386
|
|
|
|
|
387
|
|
|
/** |
|
388
|
|
|
* Allows users to specify that we shouldn't generate an example for the parameter |
|
389
|
|
|
* by writing 'No-example'. |
|
390
|
|
|
* |
|
391
|
|
|
* @param Tag $tag |
|
392
|
|
|
* |
|
393
|
|
|
* @return bool Whether no example should be generated |
|
394
|
|
|
*/ |
|
395
|
|
|
private function shouldExcludeExample(Tag $tag) |
|
396
|
|
|
{ |
|
397
|
|
|
return strpos($tag->getContent(), ' No-example') !== false; |
|
398
|
|
|
} |
|
399
|
|
|
|
|
400
|
|
|
/** |
|
401
|
|
|
* Cast a value from a string to a specified type. |
|
402
|
|
|
* |
|
403
|
|
|
* @param string $value |
|
404
|
|
|
* @param string $type |
|
405
|
|
|
* |
|
406
|
|
|
* @return mixed |
|
407
|
|
|
*/ |
|
408
|
|
|
private function castToType(string $value, string $type) |
|
409
|
|
|
{ |
|
410
|
|
|
$casts = [ |
|
411
|
|
|
'integer' => 'intval', |
|
412
|
|
|
'number' => 'floatval', |
|
413
|
|
|
'float' => 'floatval', |
|
414
|
|
|
'boolean' => 'boolval', |
|
415
|
|
|
]; |
|
416
|
|
|
|
|
417
|
|
|
// First, we handle booleans. We can't use a regular cast, |
|
418
|
|
|
//because PHP considers string 'false' as true. |
|
419
|
|
|
if ($value == 'false' && $type == 'boolean') { |
|
420
|
|
|
return false; |
|
421
|
|
|
} |
|
422
|
|
|
|
|
423
|
|
|
if (isset($casts[$type])) { |
|
424
|
|
|
return $casts[$type]($value); |
|
425
|
|
|
} |
|
426
|
|
|
|
|
427
|
|
|
return $value; |
|
428
|
|
|
} |
|
429
|
|
|
} |
|
430
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.