Completed
Push — master ( c6a664...3473b8 )
by Alex
05:09
created

RouteWrapper::getAction()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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