Passed
Push — master ( 76b039...f6fc8d )
by Alex
04:36
created

RouteWrapper::getMethodReflection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
1
<?php
2
3
namespace AlexWells\ApiDocsGenerator\Parsers;
4
5
use AlexWells\ApiDocsGenerator\Exceptions\ClosureRouteException;
6
use AlexWells\ApiDocsGenerator\Exceptions\InvalidTagFormat;
7
use AlexWells\ApiDocsGenerator\Helpers;
8
use Illuminate\Contracts\Validation\Factory as ValidationFactory;
9
use Illuminate\Contracts\Validation\ValidatesWhenResolved;
10
use Illuminate\Foundation\Http\FormRequest;
11
use Illuminate\Routing\Route;
12
use Illuminate\Support\Collection;
13
use Mpociot\Reflection\DocBlock\Tag;
14
use ReflectionClass;
15
use ReflectionFunctionAbstract;
16
use ReflectionParameter;
17
18
class RouteWrapper
19
{
20
    /**
21
     * Original route object.
22
     *
23
     * @var Route
24
     */
25
    protected $route;
26
27
    /**
28
     * Injected array of options from instance.
29
     *
30
     * @var array
31
     */
32
    protected $options;
33
34
    /**
35
     * Parsed FormRequest's reflection.
36
     *
37
     * @var ReflectionClass
38
     */
39
    protected $parsedFormRequest;
40
41
    /**
42
     * Parsed doc block for controller.
43
     *
44
     * @var DocBlockWrapper
45
     */
46
    protected $controllerDockBlock;
47
48
    /**
49
     * Parsed doc block for method.
50
     *
51
     * @var DocBlockWrapper
52
     */
53
    protected $methodDocBlock;
54
55
    /**
56
     * Parsed describe tag parser.
57
     *
58
     * @var DescribeParameterTagsParser
59
     */
60
    protected $describeTagsParser;
61
62
    /**
63
     * Parsed default tag parser.
64
     *
65
     * @var DefaultParameterTagsParser
66
     */
67
    protected $defaultTagsParser;
68
69
    /**
70
     * RouteWrapper constructor.
71
     *
72
     * @param Route $route
73
     * @param array $options
74
     */
75
    public function __construct($route, $options)
76
    {
77
        $this->route = $route;
78
        $this->options = $options;
79
    }
80
81
    /**
82
     * Returns original (laravel's) route object.
83
     *
84
     * @return Route
85
     */
86
    public function getOriginalRoute()
87
    {
88
        return $this->route;
89
    }
90
91
    /**
92
     * Parse the route and return summary information for it.
93
     *
94
     * @return array
95
     */
96
    public function getSummary()
97
    {
98
        return [
99
            'id' => $this->getSignature(),
100
            'resource' => $this->getResourceName(),
101
            'uri' => $this->getUri(),
102
            'methods' => $this->getMethods(),
103
            'title' => $this->getTitle(),
104
            'description' => $this->getDescription(),
105
            'parameters' => [
106
                'path' => $this->getPathParameters(),
107
                'query' => $this->getQueryParameters()
108
            ],
109
            'responses' => $this->getResponses()
110
        ];
111
    }
112
113
    /**
114
     * Returns route's unique signature.
115
     *
116
     * @return string
117
     */
118
    public function getSignature()
119
    {
120
        return md5($this->getUri() . ':' . implode(',', $this->getMethods()));
121
    }
122
123
    /**
124
     * Returns route's name.
125
     *
126
     * @return string
127
     */
128
    public function getName()
129
    {
130
        return $this->route->getName();
131
    }
132
133
    /**
134
     * Returns route's HTTP methods.
135
     *
136
     * @return array
137
     */
138
    public function getMethods()
139
    {
140
        if (method_exists($this->route, 'getMethods')) {
141
            $methods = $this->route->getMethods();
142
        } else {
143
            $methods = $this->route->methods();
144
        }
145
146
        return array_except($methods, 'HEAD');
147
    }
148
149
    /**
150
     * Returns route's URI.
151
     *
152
     * @return string
153
     */
154
    public function getUri()
155
    {
156
        if (method_exists($this->route, 'getUri')) {
157
            return $this->route->getUri();
158
        }
159
160
        return $this->route->uri();
161
    }
162
163
    /**
164
     * Parse the action and return it.
165
     *
166
     * @return string[2]
167
     */
168
    protected function parseAction()
169
    {
170
        return explode('@', $this->getAction(), 2);
171
    }
172
173
    /**
174
     * Returns route's action string.
175
     *
176
     * @throws ClosureRouteException
177
     *
178
     * @return string
179
     */
180
    public function getAction()
181
    {
182
        if (! $this->isSupported()) {
183
            throw new ClosureRouteException('Closure callbacks are not supported. Please use a controller method.');
184
        }
185
186
        return $this->getActionSafe();
187
    }
188
189
    /**
190
     * Returns route's action string safe (without any exceptions).
191
     *
192
     * @return string
193
     */
194
    public function getActionSafe()
195
    {
196
        return $this->route->getActionName();
197
    }
198
199
    /**
200
     * Checks if the route is supported.
201
     *
202
     * @return bool
203
     */
204
    public function isSupported()
205
    {
206
        return isset($this->route->getAction()['controller']);
207
    }
208
209
    /**
210
     * Checks if it matches any of the masks.
211
     *
212
     * @param array $masks
213
     *
214
     * @return bool
215
     */
216
    public function matchesAnyMask($masks)
217
    {
218
        return collect($masks)
219
            ->contains(function ($mask) {
220
                return str_is($mask, $this->getUri()) || str_is($mask, $this->getName());
221
            });
222
    }
223
224
    /**
225
     * Parses path parameters and returns them.
226
     *
227
     * @return array
228
     */
229
    public function getPathParameters()
230
    {
231
        // Get all path parameters from path, including ? symbols after them
232
        preg_match_all('/\{(.*?)\}/', $this->getUri(), $matches);
233
234
        return array_map(function ($pathParameter) {
235
            return (new PathParameterParser($pathParameter, !$this->options['noTypeChecks'], $this))->parse();
236
        }, $matches[1]);
237
    }
238
239
    /**
240
     * Parses validation rules and converts them into an array of parameters.
241
     *
242
     * @return array
243
     */
244
    public function getQueryParameters()
245
    {
246
        $params = [];
247
248
        foreach ($this->getQueryValidationRules() as $name => $rules) {
249
            $params[] = [
250
                'name' => $name,
251
                'default' => $this->getDefaultTagsParser()->get('query', $name),
252
                'rules' => $rules,
253
                'description' => $this->getDescribeTagsParser()->get('query', $name)
254
            ];
255
        }
256
257
        return $params;
258
    }
259
260
    /**
261
     * Return an array of query validation rules.
262
     *
263
     * @return array
264
     */
265
    protected function getQueryValidationRules()
266
    {
267
        if (! ($reflection = $this->getFormRequestClassReflection())) {
268
            return [];
269
        }
270
271
        return FormRequestRulesParser::withReflection($reflection)
272
            ->parse();
273
    }
274
275
    /**
276
     * Returns route's title (defaults to method name).
277
     *
278
     * @return string
279
     */
280
    public function getTitle()
281
    {
282
        if($title = $this->getMethodDocBlock()->getShortDescription()) {
283
            return $title;
284
        }
285
286
        $title = $this->getMethodReflection()->getName();
287
288
        return Helpers::functionNameToText($title);
289
    }
290
291
    /**
292
     * Returns route's description.
293
     *
294
     * @return string|null
295
     */
296
    public function getDescription()
297
    {
298
        $description = $this->getMethodDocBlock()->getLongDescription()->getContents();
299
300
        return Helpers::clearNewlines($description);
301
    }
302
303
    /**
304
     * Returns route's resource name.
305
     *
306
     * @return string
307
     */
308
    public function getResourceName()
309
    {
310
        return $this->getDocBlocks()
311
            ->map(function (DocBlockWrapper $docBlock) {
312
                $tag = $docBlock->getDocTag('resource');
313
314
                if (! $tag) {
315
                    return null;
316
                }
317
318
                $resource = $tag->getContent();
319
320
                if(! $resource) {
321
                    throw new InvalidTagFormat('Resource name not specified');
322
                }
323
324
                return $resource;
325
            })
326
            ->filter()
327
            ->first(null, 'Unclassified routes');
328
    }
329
330
    /**
331
     * Returns all route's responses.
332
     *
333
     * @return array
334
     */
335
    public function getResponses()
336
    {
337
        return $this->getMethodDocBlock()
338
            ->getDocTags('response')
339
            ->map(function (Tag $tag) {
340
                return (new ResponseParser($tag->getContent()))->parse();
341
            })
342
            ->filter()
343
            ->toArray();
344
    }
345
346
    /**
347
     * Checks if the route is hidden from docs by annotation.
348
     *
349
     * @return bool
350
     */
351
    public function isHiddenFromDocs()
352
    {
353
        return $this->getDocBlocks()
354
            ->contains(function (DocBlockWrapper $docBlock) {
355
                return $docBlock->hasDocTag('docsHide');
356
            });
357
    }
358
359
    /**
360
     * Returns describe tags parser.
361
     *
362
     * @return DescribeParameterTagsParser
363
     */
364
    public function getDescribeTagsParser()
365
    {
366
        if (! $this->describeTagsParser) {
367
            return $this->describeTagsParser = new DescribeParameterTagsParser($this->getDocBlocksArray());
368
        }
369
370
        return $this->describeTagsParser;
371
    }
372
373
    /**
374
     * Returns default tags parser.
375
     *
376
     * @return DefaultParameterTagsParser
377
     */
378
    public function getDefaultTagsParser()
379
    {
380
        if (! $this->defaultTagsParser) {
381
            return $this->defaultTagsParser = new DefaultParameterTagsParser($this->getDocBlocksArray());
382
        }
383
384
        return $this->defaultTagsParser;
385
    }
386
387
    /**
388
     * Get all doc blocks.
389
     *
390
     * @return Collection|DocBlockWrapper[]
391
     */
392
    protected function getDocBlocks()
393
    {
394
        return collect($this->getDocBlocksArray());
395
    }
396
397
    /**
398
     * Get all doc blocks as array.
399
     *
400
     * @return DocBlockWrapper[]
401
     */
402
    protected function getDocBlocksArray()
403
    {
404
        return [$this->getMethodDocBlock(), $this->getControllerDocBlock()];
405
    }
406
407
    /**
408
     * Returns DocBlock for route method.
409
     *
410
     * @return DocBlockWrapper
411
     */
412
    protected function getMethodDocBlock()
413
    {
414
        if (! $this->methodDocBlock) {
415
            return $this->methodDocBlock = new DocBlockWrapper($this->getMethodReflection());
416
        }
417
418
        return $this->methodDocBlock;
419
    }
420
421
    /**
422
     * Returns DocBlock for the controller.
423
     *
424
     * @return DocBlockWrapper
425
     */
426
    protected function getControllerDocBlock()
427
    {
428
        if (! $this->controllerDockBlock) {
429
            return $this->controllerDockBlock = new DocBlockWrapper($this->getControllerReflection());
430
        }
431
432
        return $this->controllerDockBlock;
433
    }
434
435
    /**
436
     * Returns route's FormRequest reflection if exists.
437
     *
438
     * @return ReflectionClass
439
     */
440
    protected function getFormRequestClassReflection()
441
    {
442
        if (! $this->parsedFormRequest) {
443
            $methodParameter = $this->getMethodParameterByType(FormRequest::class);
444
445
            if (! $methodParameter) {
446
                return null;
447
            }
448
449
            return $this->parsedFormRequest = $methodParameter->getClass();
450
        }
451
452
        return $this->parsedFormRequest;
453
    }
454
455
    /**
456
     * Returns method parameter by type (single).
457
     *
458
     * @param string $filter
459
     *
460
     * @return ReflectionParameter
461
     */
462
    protected function getMethodParameterByType($filter)
463
    {
464
        $formRequestParameters = $this->getMethodParametersByType($filter);
465
466
        if (empty($formRequestParameters)) {
467
            return null;
468
        }
469
470
        return array_first($formRequestParameters);
471
    }
472
473
    /**
474
     * Returns route method's parameters filtered by type.
475
     *
476
     * @param string $filter A parameter type to filter
477
     *
478
     * @return ReflectionParameter[]
479
     */
480
    protected function getMethodParametersByType($filter)
481
    {
482
        return $this->getMethodParameters(function (ReflectionParameter $parameter) use ($filter) {
483
            if (! ($type = $parameter->getType())) {
484
                return false;
485
            }
486
487
            if ($type->isBuiltin()) {
488
                return strval($type) === $filter;
489
            }
490
491
            return ($class = $parameter->getClass()) && $class->isSubclassOf($filter);
492
        });
493
    }
494
495
    /**
496
     * Returns route method's parameters filtered by name (ignore case).
497
     *
498
     * TODO: get reflections into other class
499
     *
500
     * @param string $name
501
     *
502
     * @return ReflectionParameter
503
     */
504
    public function getMethodParameterByName($name)
505
    {
506
        return $this->getMethodParameter(function (ReflectionParameter $parameter) use ($name) {
507
            return strtolower($parameter->getName()) === strtolower($name);
508
        });
509
    }
510
511
    /**
512
     * Returns route method's parameter filtered by callable.
513
     *
514
     * @param callable $filter A callable returning bool
515
     *
516
     * @return ReflectionParameter
517
     */
518
    protected function getMethodParameter(callable $filter)
519
    {
520
        return array_first($this->getMethodParameters($filter));
521
    }
522
523
    /**
524
     * Returns route method's parameters filtered by callable.
525
     *
526
     * @param callable $filter A callable returning bool
527
     *
528
     * @return ReflectionParameter[]
529
     */
530
    protected function getMethodParameters(callable $filter = null)
531
    {
532
        $parameters = $this->getMethodReflection()->getParameters();
533
534
        if ($filter === null) {
535
            return $parameters;
536
        }
537
538
        return array_filter($parameters, $filter);
539
    }
540
541
    /**
542
     * Returns route method's reflection.
543
     *
544
     * @return ReflectionFunctionAbstract
545
     */
546
    protected function getMethodReflection()
547
    {
548
        return $this->getControllerReflection()->getMethod($this->parseAction()[1]);
549
    }
550
551
    /**
552
     * Returns controller class reflection.
553
     *
554
     * @return ReflectionClass
555
     */
556
    protected function getControllerReflection()
557
    {
558
        return new ReflectionClass($this->parseAction()[0]);
559
    }
560
}
561