Completed
Pull Request — master (#516)
by
unknown
01:36
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 string The seed to be used with Faker.
19
     * Useful when you want to always have the same fake output.
20
     */
21
    private $fakerSeed = null;
22
23
    public function __construct(string $fakerSeed = null)
24
    {
25
        $this->fakerSeed = $fakerSeed;
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
        $action = $route->getAction()['uses'];
57
        list($class, $method) = is_array($action) ? $action : explode('@', $action);
58
        $controller = new ReflectionClass($class);
59
        $method = $controller->getMethod($method);
60
61
        $routeGroup = $this->getRouteGroup($controller, $method);
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
            '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
            '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';
133
            })
134
            ->mapWithKeys(function ($tag) {
135
                preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
136 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...
137
                    // this means only name and type were supplied
138
                    list($name, $type) = preg_split('/\s+/', $tag->getContent());
139
                    $required = false;
140
                    $description = '';
141
                } else {
142
                    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...
143
                    $description = trim($description);
144
                    if ($description == 'required' && empty(trim($required))) {
145
                        $required = $description;
146
                        $description = '';
147
                    }
148
                    $required = trim($required) == 'required' ? true : false;
149
                }
150
151
                $type = $this->normalizeParameterType($type);
152
                list($description, $example) = $this->parseDescription($description, $type);
153
                $value = is_null($example) ? $this->generateDummyValue($type) : $example;
154
155
                return [$name => compact('type', 'description', 'required', 'value')];
156
            })->toArray();
157
158
        return $parameters;
159
    }
160
161
    /**
162
     * @param ReflectionMethod $method
163
     * @param array $tags
164
     *
165
     * @return array
166
     */
167 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...
168
    {
169
        foreach ($method->getParameters() as $param) {
170
            $paramType = $param->getType();
171
            if ($paramType === null) {
172
                continue;
173
            }
174
175
            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
176
                ? $paramType->__toString()
177
                : $paramType->getName();
178
179
            try {
180
                $parameterClass = new ReflectionClass($parameterClassName);
181
            } catch (\ReflectionException $e) {
182
                continue;
183
            }
184
185
            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)) {
186
                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
187
                $queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags());
188
189
                if (count($queryParametersFromDocBlock)) {
190
                    return $queryParametersFromDocBlock;
191
                }
192
            }
193
        }
194
195
        return $this->getQueryParametersFromDocBlock($tags);
196
    }
197
198
    /**
199
     * @param array $tags
200
     *
201
     * @return array
202
     */
203
    protected function getQueryParametersFromDocBlock(array $tags)
204
    {
205
        $parameters = collect($tags)
206
            ->filter(function ($tag) {
207
                return $tag instanceof Tag && $tag->getName() === 'queryParam';
208
            })
209
            ->mapWithKeys(function ($tag) {
210
                preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
211 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...
212
                    // this means only name was supplied
213
                    list($name) = preg_split('/\s+/', $tag->getContent());
214
                    $required = false;
215
                    $description = '';
216
                } else {
217
                    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...
218
                    $description = trim($description);
219
                    if ($description == 'required' && empty(trim($required))) {
220
                        $required = $description;
221
                        $description = '';
222
                    }
223
                    $required = trim($required) == 'required' ? true : false;
224
                }
225
226
                list($description, $value) = $this->parseDescription($description, 'string');
227
                if (is_null($value)) {
228
                    $value = str_contains($description, ['number', 'count', 'page'])
229
                        ? $this->generateDummyValue('integer')
230
                        : $this->generateDummyValue('string');
231
                }
232
233
                return [$name => compact('description', 'required', 'value')];
234
            })->toArray();
235
236
        return $parameters;
237
    }
238
239
    /**
240
     * @param array $tags
241
     *
242
     * @return bool
243
     */
244
    protected function getAuthStatusFromDocBlock(array $tags)
245
    {
246
        $authTag = collect($tags)
247
            ->first(function ($tag) {
248
                return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated';
249
            });
250
251
        return (bool) $authTag;
252
    }
253
254
    /**
255
     * @param ReflectionMethod $method
256
     *
257
     * @return array
258
     */
259
    protected function parseDocBlock(ReflectionMethod $method)
260
    {
261
        $comment = $method->getDocComment();
262
        $phpdoc = new DocBlock($comment);
263
264
        return [
265
            'short' => $phpdoc->getShortDescription(),
266
            'long' => $phpdoc->getLongDescription()->getContents(),
267
            'tags' => $phpdoc->getTags(),
268
        ];
269
    }
270
271
    /**
272
     * @param ReflectionClass $controller
273
     * @param ReflectionMethod $method
274
     *
275
     * @return string
276
     */
277
    protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $method)
278
    {
279
        // @group tag on the method overrides that on the controller
280
        $docBlockComment = $method->getDocComment();
281 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...
282
            $phpdoc = new DocBlock($docBlockComment);
283
            foreach ($phpdoc->getTags() as $tag) {
284
                if ($tag->getName() === 'group') {
285
                    return $tag->getContent();
286
                }
287
            }
288
        }
289
290
        $docBlockComment = $controller->getDocComment();
291 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...
292
            $phpdoc = new DocBlock($docBlockComment);
293
            foreach ($phpdoc->getTags() as $tag) {
294
                if ($tag->getName() === 'group') {
295
                    return $tag->getContent();
296
                }
297
            }
298
        }
299
300
        return config('apidoc.default_group', 'general');
301
    }
302
303
    private function normalizeParameterType($type)
304
    {
305
        $typeMap = [
306
            'int' => 'integer',
307
            'bool' => 'boolean',
308
            'double' => 'float',
309
        ];
310
311
        return $type ? ($typeMap[$type] ?? $type) : 'string';
312
    }
313
314
    private function generateDummyValue(string $type)
315
    {
316
        $faker = Factory::create();
317
        if ($this->fakerSeed) {
318
            $faker->seed($this->fakerSeed);
319
        }
320
        $fakeFactories = [
321
            'integer' => function () use ($faker) {
322
                return $faker->numberBetween(1, 20);
323
            },
324
            'number' => function () use ($faker) {
325
                return $faker->randomFloat();
326
            },
327
            'float' => function () use ($faker) {
328
                return $faker->randomFloat();
329
            },
330
            'boolean' => function () use ($faker) {
331
                return $faker->boolean();
332
            },
333
            'string' => function () use ($faker) {
334
                return $faker->word;
335
            },
336
            'array' => function () {
337
                return [];
338
            },
339
            'object' => function () {
340
                return new \stdClass;
341
            },
342
        ];
343
344
        $fakeFactory = $fakeFactories[$type] ?? $fakeFactories['string'];
345
346
        return $fakeFactory();
347
    }
348
349
    /**
350
     * Allows users to specify an example for the parameter by writing 'Example: the-example',
351
     * to be used in example requests and response calls.
352
     *
353
     * @param string $description
354
     * @param string $type The type of the parameter. Used to cast the example provided, if any.
355
     *
356
     * @return array The description and included example.
357
     */
358
    private function parseDescription(string $description, string $type)
359
    {
360
        $example = null;
361
        if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) {
362
            $description = $content[1];
363
364
            // examples are parsed as strings by default, we need to cast them properly
365
            $example = $this->castToType($content[2], $type);
366
        }
367
368
        return [$description, $example];
369
    }
370
371
    /**
372
     * Cast a value from a string to a specified type.
373
     *
374
     * @param string $value
375
     * @param string $type
376
     *
377
     * @return mixed
378
     */
379
    private function castToType(string $value, string $type)
380
    {
381
        $casts = [
382
            'integer' => 'intval',
383
            'number' => 'floatval',
384
            'float' => 'floatval',
385
            'boolean' => 'boolval',
386
        ];
387
388
        // First, we handle booleans. We can't use a regular cast,
389
        //because PHP considers string 'false' as true.
390
        if ($value == 'false' && $type == 'boolean') {
391
            return false;
392
        }
393
394
        if (isset($casts[$type])) {
395
            return $casts[$type]($value);
396
        }
397
398
        return $value;
399
    }
400
}
401