Passed
Branch master (76b039)
by Alex
02:42
created

RouteWrapper::getResponses()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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