Completed
Push — master ( f297bf...f3b50d )
by
unknown
18s queued 15s
created

Generator::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
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 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
        $routeAction = $route->getAction();
57
        list($class, $method) = explode('@', $routeAction['uses']);
58
        $controller = new ReflectionClass($class);
59
        $method = $controller->getMethod($method);
60
61
        $routeGroup = $this->getRouteGroup($controller, $method);
62
        $docBlock = $this->parseDocBlock($method);
63
        $bodyParameters = $this->getBodyParameters($method, $docBlock['tags']);
64
        $queryParameters = $this->getQueryParametersFromDocBlock($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
            'group' => $routeGroup,
74
            'title' => $docBlock['short'],
75
            'description' => $docBlock['long'],
76
            'methods' => $this->getMethods($route),
77
            'uri' => $this->getUri($route),
78
            'boundUri' => Utils::getFullUrl($route, $rulesToApply['bindings'] ?? []),
79
            'bodyParameters' => $bodyParameters,
80
            'cleanBodyParameters' => $this->cleanParams($bodyParameters),
81
            'queryParameters' => $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
    protected function getBodyParameters(ReflectionMethod $method, array $tags)
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)) {
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 array $tags
162
     *
163
     * @return array
164
     */
165
    protected function getQueryParametersFromDocBlock(array $tags)
166
    {
167
        $parameters = collect($tags)
168
            ->filter(function ($tag) {
169
                return $tag instanceof Tag && $tag->getName() === 'queryParam';
170
            })
171
            ->mapWithKeys(function ($tag) {
172
                preg_match('/(.+?)\s+(required\s+)?(.*)/', $tag->getContent(), $content);
173 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...
174
                    // this means only name was supplied
175
                    list($name) = preg_split('/\s+/', $tag->getContent());
176
                    $required = false;
177
                    $description = '';
178
                } else {
179
                    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...
180
                    $description = trim($description);
181
                    if ($description == 'required' && empty(trim($required))) {
182
                        $required = $description;
183
                        $description = '';
184
                    }
185
                    $required = trim($required) == 'required' ? true : false;
186
                }
187
188
                list($description, $value) = $this->parseDescription($description, 'string');
189
                if (is_null($value)) {
190
                    $value = str_contains($description, ['number', 'count', 'page'])
191
                        ? $this->generateDummyValue('integer')
192
                        : $this->generateDummyValue('string');
193
                }
194
195
                return [$name => compact('description', 'required', 'value')];
196
            })->toArray();
197
198
        return $parameters;
199
    }
200
201
    /**
202
     * @param array $tags
203
     *
204
     * @return bool
205
     */
206
    protected function getAuthStatusFromDocBlock(array $tags)
207
    {
208
        $authTag = collect($tags)
209
            ->first(function ($tag) {
210
                return $tag instanceof Tag && strtolower($tag->getName()) === 'authenticated';
211
            });
212
213
        return (bool) $authTag;
214
    }
215
216
    /**
217
     * @param ReflectionMethod $method
218
     *
219
     * @return array
220
     */
221
    protected function parseDocBlock(ReflectionMethod $method)
222
    {
223
        $comment = $method->getDocComment();
224
        $phpdoc = new DocBlock($comment);
225
226
        return [
227
            'short' => $phpdoc->getShortDescription(),
228
            'long' => $phpdoc->getLongDescription()->getContents(),
229
            'tags' => $phpdoc->getTags(),
230
        ];
231
    }
232
233
    /**
234
     * @param ReflectionClass $controller
235
     * @param ReflectionMethod $method
236
     *
237
     * @return string
238
     */
239
    protected function getRouteGroup(ReflectionClass $controller, ReflectionMethod $method)
240
    {
241
        // @group tag on the method overrides that on the controller
242
        $docBlockComment = $method->getDocComment();
243 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...
244
            $phpdoc = new DocBlock($docBlockComment);
245
            foreach ($phpdoc->getTags() as $tag) {
246
                if ($tag->getName() === 'group') {
247
                    return $tag->getContent();
248
                }
249
            }
250
        }
251
252
        $docBlockComment = $controller->getDocComment();
253 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...
254
            $phpdoc = new DocBlock($docBlockComment);
255
            foreach ($phpdoc->getTags() as $tag) {
256
                if ($tag->getName() === 'group') {
257
                    return $tag->getContent();
258
                }
259
            }
260
        }
261
262
        return config('apidoc.ungrouped_name') ?: 'general';
263
    }
264
265
    private function normalizeParameterType($type)
266
    {
267
        $typeMap = [
268
            'int' => 'integer',
269
            'bool' => 'boolean',
270
            'double' => 'float',
271
        ];
272
273
        return $type ? ($typeMap[$type] ?? $type) : 'string';
274
    }
275
276
    private function generateDummyValue(string $type)
277
    {
278
        $faker = Factory::create();
279
        if ($this->fakerSeed) {
280
            $faker->seed($this->fakerSeed);
281
        }
282
        $fakes = [
283
            'integer' => function () use ($faker) {
284
                return $faker->numberBetween(1, 20);
285
            },
286
            'number' => function () use ($faker) {
287
                return $faker->randomFloat();
288
            },
289
            'float' => function () use ($faker) {
290
                return $faker->randomFloat();
291
            },
292
            'boolean' => function () use ($faker) {
293
                return $faker->boolean();
294
            },
295
            'string' => function () use ($faker) {
296
                return $faker->word;
297
            },
298
            'array' => function () {
299
                return [];
300
            },
301
            'object' => function () {
302
                return new \stdClass;
303
            },
304
        ];
305
306
        $fake = $fakes[$type] ?? $fakes['string'];
307
308
        return $fake();
309
    }
310
311
    /**
312
     * Allows users to specify an example for the parameter by writing 'Example: the-example',
313
     * to be used in example requests and response calls.
314
     *
315
     * @param string $description
316
     * @param string $type The type of the parameter. Used to cast the example provided, if any.
317
     *
318
     * @return array The description and included example.
319
     */
320
    private function parseDescription(string $description, string $type)
321
    {
322
        $example = null;
323
        if (preg_match('/(.*)\s+Example:\s*(.*)\s*/', $description, $content)) {
324
            $description = $content[1];
325
326
            // examples are parsed as strings by default, we need to cast them properly
327
            $example = $this->castToType($content[2], $type);
328
        }
329
330
        return [$description, $example];
331
    }
332
333
    /**
334
     * Cast a value from a string to a specified type.
335
     *
336
     * @param string $value
337
     * @param string $type
338
     *
339
     * @return mixed
340
     */
341
    private function castToType(string $value, string $type)
342
    {
343
        $casts = [
344
            'integer' => 'intval',
345
            'number' => 'floatval',
346
            'float' => 'floatval',
347
            'boolean' => 'boolval',
348
        ];
349
350
        // First, we handle booleans. We can't use a regular cast,
351
        //because PHP considers string 'false' as true.
352
        if ($value == 'false' && $type == 'boolean') {
353
            return false;
354
        }
355
356
        if (isset($casts[$type])) {
357
            return $casts[$type]($value);
358
        }
359
360
        return $value;
361
    }
362
}
363