Completed
Push — master ( fc16f2...341713 )
by
unknown
15s queued 11s
created

Generator::parseDocBlock()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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