Completed
Pull Request — master (#362)
by
unknown
02:57 queued 37s
created

AbstractGenerator::transformHeadersToServerVars()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.7
c 0
b 0
f 0
cc 4
nc 3
nop 1
1
<?php
2
3
namespace Mpociot\ApiDoc\Generators;
4
5
use ReflectionClass;
6
use Illuminate\Support\Str;
7
use League\Fractal\Manager;
8
use Illuminate\Routing\Route;
9
use Mpociot\Reflection\DocBlock;
10
use League\Fractal\Resource\Item;
11
use Mpociot\Reflection\DocBlock\Tag;
12
use League\Fractal\Resource\Collection;
13
14
abstract class AbstractGenerator
15
{
16
    /**
17
     * @param Route $route
18
     *
19
     * @return mixed
20
     */
21
    public function getDomain(Route $route)
22
    {
23
        return $route->domain() == null ? '*' : $route->domain();
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $route->domain() of type string|null|Illuminate\Routing\Route against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
24
    }
25
26
    /**
27
     * @param Route $route
28
     *
29
     * @return mixed
30
     */
31
    public function getUri(Route $route)
32
    {
33
        return $route->uri();
34
    }
35
36
    /**
37
     * @param Route $route
38
     *
39
     * @return mixed
40
     */
41
    public function getMethods(Route $route)
42
    {
43
        return array_diff($route->methods(), ['HEAD']);
44
    }
45
46
    /**
47
     * @param  \Illuminate\Routing\Route $route
48
     * @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...
49
     *
50
     * @return array
51
     */
52
    public function processRoute($route)
53
    {
54
        $routeAction = $route->getAction();
55
        $routeGroup = $this->getRouteGroup($routeAction['uses']);
56
        $docBlock = $this->parseDocBlock($routeAction['uses']);
57
        $content = $this->getResponse($docBlock['tags']);
58
59
        return [
60
            'id' => md5($this->getUri($route).':'.implode($this->getMethods($route))),
61
            'resource' => $routeGroup,
62
            'title' => $docBlock['short'],
63
            'description' => $docBlock['long'],
64
            'methods' => $this->getMethods($route),
65
            'uri' => $this->getUri($route),
66
            'parameters' => $this->getParametersFromDocBlock($docBlock['tags']),
67
            'response' => $content,
68
            'showresponse' => ! empty($content),
69
        ];
70
    }
71
72
    /**
73
     * Prepares / Disables route middlewares.
74
     *
75
     * @param  bool $disable
0 ignored issues
show
Bug introduced by
There is no parameter named $disable. 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...
76
     *
77
     * @return  void
78
     */
79
    abstract public function prepareMiddleware($enable = false);
80
81
    /**
82
     * Get the response from the docblock if available.
83
     *
84
     * @param array $tags
85
     *
86
     * @return mixed
87
     */
88
    protected function getDocblockResponse($tags)
89
    {
90
        $responseTags = array_filter($tags, function ($tag) {
91
            return $tag instanceof Tag && \strtolower($tag->getName()) == 'response';
92
        });
93
        if (empty($responseTags)) {
94
            return;
95
        }
96
        $responseTag = \array_first($responseTags);
97
98
        return \response(json_encode($responseTag->getContent()), 200, ['Content-Type' => 'application/json']);
99
    }
100
101
    /**
102
     * @param array $tags
103
     *
104
     * @return array
105
     */
106
    protected function getParametersFromDocBlock($tags)
107
    {
108
        $parameters = collect($tags)
109
            ->filter(function ($tag) {
110
                return $tag instanceof Tag && $tag->getName() === 'bodyParam';
111
            })
112
            ->mapWithKeys(function ($tag) {
113
                preg_match('/(.+?)\s+(.+?)\s+(required\s+)?(.+)/', $tag->getContent(), $content);
114
                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...
115
                $required = trim($required) == 'required' ? true : false;
116
                $type = $this->normalizeParameterType($type);
117
118
                return [$name => compact('type', 'description', 'required')];
119
            })->toArray();
120
121
        return $parameters;
122
    }
123
124
    /**
125
     * @param  $route
126
     * @param  $bindings
127
     * @param  $headers
128
     *
129
     * @return \Illuminate\Http\Response
130
     */
131
    protected function getRouteResponse($route, $bindings, $headers = [])
132
    {
133
        $uri = $this->addRouteModelBindings($route, $bindings);
134
135
        $methods = $this->getMethods($route);
136
137
        // Split headers into key - value pairs
138
        $headers = collect($headers)->map(function ($value) {
139
            $split = explode(':', $value); // explode to get key + values
140
            $key = array_shift($split); // extract the key and keep the values in the array
141
            $value = implode(':', $split); // implode values into string again
142
143
            return [trim($key) => trim($value)];
144
        })->collapse()->toArray();
145
146
        //Changes url with parameters like /users/{user} to /users/1
147
        $uri = preg_replace('/{(.*?)}/', 1, $uri); // 1 is the default value for route parameters
148
149
        return $this->callRoute(array_shift($methods), $uri, [], [], [], $headers);
150
    }
151
152
    /**
153
     * @param $route
154
     * @param array $bindings
155
     *
156
     * @return mixed
157
     */
158
    protected function addRouteModelBindings($route, $bindings)
159
    {
160
        $uri = $this->getUri($route);
161
        foreach ($bindings as $model => $id) {
162
            $uri = str_replace('{'.$model.'}', $id, $uri);
163
            $uri = str_replace('{'.$model.'?}', $id, $uri);
164
        }
165
166
        return $uri;
167
    }
168
169
    /**
170
     * @param  \Illuminate\Routing\Route  $route
171
     *
172
     * @return array
173
     */
174
    protected function parseDocBlock($route)
175
    {
176
        list($class, $method) = explode('@', $route);
177
        $reflection = new ReflectionClass($class);
178
        $reflectionMethod = $reflection->getMethod($method);
179
180
        $comment = $reflectionMethod->getDocComment();
181
        $phpdoc = new DocBlock($comment);
182
183
        return [
184
            'short' => $phpdoc->getShortDescription(),
185
            'long' => $phpdoc->getLongDescription()->getContents(),
186
            'tags' => $phpdoc->getTags(),
187
        ];
188
    }
189
190
    /**
191
     * @param  string  $route
192
     *
193
     * @return string
194
     */
195
    protected function getRouteGroup($route)
196
    {
197
        list($class, $method) = explode('@', $route);
0 ignored issues
show
Unused Code introduced by
The assignment to $method 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...
198
        $reflection = new ReflectionClass($class);
199
        $comment = $reflection->getDocComment();
200
        if ($comment) {
201
            $phpdoc = new DocBlock($comment);
202
            foreach ($phpdoc->getTags() as $tag) {
203
                if ($tag->getName() === 'resource') {
204
                    return $tag->getContent();
205
                }
206
            }
207
        }
208
209
        return 'general';
210
    }
211
212
    /**
213
     * Call the given URI and return the Response.
214
     *
215
     * @param  string  $method
216
     * @param  string  $uri
217
     * @param  array  $parameters
218
     * @param  array  $cookies
219
     * @param  array  $files
220
     * @param  array  $server
221
     * @param  string  $content
222
     *
223
     * @return \Illuminate\Http\Response
224
     */
225
    abstract public function callRoute($method, $uri, $parameters = [], $cookies = [], $files = [], $server = [], $content = null);
226
227
    /**
228
     * Transform headers array to array of $_SERVER vars with HTTP_* format.
229
     *
230
     * @param  array  $headers
231
     *
232
     * @return array
233
     */
234
    protected function transformHeadersToServerVars(array $headers)
235
    {
236
        $server = [];
237
        $prefix = 'HTTP_';
238
239
        foreach ($headers as $name => $value) {
240
            $name = strtr(strtoupper($name), '-', '_');
241
242
            if (! Str::startsWith($name, $prefix) && $name !== 'CONTENT_TYPE') {
243
                $name = $prefix.$name;
244
            }
245
246
            $server[$name] = $value;
247
        }
248
249
        return $server;
250
    }
251
252
    /**
253
     * @param $response
254
     *
255
     * @return mixed
256
     */
257
    private function getResponseContent($response)
258
    {
259
        if (empty($response)) {
260
            return '';
261
        }
262
        if ($response->headers->get('Content-Type') === 'application/json') {
263
            $content = json_decode($response->getContent(), JSON_PRETTY_PRINT);
264
        } else {
265
            $content = $response->getContent();
266
        }
267
268
        return $content;
269
    }
270
271
    /**
272
     * Get a response from the transformer tags.
273
     *
274
     * @param array $tags
275
     *
276
     * @return mixed
277
     */
278
    protected function getTransformerResponse($tags)
279
    {
280
        try {
281
            $transFormerTags = array_filter($tags, function ($tag) {
282
                if (! ($tag instanceof Tag)) {
283
                    return false;
284
                }
285
286
                return \in_array(\strtolower($tag->getName()), ['transformer', 'transformercollection']);
287
            });
288
            if (empty($transFormerTags)) {
289
                // we didn't have any of the tags so goodbye
290
                return false;
291
            }
292
293
            $modelTag = array_first(array_filter($tags, function ($tag) {
294
                if (! ($tag instanceof Tag)) {
295
                    return false;
296
                }
297
298
                return \in_array(\strtolower($tag->getName()), ['transformermodel']);
299
            }));
300
            $tag = \array_first($transFormerTags);
301
            $transformer = $tag->getContent();
302
            if (! \class_exists($transformer)) {
303
                // if we can't find the transformer we can't generate a response
304
                return;
305
            }
306
            $demoData = [];
307
308
            $reflection = new ReflectionClass($transformer);
309
            $method = $reflection->getMethod('transform');
310
            $parameter = \array_first($method->getParameters());
311
            $type = null;
312
            if ($modelTag) {
313
                $type = $modelTag->getContent();
314
            }
315
            if (\is_null($type)) {
316
                if ($parameter->hasType() &&
317
                    ! $parameter->getType()->isBuiltin() &&
318
                    \class_exists((string) $parameter->getType())) {
319
                    //we have a type
320
                    $type = (string) $parameter->getType();
321
                }
322
            }
323
            if ($type) {
324
                // we have a class so we try to create an instance
325
                $demoData = new $type;
326
                try {
327
                    // try a factory
328
                    $demoData = \factory($type)->make();
329
                } catch (\Exception $e) {
330
                    if ($demoData instanceof \Illuminate\Database\Eloquent\Model) {
331
                        // we can't use a factory but can try to get one from the database
332
                        try {
333
                            // check if we can find one
334
                            $newDemoData = $type::first();
335
                            if ($newDemoData) {
336
                                $demoData = $newDemoData;
337
                            }
338
                        } catch (\Exception $e) {
339
                            // do nothing
340
                        }
341
                    }
342
                }
343
            }
344
345
            $fractal = new Manager();
346
            $resource = [];
347
            if ($tag->getName() == 'transformer') {
348
                // just one
349
                $resource = new Item($demoData, new $transformer);
350
            }
351
            if ($tag->getName() == 'transformercollection') {
352
                // a collection
353
                $resource = new Collection([$demoData, $demoData], new $transformer);
354
            }
355
356
            return \response($fractal->createData($resource)->toJson());
0 ignored issues
show
Bug introduced by
It seems like $resource defined by array() on line 346 can also be of type array; however, League\Fractal\Manager::createData() does only seem to accept object<League\Fractal\Resource\ResourceInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
357
        } catch (\Exception $e) {
358
            // it isn't possible to parse the transformer
359
            return;
360
        }
361
    }
362
363
    private function getResponse(array $annotationTags)
364
    {
365
        $response = null;
366
        if ($docblockResponse = $this->getDocblockResponse($annotationTags)) {
367
            // we have a response from the docblock ( @response )
368
            $response = $docblockResponse;
369
        }
370
        if (! $response && ($transformerResponse = $this->getTransformerResponse($annotationTags))) {
371
            // we have a transformer response from the docblock ( @transformer || @transformercollection )
372
            $response = $transformerResponse;
373
        }
374
375
        $content = $response ? $this->getResponseContent($response) : null;
376
377
        return $content;
378
    }
379
380
    private function normalizeParameterType($type)
381
    {
382
        $typeMap = [
383
            'int' => 'integer',
384
            'bool' => 'boolean',
385
        ];
386
387
        return $type ? ($typeMap[$type] ?? $type) : 'string';
388
    }
389
}
390