Completed
Pull Request — master (#552)
by
unknown
01:30
created

Generator::getQueryParametersFromDocBlock()   B

Complexity

Conditions 9
Paths 1

Size

Total Lines 36

Duplication

Lines 14
Ratio 38.89 %

Importance

Changes 0
Metric Value
dl 14
loc 36
rs 8.0555
c 0
b 0
f 0
cc 9
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 $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)
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...
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';
0 ignored issues
show
Bug introduced by
The class Mpociot\Reflection\DocBlock\Tag does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
134
            })
135
            ->mapWithKeys(function ($tag) {
136
                preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
137
                $content = preg_replace('/\s?No-example.?/', '', $content);
138 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...
139
                    // this means only name and type were supplied
140
                    list($name, $type) = preg_split('/\s+/', $tag->getContent());
141
                    $required = false;
142
                    $description = '';
143
                } else {
144
                    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...
145
                    $description = trim($description);
146
                    if ($description == 'required' && empty(trim($required))) {
147
                        $required = $description;
148
                        $description = '';
149
                    }
150
                    $required = trim($required) == 'required' ? true : false;
151
                }
152
153
                $type = $this->normalizeParameterType($type);
154
                list($description, $example) = $this->parseDescription($description, $type);
155
                $value = is_null($example)  && !$this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example;
156
157
                return [$name => compact('type', 'description', 'required', 'value')];
158
            })->toArray();
159
160
        return $parameters;
161
    }
162
163
    /**
164
     * @param ReflectionMethod $method
165
     * @param array $tags
166
     *
167
     * @return array
168
     */
169 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...
170
    {
171
        foreach ($method->getParameters() as $param) {
172
            $paramType = $param->getType();
173
            if ($paramType === null) {
174
                continue;
175
            }
176
177
            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
178
                ? $paramType->__toString()
179
                : $paramType->getName();
180
181
            try {
182
                $parameterClass = new ReflectionClass($parameterClassName);
183
            } catch (\ReflectionException $e) {
184
                continue;
185
            }
186
187
            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)) {
188
                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
189
                $queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags());
190
191
                if (count($queryParametersFromDocBlock)) {
192
                    return $queryParametersFromDocBlock;
193
                }
194
            }
195
        }
196
197
        return $this->getQueryParametersFromDocBlock($tags);
198
    }
199
200
    /**
201
     * @param array $tags
202
     *
203
     * @return array
204
     */
205
    protected function getQueryParametersFromDocBlock(array $tags)
206
    {
207
        $parameters = collect($tags)
208
            ->filter(function ($tag) {
209
                return $tag instanceof Tag && $tag->getName() === 'queryParam';
0 ignored issues
show
Bug introduced by
The class Mpociot\Reflection\DocBlock\Tag does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
210
            })
211
            ->mapWithKeys(function ($tag) {
212
                preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
213
                $content = preg_replace('/\s?No-example.?/', '', $content);
214 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...
215
                    // this means only name was supplied
216
                    list($name) = preg_split('/\s+/', $tag->getContent());
217
                    $required = false;
218
                    $description = '';
219
                } else {
220
                    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...
221
                    $description = trim($description);
222
                    if ($description == 'required' && empty(trim($required))) {
223
                        $required = $description;
224
                        $description = '';
225
                    }
226
                    $required = trim($required) == 'required' ? true : false;
227
                }
228
229
                list($description, $value) = $this->parseDescription($description, 'string');
230
                if (is_null($value) && !$this->shouldExcludeExample($tag)) {
231
                    $value = str_contains($description, ['number', 'count', 'page'])
232
                        ? $this->generateDummyValue('integer')
233
                        : $this->generateDummyValue('string');
234
                }
235
236
                return [$name => compact('description', 'required', 'value')];
237
            })->toArray();
238
239
        return $parameters;
240
    }
241
242
    /**
243
     * @param array $tags
244
     *
245
     * @return bool
246
     */
247
    protected function getAuthStatusFromDocBlock(array $tags)
248
    {
249
        $authTag = collect($tags)
250
            ->first(function ($tag) {
251
                return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated';
0 ignored issues
show
Bug introduced by
The class Mpociot\Reflection\DocBlock\Tag does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
252
            });
253
254
        return (bool) $authTag;
255
    }
256
257
    /**
258
     * @param ReflectionMethod $method
259
     *
260
     * @return array
261
     */
262
    protected function parseDocBlock(ReflectionMethod $method)
263
    {
264
        $comment = $method->getDocComment();
265
        $phpdoc = new DocBlock($comment);
266
267
        return [
268
            'short' => $phpdoc->getShortDescription(),
269
            'long' => $phpdoc->getLongDescription()->getContents(),
270
            'tags' => $phpdoc->getTags(),
271
        ];
272
    }
273
274
    /**
275
     * @param ReflectionClass $controller
276
     * @param ReflectionMethod $method
277
     *
278
     * @return array The route group name and description
279
     */
280
    protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $method)
281
    {
282
        // @group tag on the method overrides that on the controller
283
        $docBlockComment = $method->getDocComment();
284 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...
285
            $phpdoc = new DocBlock($docBlockComment);
286
            foreach ($phpdoc->getTags() as $tag) {
287
                if ($tag->getName() === 'group') {
288
                    $routeGroup = trim($tag->getContent());
0 ignored issues
show
Unused Code introduced by
$routeGroup 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...
289
                    $routeGroupParts = explode("\n", $tag->getContent());
290
                    $routeGroupName = array_shift($routeGroupParts);
291
                    $routeGroupDescription = implode("\n", $routeGroupParts);
292
293
                    return [$routeGroupName, $routeGroupDescription];
294
                }
295
            }
296
        }
297
298
        $docBlockComment = $controller->getDocComment();
299 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...
300
            $phpdoc = new DocBlock($docBlockComment);
301
            foreach ($phpdoc->getTags() as $tag) {
302
                if ($tag->getName() === 'group') {
303
                    $routeGroupParts = explode("\n", $tag->getContent());
304
                    $routeGroupName = array_shift($routeGroupParts);
305
                    $routeGroupDescription = implode("\n", $routeGroupParts);
306
307
                    return [$routeGroupName, $routeGroupDescription];
308
                }
309
            }
310
        }
311
312
        return [$this->config->get(('default_group')), ''];
313
    }
314
315
    private function normalizeParameterType($type)
316
    {
317
        $typeMap = [
318
            'int' => 'integer',
319
            'bool' => 'boolean',
320
            'double' => 'float',
321
        ];
322
323
        return $type ? ($typeMap[$type] ?? $type) : 'string';
324
    }
325
326
    private function generateDummyValue(string $type)
327
    {
328
        $faker = Factory::create();
329
        if ($this->config->get('faker_seed')) {
330
            $faker->seed($this->config->get('faker_seed'));
331
        }
332
        $fakeFactories = [
333
            'integer' => function () use ($faker) {
334
                return $faker->numberBetween(1, 20);
335
            },
336
            'number' => function () use ($faker) {
337
                return $faker->randomFloat();
338
            },
339
            'float' => function () use ($faker) {
340
                return $faker->randomFloat();
341
            },
342
            'boolean' => function () use ($faker) {
343
                return $faker->boolean();
344
            },
345
            'string' => function () use ($faker) {
346
                return $faker->word;
347
            },
348
            'array' => function () {
349
                return [];
350
            },
351
            'object' => function () {
352
                return new \stdClass;
353
            },
354
        ];
355
356
        $fakeFactory = $fakeFactories[$type] ?? $fakeFactories['string'];
357
358
        return $fakeFactory();
359
    }
360
361
    /**
362
     * Allows users to specify an example for the parameter by writing 'Example: the-example',
363
     * to be used in example requests and response calls.
364
     *
365
     * @param string $description
366
     * @param string $type The type of the parameter. Used to cast the example provided, if any.
367
     *
368
     * @return array The description and included example.
369
     */
370
    private function parseDescription(string $description, string $type)
371
    {
372
        $example = null;
373
        if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) {
374
            $description = $content[1];
375
376
            // examples are parsed as strings by default, we need to cast them properly
377
            $example = $this->castToType($content[2], $type);
378
        }
379
380
        return [$description, $example];
381
    }
382
383
    /**
384
     * Allows users to specify that we shouldn't generate an example for the parameter
385
     * by writing 'No-example'.
386
     *
387
     * @param Tag $tag
388
     *
389
     * @return bool Whether no example should be generated
390
     */
391
    private function shouldExcludeExample(Tag $tag)
392
    {
393
        return strpos($tag->getContent(), ' No-example') !== false;
394
    }
395
396
    /**
397
     * Cast a value from a string to a specified type.
398
     *
399
     * @param string $value
400
     * @param string $type
401
     *
402
     * @return mixed
403
     */
404
    private function castToType(string $value, string $type)
405
    {
406
        $casts = [
407
            'integer' => 'intval',
408
            'number' => 'floatval',
409
            'float' => 'floatval',
410
            'boolean' => 'boolval',
411
        ];
412
413
        // First, we handle booleans. We can't use a regular cast,
414
        //because PHP considers string 'false' as true.
415
        if ($value == 'false' && $type == 'boolean') {
416
            return false;
417
        }
418
419
        if (isset($casts[$type])) {
420
            return $casts[$type]($value);
421
        }
422
423
        return $value;
424
    }
425
}
426