Completed
Pull Request — master (#460)
by
unknown
01:39
created

Generator::getQueryParametersFromDocBlock()   B

Complexity

Conditions 8
Paths 1

Size

Total Lines 35

Duplication

Lines 14
Ratio 40 %

Importance

Changes 0
Metric Value
dl 14
loc 35
rs 8.1155
c 0
b 0
f 0
cc 8
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
     * @param Route $route
19
     *
20
     * @return mixed
21
     */
22
    public function getUri(Route $route)
23
    {
24
        return $route->uri();
25
    }
26
27
    /**
28
     * @param Route $route
29
     *
30
     * @return mixed
31
     */
32
    public function getMethods(Route $route)
33
    {
34
        return array_diff($route->methods(), ['HEAD']);
35
    }
36
37
    /**
38
     * @param  \Illuminate\Routing\Route $route
39
     * @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...
40
     *
41
     * @return array
42
     */
43
    public function processRoute(Route $route, array $rulesToApply = [])
44
    {
45
        $routeAction = $route->getAction();
46
        list($class, $method) = explode('@', $routeAction['uses']);
47
        $controller = new ReflectionClass($class);
48
        $method = $controller->getMethod($method);
49
50
        $routeGroup = $this->getRouteGroup($controller, $method);
51
        $docBlock = $this->parseDocBlock($method);
52
        $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']);
53
        $queryParameters = $this->getQueryParametersFromDocBlock($docBlock['tags']);
54
        $content = ResponseResolver::getResponse($route, $docBlock['tags'], [
55
            'rules' => $rulesToApply,
56
            'body' => $bodyParameters,
57
            'query' => $queryParameters,
58
        ]);
59
60
        $parsedRoute = [
61
            'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
62
            'group' => $routeGroup,
63
            'title' => $docBlock['short'],
64
            'description' => $docBlock['long'],
65
            'methods' => $this->getMethods($route),
66
            'uri' => $this->getUri($route),
67
            'bodyParameters' => $bodyParameters,
68
            'cleanBodyParameters' => $this->cleanParams($bodyParameters),
69
            'queryParameters' => $queryParameters,
70
            'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']),
71
            'response' => $content,
72
            'showresponse' => ! empty($content),
73
        ];
74
        $parsedRoute['headers'] = $rulesToApply['headers'] ?? [];
75
76
        return $parsedRoute;
77
    }
78
79
    protected function getBodyParameters(ReflectionMethod $method, array $tags)
80
    {
81
        /** @var ReflectionClass $cls */
82
        $cls = collect($method->getParameters())
83
            ->reduce(function ($carry, $param) use ($method) {
84
                if (!$param->getType()) {
85
                    return $carry;
86
                }
87
88
                $cls = new ReflectionClass($param->getType()->getName());
89
90
                if ($cls->isSubclassOf(\Illuminate\Foundation\Http\FormRequest::class)) {
91
                    return $cls;
92
                }
93
94
                return $carry;
95
            }, null);
96
97
        if ($cls) {
98
            $docBlock = new DocBlock($cls->getDocComment());
99
            $result = $this->getBodyParametersFromDocBlock($docBlock->getTags());
100
101
            if (count($result)) {
102
                return $result;
103
            }
104
        }
105
106
        return $this->getBodyParametersFromDocBlock($tags);
107
    }
108
109
    /**
110
     * @param array $tags
111
     *
112
     * @return array
113
     */
114
    protected function getBodyParametersFromDocBlock(array $tags)
115
    {
116
        $parameters = collect($tags)
117
            ->filter(function ($tag) {
118
                return $tag instanceof Tag && $tag->getName() === 'bodyParam';
119
            })
120
            ->mapWithKeys(function ($tag) {
121
                preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
122 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...
123
                    // this means only name and type were supplied
124
                    list($name, $type) = preg_split('/\s+/', $tag->getContent());
125
                    $required = false;
126
                    $description = '';
127
                } else {
128
                    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...
129
                    $description = trim($description);
130
                    if ($description == 'required' && empty(trim($required))) {
131
                        $required = $description;
132
                        $description = '';
133
                    }
134
                    $required = trim($required) == 'required' ? true : false;
135
                }
136
137
                $type = $this->normalizeParameterType($type);
138
                list($description, $example) = $this->parseDescription($description, $type);
139
                $value = is_null($example) ? $this->generateDummyValue($type) : $example;
140
141
                return [$name => compact('type', 'description', 'required', 'value')];
142
            })->toArray();
143
144
        return $parameters;
145
    }
146
147
    /**
148
     * @param array $tags
149
     *
150
     * @return array
151
     */
152
    protected function getQueryParametersFromDocBlock(array $tags)
153
    {
154
        $parameters = collect($tags)
155
            ->filter(function ($tag) {
156
                return $tag instanceof Tag && $tag->getName() === 'queryParam';
157
            })
158
            ->mapWithKeys(function ($tag) {
159
                preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
160 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...
161
                    // this means only name was supplied
162
                    list($name) = preg_split('/\s+/', $tag->getContent());
163
                    $required = false;
164
                    $description = '';
165
                } else {
166
                    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...
167
                    $description = trim($description);
168
                    if ($description == 'required' && empty(trim($required))) {
169
                        $required = $description;
170
                        $description = '';
171
                    }
172
                    $required = trim($required) == 'required' ? true : false;
173
                }
174
175
                list($description, $value) = $this->parseDescription($description, 'string');
176
                if (is_null($value)) {
177
                    $value = str_contains($description, ['number', 'count', 'page'])
178
                        ? $this->generateDummyValue('integer')
179
                        : $this->generateDummyValue('string');
180
                }
181
182
                return [$name => compact('description', 'required', 'value')];
183
            })->toArray();
184
185
        return $parameters;
186
    }
187
188
    /**
189
     * @param array $tags
190
     *
191
     * @return bool
192
     */
193
    protected function getAuthStatusFromDocBlock(array $tags)
194
    {
195
        $authTag = collect($tags)
196
            ->first(function ($tag) {
197
                return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated';
198
            });
199
200
        return (bool) $authTag;
201
    }
202
203
    /**
204
     * @param ReflectionMethod $method
205
     *
206
     * @return array
207
     */
208
    protected function parseDocBlock(ReflectionMethod $method)
209
    {
210
        $comment = $method->getDocComment();
211
        $phpdoc = new DocBlock($comment);
212
213
        return [
214
            'short' => $phpdoc->getShortDescription(),
215
            'long' => $phpdoc->getLongDescription()->getContents(),
216
            'tags' => $phpdoc->getTags(),
217
        ];
218
    }
219
220
    /**
221
     * @param ReflectionClass $controller
222
     * @param ReflectionMethod $method
223
     *
224
     * @return string
225
     */
226
    protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $method)
227
    {
228
        // @group tag on the method overrides that on the controller
229
        $docBlockComment = $method->getDocComment();
230 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...
231
            $phpdoc = new DocBlock($docBlockComment);
232
            foreach ($phpdoc->getTags() as $tag) {
233
                if ($tag->getName() === 'group') {
234
                    return $tag->getContent();
235
                }
236
            }
237
        }
238
239
        $docBlockComment = $controller->getDocComment();
240 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...
241
            $phpdoc = new DocBlock($docBlockComment);
242
            foreach ($phpdoc->getTags() as $tag) {
243
                if ($tag->getName() === 'group') {
244
                    return $tag->getContent();
245
                }
246
            }
247
        }
248
249
        return 'general';
250
    }
251
252
    private function normalizeParameterType($type)
253
    {
254
        $typeMap = [
255
            'int' => 'integer',
256
            'bool' => 'boolean',
257
            'double' => 'float',
258
        ];
259
260
        return $type ? ($typeMap[$type] ?? $type) : 'string';
261
    }
262
263
    private function generateDummyValue(string $type)
264
    {
265
        $faker = Factory::create();
266
        $fakes = [
267
            'integer' => function () {
268
                return rand(1, 20);
269
            },
270
            'number' => function () use ($faker) {
271
                return $faker->randomFloat();
272
            },
273
            'float' => function () use ($faker) {
274
                return $faker->randomFloat();
275
            },
276
            'boolean' => function () use ($faker) {
277
                return $faker->boolean();
278
            },
279
            'string' => function () use ($faker) {
280
                return str_random();
281
            },
282
            'array' => function () {
283
                return [];
284
            },
285
            'object' => function () {
286
                return new \stdClass;
287
            },
288
        ];
289
290
        $fake = $fakes[$type] ?? $fakes['string'];
291
292
        return $fake();
293
    }
294
295
    /**
296
     * Allows users to specify an example for the parameter by writing 'Example: the-example',
297
     * to be used in example requests and response calls.
298
     *
299
     * @param string $description
300
     * @param string $type The type of the parameter. Used to cast the example provided, if any.
301
     *
302
     * @return array The description and included example.
303
     */
304
    private function parseDescription(string $description, string $type)
305
    {
306
        $example = null;
307
        if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) {
308
            $description = $content[1];
309
310
            // examples are parsed as strings by default, we need to cast them properly
311
            $example = $this->castToType($content[2], $type);
312
        }
313
314
        return [$description, $example];
315
    }
316
317
    /**
318
     * Cast a value from a string to a specified type.
319
     *
320
     * @param string $value
321
     * @param string $type
322
     *
323
     * @return mixed
324
     */
325
    private function castToType(string $value, string $type)
326
    {
327
        $casts = [
328
            'integer' => 'intval',
329
            'number' => 'floatval',
330
            'float' => 'floatval',
331
            'boolean' => 'boolval',
332
        ];
333
334
        // First, we handle booleans. We can't use a regular cast,
335
        //because PHP considers string 'false' as true.
336
        if ($value == 'false' && $type == 'boolean') {
337
            return false;
338
        }
339
340
        if (isset($casts[$type])) {
341
            return $casts[$type]($value);
342
        }
343
344
        return $value;
345
    }
346
}
347