Completed
Pull Request — master (#552)
by
unknown
01:58 queued 41s
created

Generator::getQueryParameters()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 30

Duplication

Lines 30
Ratio 100 %

Importance

Changes 0
Metric Value
dl 30
loc 30
rs 7.6666
c 0
b 0
f 0
cc 10
nc 10
nop 2

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
        $docBlock = $this->parseDocBlock($method);
61
        list($routeGroupName, $routeGroupDescription, $routeTitle) = $this->getRouteGroup($controller, $docBlock);
62
        $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']);
63
        $queryParameters = $this->getQueryParameters($method, $docBlock['tags']);
64
        $content = ResponseResolver::getResponse($route, $docBlock['tags'], [
65
            'rules' => $rulesToApply,
66
            'body' => $bodyParameters,
67
            'query' => $queryParameters,
68
        ]);
69
70
        $parsedRoute = [
71
            'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
72
            'groupName' => $routeGroupName,
73
            'groupDescription' => $routeGroupDescription,
74
            'title' => $routeTitle ?: $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
            'queryParameters' => $queryParameters,
80
            'bodyParameters' => $bodyParameters,
81
            'cleanBodyParameters' => $this->cleanParams($bodyParameters),
82
            'cleanQueryParameters' => $this->cleanParams($queryParameters),
83
            'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']),
84
            'response' => $content,
85
            'showresponse' => ! empty($content),
86
        ];
87
        $parsedRoute['headers'] = $rulesToApply['headers'] ?? [];
88
89
        return $parsedRoute;
90
    }
91
92 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...
93
    {
94
        foreach ($method->getParameters() as $param) {
95
            $paramType = $param->getType();
96
            if ($paramType === null) {
97
                continue;
98
            }
99
100
            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
101
                ? $paramType->__toString()
102
                : $paramType->getName();
103
104
            try {
105
                $parameterClass = new ReflectionClass($parameterClassName);
106
            } catch (\ReflectionException $e) {
107
                continue;
108
            }
109
110
            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)) {
111
                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
112
                $bodyParametersFromDocBlock = $this->getBodyParametersFromDocBlock($formRequestDocBlock->getTags());
113
114
                if (count($bodyParametersFromDocBlock)) {
115
                    return $bodyParametersFromDocBlock;
116
                }
117
            }
118
        }
119
120
        return $this->getBodyParametersFromDocBlock($tags);
121
    }
122
123
    /**
124
     * @param array $tags
125
     *
126
     * @return array
127
     */
128
    protected function getBodyParametersFromDocBlock(array $tags)
129
    {
130
        $parameters = collect($tags)
131
            ->filter(function ($tag) {
132
                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...
133
            })
134
            ->mapWithKeys(function ($tag) {
135
                preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
136
                $content = preg_replace('/\s?No-example.?/', '', $content);
137 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...
138
                    // this means only name and type were supplied
139
                    list($name, $type) = preg_split('/\s+/', $tag->getContent());
140
                    $required = false;
141
                    $description = '';
142
                } else {
143
                    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...
144
                    $description = trim($description);
145
                    if ($description == 'required' && empty(trim($required))) {
146
                        $required = $description;
147
                        $description = '';
148
                    }
149
                    $required = trim($required) == 'required' ? true : false;
150
                }
151
152
                $type = $this->normalizeParameterType($type);
153
                list($description, $example) = $this->parseDescription($description, $type);
154
                $value = is_null($example) && ! $this->shouldExcludeExample($tag) ? $this->generateDummyValue($type) : $example;
155
156
                return [$name => compact('type', 'description', 'required', 'value')];
157
            })->toArray();
158
159
        return $parameters;
160
    }
161
162
    /**
163
     * @param ReflectionMethod $method
164
     * @param array $tags
165
     *
166
     * @return array
167
     */
168 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...
169
    {
170
        foreach ($method->getParameters() as $param) {
171
            $paramType = $param->getType();
172
            if ($paramType === null) {
173
                continue;
174
            }
175
176
            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
177
                ? $paramType->__toString()
178
                : $paramType->getName();
179
180
            try {
181
                $parameterClass = new ReflectionClass($parameterClassName);
182
            } catch (\ReflectionException $e) {
183
                continue;
184
            }
185
186
            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)) {
187
                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
188
                $queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags());
189
190
                if (count($queryParametersFromDocBlock)) {
191
                    return $queryParametersFromDocBlock;
192
                }
193
            }
194
        }
195
196
        return $this->getQueryParametersFromDocBlock($tags);
197
    }
198
199
    /**
200
     * @param array $tags
201
     *
202
     * @return array
203
     */
204
    protected function getQueryParametersFromDocBlock(array $tags)
205
    {
206
        $parameters = collect($tags)
207
            ->filter(function ($tag) {
208
                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...
209
            })
210
            ->mapWithKeys(function ($tag) {
211
                preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
212
                $content = preg_replace('/\s?No-example.?/', '', $content);
213 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...
214
                    // this means only name was supplied
215
                    list($name) = preg_split('/\s+/', $tag->getContent());
216
                    $required = false;
217
                    $description = '';
218
                } else {
219
                    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...
220
                    $description = trim($description);
221
                    if ($description == 'required' && empty(trim($required))) {
222
                        $required = $description;
223
                        $description = '';
224
                    }
225
                    $required = trim($required) == 'required' ? true : false;
226
                }
227
228
                list($description, $value) = $this->parseDescription($description, 'string');
229
                if (is_null($value) && ! $this->shouldExcludeExample($tag)) {
230
                    $value = str_contains($description, ['number', 'count', 'page'])
231
                        ? $this->generateDummyValue('integer')
232
                        : $this->generateDummyValue('string');
233
                }
234
235
                return [$name => compact('description', 'required', 'value')];
236
            })->toArray();
237
238
        return $parameters;
239
    }
240
241
    /**
242
     * @param array $tags
243
     *
244
     * @return bool
245
     */
246
    protected function getAuthStatusFromDocBlock(array $tags)
247
    {
248
        $authTag = collect($tags)
249
            ->first(function ($tag) {
250
                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...
251
            });
252
253
        return (bool) $authTag;
254
    }
255
256
    /**
257
     * @param ReflectionMethod $method
258
     *
259
     * @return array
260
     */
261
    protected function parseDocBlock(ReflectionMethod $method)
262
    {
263
        $comment = $method->getDocComment();
264
        $phpdoc = new DocBlock($comment);
265
266
        return [
267
            'short' => $phpdoc->getShortDescription(),
268
            'long' => $phpdoc->getLongDescription()->getContents(),
269
            'tags' => $phpdoc->getTags(),
270
        ];
271
    }
272
273
    /**
274
     * @param ReflectionClass $controller
275
     * @param array $methodDocBlock
276
     *
277
     * @return array The route group name, the group description, ad the route title
278
     */
279
    protected function getRouteGroup(ReflectionClass $controller, array $methodDocBlock)
280
    {
281
        // @group tag on the method overrides that on the controller
282
        if (! empty($methodDocBlock['tags'])) {
283
            foreach ($methodDocBlock['tags'] as $tag) {
284
                if ($tag->getName() === 'group') {
285
                    $routeGroupParts = explode("\n", trim($tag->getContent()));
286
                    $routeGroupName = array_shift($routeGroupParts);
287
                    $routeGroupDescription = trim(implode("\n", $routeGroupParts));
288
289
                    // If the route has no title (aka "short"),
290
                    // we'll assume the routeGroupDescription is actually the title
291
                    // Something like this:
292
                    // /**
293
                    //   * Fetch cars. <-- This is route title.
294
                    //   * @group Cars <-- This is group name.
295
                    //   * APIs for cars. <-- This is group description (not required).
296
                    //   **/
297
                    // VS
298
                    // /**
299
                    //   * @group Cars <-- This is group name.
300
                    //   * Fetch cars. <-- This is route title, NOT group description.
301
                    //   **/
302
303
                    // BTW, this is a spaghetti way of doing this.
304
                    // It shall be refactored soon. Deus vult!💪
305
                    if (empty($methodDocBlock['short'])) {
306
                        return [$routeGroupName, '', $routeGroupDescription];
307
                    }
308
309
                    return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']];
310
                }
311
            }
312
        }
313
314
        $docBlockComment = $controller->getDocComment();
315
        if ($docBlockComment) {
316
            $phpdoc = new DocBlock($docBlockComment);
317
            foreach ($phpdoc->getTags() as $tag) {
318
                if ($tag->getName() === 'group') {
319
                    $routeGroupParts = explode("\n", trim($tag->getContent()));
320
                    $routeGroupName = array_shift($routeGroupParts);
321
                    $routeGroupDescription = implode("\n", $routeGroupParts);
322
323
                    return [$routeGroupName, $routeGroupDescription, $methodDocBlock['short']];
324
                }
325
            }
326
        }
327
328
        return [$this->config->get(('default_group')), '', $methodDocBlock['short']];
329
    }
330
331
    private function normalizeParameterType($type)
332
    {
333
        $typeMap = [
334
            'int' => 'integer',
335
            'bool' => 'boolean',
336
            'double' => 'float',
337
        ];
338
339
        return $type ? ($typeMap[$type] ?? $type) : 'string';
340
    }
341
342
    private function generateDummyValue(string $type)
343
    {
344
        $faker = Factory::create();
345
        if ($this->config->get('faker_seed')) {
346
            $faker->seed($this->config->get('faker_seed'));
347
        }
348
        $fakeFactories = [
349
            'integer' => function () use ($faker) {
350
                return $faker->numberBetween(1, 20);
351
            },
352
            'number' => function () use ($faker) {
353
                return $faker->randomFloat();
354
            },
355
            'float' => function () use ($faker) {
356
                return $faker->randomFloat();
357
            },
358
            'boolean' => function () use ($faker) {
359
                return $faker->boolean();
360
            },
361
            'string' => function () use ($faker) {
362
                return $faker->word;
363
            },
364
            'array' => function () {
365
                return [];
366
            },
367
            'object' => function () {
368
                return new \stdClass;
369
            },
370
        ];
371
372
        $fakeFactory = $fakeFactories[$type] ?? $fakeFactories['string'];
373
374
        return $fakeFactory();
375
    }
376
377
    /**
378
     * Allows users to specify an example for the parameter by writing 'Example: the-example',
379
     * to be used in example requests and response calls.
380
     *
381
     * @param string $description
382
     * @param string $type The type of the parameter. Used to cast the example provided, if any.
383
     *
384
     * @return array The description and included example.
385
     */
386
    private function parseDescription(string $description, string $type)
387
    {
388
        $example = null;
389
        if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) {
390
            $description = $content[1];
391
392
            // examples are parsed as strings by default, we need to cast them properly
393
            $example = $this->castToType($content[2], $type);
394
        }
395
396
        return [$description, $example];
397
    }
398
399
    /**
400
     * Allows users to specify that we shouldn't generate an example for the parameter
401
     * by writing 'No-example'.
402
     *
403
     * @param Tag $tag
404
     *
405
     * @return bool Whether no example should be generated
406
     */
407
    private function shouldExcludeExample(Tag $tag)
408
    {
409
        return strpos($tag->getContent(), ' No-example') !== false;
410
    }
411
412
    /**
413
     * Cast a value from a string to a specified type.
414
     *
415
     * @param string $value
416
     * @param string $type
417
     *
418
     * @return mixed
419
     */
420
    private function castToType(string $value, string $type)
421
    {
422
        $casts = [
423
            'integer' => 'intval',
424
            'number' => 'floatval',
425
            'float' => 'floatval',
426
            'boolean' => 'boolval',
427
        ];
428
429
        // First, we handle booleans. We can't use a regular cast,
430
        //because PHP considers string 'false' as true.
431
        if ($value == 'false' && $type == 'boolean') {
432
            return false;
433
        }
434
435
        if (isset($casts[$type])) {
436
            return $casts[$type]($value);
437
        }
438
439
        return $value;
440
    }
441
}
442