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

Generator::getRouteGroup()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 25

Duplication

Lines 16
Ratio 64 %

Importance

Changes 0
Metric Value
dl 16
loc 25
rs 8.5866
c 0
b 0
f 0
cc 7
nc 7
nop 2
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();
57
        if (is_array($action) && array_key_exists('uses', $action)) {
58
            $action = $action['uses'];
59
        }
60
        list($class, $method) = is_array($action) ? $action : explode('@', $action);
61
        $controller = new ReflectionClass($class);
62
        $method = $controller->getMethod($method);
63
64
        $routeGroup = $this->getRouteGroup($controller, $method);
65
        $docBlock = $this->parseDocBlock($method);
66
        $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']);
67
        $queryParameters = $this->getQueryParameters($method, $docBlock['tags']);
68
        $content = ResponseResolver::getResponse($route, $docBlock['tags'], [
69
            'rules' => $rulesToApply,
70
            'body' => $bodyParameters,
71
            'query' => $queryParameters,
72
        ]);
73
74
        $parsedRoute = [
75
            'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
76
            'group' => $routeGroup,
77
            'title' => $docBlock['short'],
78
            'description' => $docBlock['long'],
79
            'methods' => $this->getMethods($route),
80
            'uri' => $this->getUri($route),
81
            'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? ($rulesToApply['response_calls']['bindings'] ?? [])),
82
            'queryParameters' => $queryParameters,
83
            'bodyParameters' => $bodyParameters,
84
            'cleanBodyParameters' => $this->cleanParams($bodyParameters),
85
            'cleanQueryParameters' => $this->cleanParams($queryParameters),
86
            'authenticated' => $this->getAuthStatusFromDocBlock($docBlock['tags']),
87
            'response' => $content,
88
            'showresponse' => ! empty($content),
89
        ];
90
        $parsedRoute['headers'] = $rulesToApply['headers'] ?? [];
91
92
        return $parsedRoute;
93
    }
94
95 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...
96
    {
97
        foreach ($method->getParameters() as $param) {
98
            $paramType = $param->getType();
99
            if ($paramType === null) {
100
                continue;
101
            }
102
103
            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
104
                ? $paramType->__toString()
105
                : $paramType->getName();
106
107
            try {
108
                $parameterClass = new ReflectionClass($parameterClassName);
109
            } catch (\ReflectionException $e) {
110
                continue;
111
            }
112
113
            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)) {
114
                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
115
                $bodyParametersFromDocBlock = $this->getBodyParametersFromDocBlock($formRequestDocBlock->getTags());
116
117
                if (count($bodyParametersFromDocBlock)) {
118
                    return $bodyParametersFromDocBlock;
119
                }
120
            }
121
        }
122
123
        return $this->getBodyParametersFromDocBlock($tags);
124
    }
125
126
    /**
127
     * @param array $tags
128
     *
129
     * @return array
130
     */
131
    protected function getBodyParametersFromDocBlock(array $tags)
132
    {
133
        $parameters = collect($tags)
134
            ->filter(function ($tag) {
135
                return $tag instanceof Tag && $tag->getName() === 'bodyParam';
136
            })
137
            ->mapWithKeys(function ($tag) {
138
                preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
139 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...
140
                    // this means only name and type were supplied
141
                    list($name, $type) = preg_split('/\s+/', $tag->getContent());
142
                    $required = false;
143
                    $description = '';
144
                } else {
145
                    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...
146
                    $description = trim($description);
147
                    if ($description == 'required' && empty(trim($required))) {
148
                        $required = $description;
149
                        $description = '';
150
                    }
151
                    $required = trim($required) == 'required' ? true : false;
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', 'required', 'value')];
159
            })->toArray();
160
161
        return $parameters;
162
    }
163
164
    /**
165
     * @param ReflectionMethod $method
166
     * @param array $tags
167
     *
168
     * @return array
169
     */
170 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...
171
    {
172
        foreach ($method->getParameters() as $param) {
173
            $paramType = $param->getType();
174
            if ($paramType === null) {
175
                continue;
176
            }
177
178
            $parameterClassName = version_compare(phpversion(), '7.1.0', '<')
179
                ? $paramType->__toString()
180
                : $paramType->getName();
181
182
            try {
183
                $parameterClass = new ReflectionClass($parameterClassName);
184
            } catch (\ReflectionException $e) {
185
                continue;
186
            }
187
188
            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)) {
189
                $formRequestDocBlock = new DocBlock($parameterClass->getDocComment());
190
                $queryParametersFromDocBlock = $this->getQueryParametersFromDocBlock($formRequestDocBlock->getTags());
191
192
                if (count($queryParametersFromDocBlock)) {
193
                    return $queryParametersFromDocBlock;
194
                }
195
            }
196
        }
197
198
        return $this->getQueryParametersFromDocBlock($tags);
199
    }
200
201
    /**
202
     * @param array $tags
203
     *
204
     * @return array
205
     */
206
    protected function getQueryParametersFromDocBlock(array $tags)
207
    {
208
        $parameters = collect($tags)
209
            ->filter(function ($tag) {
210
                return $tag instanceof Tag && $tag->getName() === 'queryParam';
211
            })
212
            ->mapWithKeys(function ($tag) {
213
                preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $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)) {
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';
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 string
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
                    return $tag->getContent();
289
                }
290
            }
291
        }
292
293
        $docBlockComment = $controller->getDocComment();
294 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...
295
            $phpdoc = new DocBlock($docBlockComment);
296
            foreach ($phpdoc->getTags() as $tag) {
297
                if ($tag->getName() === 'group') {
298
                    return $tag->getContent();
299
                }
300
            }
301
        }
302
303
        return config('apidoc.default_group', 'general');
304
    }
305
306
    private function normalizeParameterType($type)
307
    {
308
        $typeMap = [
309
            'int' => 'integer',
310
            'bool' => 'boolean',
311
            'double' => 'float',
312
        ];
313
314
        return $type ? ($typeMap[$type] ?? $type) : 'string';
315
    }
316
317
    private function generateDummyValue(string $type)
318
    {
319
        $faker = Factory::create();
320
        if ($this->fakerSeed) {
321
            $faker->seed($this->fakerSeed);
322
        }
323
        $fakeFactories = [
324
            'integer' => function () use ($faker) {
325
                return $faker->numberBetween(1, 20);
326
            },
327
            'number' => function () use ($faker) {
328
                return $faker->randomFloat();
329
            },
330
            'float' => function () use ($faker) {
331
                return $faker->randomFloat();
332
            },
333
            'boolean' => function () use ($faker) {
334
                return $faker->boolean();
335
            },
336
            'string' => function () use ($faker) {
337
                return $faker->word;
338
            },
339
            'array' => function () {
340
                return [];
341
            },
342
            'object' => function () {
343
                return new \stdClass;
344
            },
345
        ];
346
347
        $fakeFactory = $fakeFactories[$type] ?? $fakeFactories['string'];
348
349
        return $fakeFactory();
350
    }
351
352
    /**
353
     * Allows users to specify an example for the parameter by writing 'Example: the-example',
354
     * to be used in example requests and response calls.
355
     *
356
     * @param string $description
357
     * @param string $type The type of the parameter. Used to cast the example provided, if any.
358
     *
359
     * @return array The description and included example.
360
     */
361
    private function parseDescription(string $description, string $type)
362
    {
363
        $example = null;
364
        if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) {
365
            $description = $content[1];
366
367
            // examples are parsed as strings by default, we need to cast them properly
368
            $example = $this->castToType($content[2], $type);
369
        }
370
371
        return [$description, $example];
372
    }
373
374
    /**
375
     * Cast a value from a string to a specified type.
376
     *
377
     * @param string $value
378
     * @param string $type
379
     *
380
     * @return mixed
381
     */
382
    private function castToType(string $value, string $type)
383
    {
384
        $casts = [
385
            'integer' => 'intval',
386
            'number' => 'floatval',
387
            'float' => 'floatval',
388
            'boolean' => 'boolval',
389
        ];
390
391
        // First, we handle booleans. We can't use a regular cast,
392
        //because PHP considers string 'false' as true.
393
        if ($value == 'false' && $type == 'boolean') {
394
            return false;
395
        }
396
397
        if (isset($casts[$type])) {
398
            return $casts[$type]($value);
399
        }
400
401
        return $value;
402
    }
403
}
404