RouteWrapper::isSupported()   A
last analyzed

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 ReflectionClass;
6
use ReflectionParameter;
7
use Illuminate\Routing\Route;
8
use ReflectionFunctionAbstract;
9
use Illuminate\Support\Collection;
10
use Mpociot\Reflection\DocBlock\Tag;
11
use AlexWells\ApiDocsGenerator\Helpers;
12
use Illuminate\Foundation\Http\FormRequest;
13
use AlexWells\ApiDocsGenerator\Exceptions\InvalidFormat;
14
use AlexWells\ApiDocsGenerator\Exceptions\ClosureRouteException;
15
16
class RouteWrapper
17
{
18
    /**
19
     * Original route object.
20
     *
21
     * @var Route
22
     */
23
    protected $route;
24
25
    /**
26
     * Injected array of options from instance.
27
     *
28
     * @var array
29
     */
30
    protected $options;
31
32
    /**
33
     * Parsed FormRequest's reflection.
34
     *
35
     * @var ReflectionClass
36
     */
37
    protected $parsedFormRequest;
38
39
    /**
40
     * Parsed doc block for controller.
41
     *
42
     * @var DocBlockWrapper
43
     */
44
    protected $controllerDockBlock;
45
46
    /**
47
     * Parsed doc block for method.
48
     *
49
     * @var DocBlockWrapper
50
     */
51
    protected $methodDocBlock;
52
53
    /**
54
     * Parsed describe tag parser.
55
     *
56
     * @var DescribeParameterTagsParser
57
     */
58
    protected $describeTagsParser;
59
60
    /**
61
     * Parsed default tag parser.
62
     *
63
     * @var DefaultParameterTagsParser
64
     */
65
    protected $defaultTagsParser;
66
67
    /**
68
     * RouteWrapper constructor.
69
     *
70
     * @param Route $route
71
     * @param array $options
72
     */
73
    public function __construct($route, $options)
74
    {
75
        $this->route = $route;
76
        $this->options = $options;
77
    }
78
79
    /**
80
     * Returns original (laravel's) route object.
81
     *
82
     * @return Route
83
     */
84
    public function getOriginalRoute()
85
    {
86
        return $this->route;
87
    }
88
89
    /**
90
     * Parse the route and return summary information for it.
91
     *
92
     * @return array
93
     */
94
    public function getSummary()
95
    {
96
        return [
97
            'id' => $this->getSignature(),
98
            'resource' => $this->getResource(),
99
            'uri' => $this->getUri(),
100
            'methods' => $this->getMethods(),
101
            'title' => $this->getTitle(),
102
            'description' => $this->getDescription(),
103
            'parameters' => [
104
                'path' => $this->getPathParameters(),
105
                'query' => $this->getQueryParameters()
106
            ],
107
            'responses' => $this->getResponses()
108
        ];
109
    }
110
111
    /**
112
     * Returns route's unique signature.
113
     *
114
     * @return string
115
     */
116
    public function getSignature()
117
    {
118
        return md5($this->getUri() . ':' . implode(',', $this->getMethods()));
119
    }
120
121
    /**
122
     * Returns route's name.
123
     *
124
     * @return string
125
     */
126
    public function getName()
127
    {
128
        return $this->route->getName();
129
    }
130
131
    /**
132
     * Returns route's HTTP methods.
133
     *
134
     * @return array
135
     */
136
    public function getMethods()
137
    {
138
        if (method_exists($this->route, 'getMethods')) {
139
            $methods = $this->route->getMethods();
140
        } else {
141
            $methods = $this->route->methods();
142
        }
143
144
        return array_except($methods, 'HEAD');
145
    }
146
147
    /**
148
     * Returns route's URI.
149
     *
150
     * @return string
151
     */
152
    public function getUri()
153
    {
154
        if (method_exists($this->route, 'getUri')) {
155
            return $this->route->getUri();
156
        }
157
158
        return $this->route->uri();
159
    }
160
161
    /**
162
     * Parse the action and return it.
163
     *
164
     * @return string[2]
165
     */
166
    protected function parseAction()
167
    {
168
        return explode('@', $this->getAction(), 2);
169
    }
170
171
    /**
172
     * Returns route's action string.
173
     *
174
     * @throws ClosureRouteException
175
     *
176
     * @return string
177
     */
178
    public function getAction()
179
    {
180
        if (! $this->isSupported()) {
181
            throw new ClosureRouteException('Closure callbacks are not supported. Please use a controller method.');
182
        }
183
184
        return $this->getActionSafe();
185
    }
186
187
    /**
188
     * Returns route's action string safe (without any exceptions).
189
     *
190
     * @return string
191
     */
192
    public function getActionSafe()
193
    {
194
        return $this->route->getActionName();
195
    }
196
197
    /**
198
     * Checks if the route is supported.
199
     *
200
     * @return bool
201
     */
202
    public function isSupported()
203
    {
204
        return isset($this->route->getAction()['controller']);
205
    }
206
207
    /**
208
     * Checks if it matches any of the masks.
209
     *
210
     * @param array $masks
211
     *
212
     * @return bool
213
     */
214
    public function matchesAnyMask($masks)
215
    {
216
        return collect($masks)
217
            ->contains(function ($mask) {
218
                return str_is($mask, $this->getUri()) || str_is($mask, $this->getName());
219
            });
220
    }
221
222
    /**
223
     * Parses path parameters and returns them.
224
     *
225
     * @return array
226
     */
227
    public function getPathParameters()
228
    {
229
        // Get all path parameters from path, including ? symbols after them
230
        preg_match_all('/\{(.*?)\}/', $this->getUri(), $matches);
231
232
        return array_map(function ($pathParameter) {
233
            return (new PathParameterParser($pathParameter, ! $this->options['noTypeChecks'], $this))->parse();
234
        }, $matches[1]);
235
    }
236
237
    /**
238
     * Parses validation rules and converts them into an array of parameters.
239
     *
240
     * @return array
241
     */
242
    public function getQueryParameters()
243
    {
244
        $params = [];
245
246
        foreach ($this->getQueryValidationRules() as $name => $rules) {
247
            $params[] = [
248
                'name' => $name,
249
                'default' => $this->getDefaultTagsParser()->get('query', $name),
250
                'rules' => $rules,
251
                'description' => $this->getDescribeTagsParser()->get('query', $name)
252
            ];
253
        }
254
255
        return $params;
256
    }
257
258
    /**
259
     * Return an array of query validation rules.
260
     *
261
     * @return array
262
     */
263
    protected function getQueryValidationRules()
264
    {
265
        if (! ($reflection = $this->getFormRequestClassReflection())) {
266
            return [];
267
        }
268
269
        return FormRequestRulesParser::withReflection($reflection)
270
            ->parse();
271
    }
272
273
    /**
274
     * Returns route's title (defaults to method name).
275
     *
276
     * @return string
277
     */
278
    public function getTitle()
279
    {
280
        if($title = $this->getMethodDocBlock()->getShortDescription()) {
281
            return $title;
282
        }
283
284
        $title = $this->getMethodReflection()->getName();
285
286
        return Helpers::functionNameToText($title);
287
    }
288
289
    /**
290
     * Returns route's description.
291
     *
292
     * @return string|null
293
     */
294
    public function getDescription()
295
    {
296
        $description = $this->getMethodDocBlock()->getLongDescription()->getContents();
297
298
        return Helpers::clearNewlines($description);
299
    }
300
301
    /**
302
     * Returns route's resource.
303
     *
304
     * @return array
305
     */
306
    public function getResource()
307
    {
308
        return $this->getDocBlocks()
309
            ->map(function (DocBlockWrapper $docBlock) {
310
                $tag = $docBlock->getDocTag('resource');
311
312
                if (! $tag) {
0 ignored issues
show
introduced by
$tag is of type Mpociot\Reflection\DocBlock\Tag, thus it always evaluated to true.
Loading history...
313
                    return null;
314
                }
315
316
                $resource = $tag->getContent();
317
318
                if(! $resource) {
319
                    throw new InvalidFormat('Resource name not specified');
320
                }
321
322
                $resource = (new StringToArrayParser($resource))->parse();
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) {
0 ignored issues
show
introduced by
$methodParameter is of type ReflectionParameter, thus it always evaluated to true.
Loading history...
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