Completed
Pull Request — master (#531)
by
unknown
01:35
created

Generator::getBodyParametersFromDocBlock()   B

Complexity

Conditions 7
Paths 1

Size

Total Lines 32

Duplication

Lines 14
Ratio 43.75 %

Importance

Changes 0
Metric Value
dl 14
loc 32
rs 8.4746
c 0
b 0
f 0
cc 7
nc 1
nop 1
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 $apply Rules to apply when generating documentation for this route
0 ignored issues
show
Bug introduced by
There is no parameter named $apply. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
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
        $routeGroup = $this->getRouteGroup($controller, $method);
61
        $docBlock = $this->parseDocBlock($method);
62
        $uriParameters = $this->getUriParameters($method, $docBlock['tags']);
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
            'group' => $routeGroup,
74
            'title' => $docBlock['short'],
75
            'description' => $docBlock['long'],
76
            'methods' => $this->getMethods($route),
77
            'uri' => $this->getUri($route),
78
            'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])),
79
            'uriParameters' => $uriParameters,
80
            'queryParameters' => $queryParameters,
81
            'bodyParameters' => $bodyParameters,
82
            'cleanUriParameters' => $this->cleanParams($uriParameters),
83
            'cleanBodyParameters' => $this->cleanParams($bodyParameters),
84
            'cleanQueryParameters' => $this->cleanParams($queryParameters),
85
            'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']),
86
            'response' => $content,
87
            'showresponse' => ! empty($content),
88
        ];
89
        $parsedRoute['headers'] = $rulesToApply['headers'] ?? [];
90
91
        return $parsedRoute;
92
    }
93
94
    /**
95
     * @param ReflectionMethod $method
96
     * @param array $tags
97
     *
98
     * @return array
99
     */
100
    protected function getUriParameters(ReflectionMethod $method, array $tags)
101
    {
102
        foreach ($method->getParameters() as $param) {
103
            $paramType = $param->getType();
104
            if ($paramType === null) {
105
                continue;
106
            }
107
108
            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
109
                ? $paramType->__toString()
110
                : $paramType->getName();
111
112
            try {
113
                $parameterClass = new ReflectionClass($parameterClassName);
114
            } catch (\ReflectionException $e) {
115
                continue;
116
            }
117
118
            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)) {
119
                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
120
                $uriParametersFromDocBlock = $this->getUriParametersFromDocBlock($formRequestDocBlock->getTags());
121
122
                if (count($uriParametersFromDocBlock)) {
123
                    return $uriParametersFromDocBlock;
124
                }
125
            }
126
        }
127
128
        return $this->getUriParametersFromDocBlock($tags);
129
    }
130
131
    /**
132
     * @param array $tags
133
     *
134
     * @return array
135
     */
136
    protected function getUriParametersFromDocBlock(array $tags)
137
    {
138
        $parameters = collect($tags)
139
            ->filter(function ($tag) {
140
                return $tag instanceof Tag && $tag->getName() === 'uriParam';
141
            })
142
            ->mapWithKeys(function ($tag) {
143
                preg_match('/(.+?)\s+(.+?)\s+?(.*)/', $tag->getContent(), $content);
144
                if (empty($content)) {
145
                    // this means only name and type were supplied
146
                    list($name, $type) = preg_split('/\s+/', $tag->getContent());
147
                    $required = false;
0 ignored issues
show
Unused Code introduced by
$required is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
148
                    $description = '';
149
                } else {
150
                    list($_, $name, $type, $description) = $content;
0 ignored issues
show
Unused Code introduced by
The assignment to $_ is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
151
                    $description = trim($description);
152
                }
153
154
                $type = $this->normalizeParameterType($type);
155
                list($description, $example) = $this->parseDescription($description, $type);
156
                $value = is_null($example) ? $this->generateDummyValue($type) : $example;
157
158
                return [$name => compact('type', 'description', 'value')];
159
            })->toArray();
160
161
        return $parameters;
162
    }
163
164 View Code Duplication
    protected function getBodyParameters(ReflectionMethod $method, array $tags)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
165
    {
166
        foreach ($method->getParameters() as $param) {
167
            $paramType = $param->getType();
168
            if ($paramType === null) {
169
                continue;
170
            }
171
172
            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
173
                ? $paramType->__toString()
174
                : $paramType->getName();
175
176
            try {
177
                $parameterClass = new ReflectionClass($parameterClassName);
178
            } catch (\ReflectionException $e) {
179
                continue;
180
            }
181
182
            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)) {
183
                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
184
                $bodyParametersFromDocBlock = $this->getBodyParametersFromDocBlock($formRequestDocBlock->getTags());
185
186
                if (count($bodyParametersFromDocBlock)) {
187
                    return $bodyParametersFromDocBlock;
188
                }
189
            }
190
        }
191
192
        return $this->getBodyParametersFromDocBlock($tags);
193
    }
194
195
    /**
196
     * @param array $tags
197
     *
198
     * @return array
199
     */
200
    protected function getBodyParametersFromDocBlock(array $tags)
201
    {
202
        $parameters = collect($tags)
203
            ->filter(function ($tag) {
204
                return $tag instanceof Tag && $tag->getName() === 'bodyParam';
205
            })
206
            ->mapWithKeys(function ($tag) {
207
                preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
208 View Code Duplication
                if (empty($content)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
209
                    // this means only name and type were supplied
210
                    list($name, $type) = preg_split('/\s+/', $tag->getContent());
211
                    $required = false;
212
                    $description = '';
213
                } else {
214
                    list($_, $name, $type, $required, $description) = $content;
0 ignored issues
show
Unused Code introduced by
The assignment to $_ is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
215
                    $description = trim($description);
216
                    if ($description == 'required' && empty(trim($required))) {
217
                        $required = $description;
218
                        $description = '';
219
                    }
220
                    $required = trim($required) == 'required' ? true : false;
221
                }
222
223
                $type = $this->normalizeParameterType($type);
224
                list($description, $example) = $this->parseDescription($description, $type);
225
                $value = is_null($example) ? $this->generateDummyValue($type) : $example;
226
227
                return [$name => compact('type', 'description', 'required', 'value')];
228
            })->toArray();
229
230
        return $parameters;
231
    }
232
233
    /**
234
     * @param ReflectionMethod $method
235
     * @param array $tags
236
     *
237
     * @return array
238
     */
239 View Code Duplication
    protected function getQueryParameters(ReflectionMethod $method, array $tags)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
240
    {
241
        foreach ($method->getParameters() as $param) {
242
            $paramType = $param->getType();
243
            if ($paramType === null) {
244
                continue;
245
            }
246
247
            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
248
                ? $paramType->__toString()
249
                : $paramType->getName();
250
251
            try {
252
                $parameterClass = new ReflectionClass($parameterClassName);
253
            } catch (\ReflectionException $e) {
254
                continue;
255
            }
256
257
            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)) {
258
                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
259
                $queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags());
260
261
                if (count($queryParametersFromDocBlock)) {
262
                    return $queryParametersFromDocBlock;
263
                }
264
            }
265
        }
266
267
        return $this->getQueryParametersFromDocBlock($tags);
268
    }
269
270
    /**
271
     * @param array $tags
272
     *
273
     * @return array
274
     */
275
    protected function getQueryParametersFromDocBlock(array $tags)
276
    {
277
        $parameters = collect($tags)
278
            ->filter(function ($tag) {
279
                return $tag instanceof Tag && $tag->getName() === 'queryParam';
280
            })
281
            ->mapWithKeys(function ($tag) {
282
                preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
283 View Code Duplication
                if (empty($content)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
284
                    // this means only name was supplied
285
                    list($name) = preg_split('/\s+/', $tag->getContent());
286
                    $required = false;
287
                    $description = '';
288
                } else {
289
                    list($_, $name, $required, $description) = $content;
0 ignored issues
show
Unused Code introduced by
The assignment to $_ is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
290
                    $description = trim($description);
291
                    if ($description == 'required' && empty(trim($required))) {
292
                        $required = $description;
293
                        $description = '';
294
                    }
295
                    $required = trim($required) == 'required' ? true : false;
296
                }
297
298
                list($description, $value) = $this->parseDescription($description, 'string');
299
                if (is_null($value)) {
300
                    $value = str_contains($description, ['number', 'count', 'page'])
301
                        ? $this->generateDummyValue('integer')
302
                        : $this->generateDummyValue('string');
303
                }
304
305
                return [$name => compact('description', 'required', 'value')];
306
            })->toArray();
307
308
        return $parameters;
309
    }
310
311
    /**
312
     * @param array $tags
313
     *
314
     * @return bool
315
     */
316
    protected function getAuthStatusFromDocBlock(array $tags)
317
    {
318
        $authTag = collect($tags)
319
            ->first(function ($tag) {
320
                return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated';
321
            });
322
323
        return (bool) $authTag;
324
    }
325
326
    /**
327
     * @param ReflectionMethod $method
328
     *
329
     * @return array
330
     */
331
    protected function parseDocBlock(ReflectionMethod $method)
332
    {
333
        $comment = $method->getDocComment();
334
        $phpdoc = new DocBlock($comment);
335
336
        return [
337
            'short' => $phpdoc->getShortDescription(),
338
            'long' => $phpdoc->getLongDescription()->getContents(),
339
            'tags' => $phpdoc->getTags(),
340
        ];
341
    }
342
343
    /**
344
     * @param ReflectionClass $controller
345
     * @param ReflectionMethod $method
346
     *
347
     * @return string
348
     */
349
    protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $method)
350
    {
351
        // @group tag on the method overrides that on the controller
352
        $docBlockComment = $method->getDocComment();
353 View Code Duplication
        if ($docBlockComment) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
354
            $phpdoc = new DocBlock($docBlockComment);
355
            foreach ($phpdoc->getTags() as $tag) {
356
                if ($tag->getName() === 'group') {
357
                    return $tag->getContent();
358
                }
359
            }
360
        }
361
362
        $docBlockComment = $controller->getDocComment();
363 View Code Duplication
        if ($docBlockComment) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
364
            $phpdoc = new DocBlock($docBlockComment);
365
            foreach ($phpdoc->getTags() as $tag) {
366
                if ($tag->getName() === 'group') {
367
                    return $tag->getContent();
368
                }
369
            }
370
        }
371
372
        return $this->config->get(('default_group'));
373
    }
374
375
    private function normalizeParameterType($type)
376
    {
377
        $typeMap = [
378
            'int' => 'integer',
379
            'bool' => 'boolean',
380
            'double' => 'float',
381
        ];
382
383
        return $type ? ($typeMap[$type] ?? $type) : 'string';
384
    }
385
386
    private function generateDummyValue(string $type)
387
    {
388
        $faker = Factory::create();
389
        if ($this->config->get('faker_seed')) {
390
            $faker->seed($this->config->get('faker_seed'));
391
        }
392
        $fakeFactories = [
393
            'integer' => function () use ($faker) {
394
                return $faker->numberBetween(1, 20);
395
            },
396
            'number' => function () use ($faker) {
397
                return $faker->randomFloat();
398
            },
399
            'float' => function () use ($faker) {
400
                return $faker->randomFloat();
401
            },
402
            'boolean' => function () use ($faker) {
403
                return $faker->boolean();
404
            },
405
            'string' => function () use ($faker) {
406
                return $faker->word;
407
            },
408
            'array' => function () {
409
                return [];
410
            },
411
            'object' => function () {
412
                return new \stdClass;
413
            },
414
        ];
415
416
        $fakeFactory = $fakeFactories[$type] ?? $fakeFactories['string'];
417
418
        return $fakeFactory();
419
    }
420
421
    /**
422
     * Allows users to specify an example for the parameter by writing 'Example: the-example',
423
     * to be used in example requests and response calls.
424
     *
425
     * @param string $description
426
     * @param string $type The type of the parameter. Used to cast the example provided, if any.
427
     *
428
     * @return array The description and included example.
429
     */
430
    private function parseDescription(string $description, string $type)
431
    {
432
        $example = null;
433
        if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) {
434
            $description = $content[1];
435
436
            // examples are parsed as strings by default, we need to cast them properly
437
            $example = $this->castToType($content[2], $type);
438
        }
439
440
        return [$description, $example];
441
    }
442
443
    /**
444
     * Cast a value from a string to a specified type.
445
     *
446
     * @param string $value
447
     * @param string $type
448
     *
449
     * @return mixed
450
     */
451
    private function castToType(string $value, string $type)
452
    {
453
        $casts = [
454
            'integer' => 'intval',
455
            'number' => 'floatval',
456
            'float' => 'floatval',
457
            'boolean' => 'boolval',
458
        ];
459
460
        // First, we handle booleans. We can't use a regular cast,
461
        //because PHP considers string 'false' as true.
462
        if ($value == 'false' && $type == 'boolean') {
463
            return false;
464
        }
465
466
        if (isset($casts[$type])) {
467
            return $casts[$type]($value);
468
        }
469
470
        return $value;
471
    }
472
}
473