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

Generator::getAuthStatusFromDocBlock()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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