Completed
Pull Request — master (#552)
by
unknown
02:18 queued 36s
created

Generator::generateDummyValue()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

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