Passed
Push — master ( f1daf3...8fd0f0 )
by Alex
02:27
created

RouteWrapper::getParameterDescriptions()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
c 0
b 0
f 0
rs 9.4285
cc 2
eloc 3
nc 2
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
        preg_match_all('/\{(.*?)\}/', $this->getUri(), $matches);
234
        $methodParameters = $this->getMethodReflection()->getParameters();
235
236
        return array_map(function ($pathParameter) use ($methodParameters) {
0 ignored issues
show
Unused Code introduced by
The import $methodParameters is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
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
                $content = $tag->getContent();
391
                $content = Helpers::clearNewlines($content);
392
393
                if(! $content) {
394
                    return null;
395
                }
396
397
                // TODO: extract into a class and each "replace" into method like `replaceWhat`
398
399
                $cutOffInQuotes = "\s*(?=([^\"]*\"[^\"]*\")*[^\"]*$)";
400
401
                // TODO: allow to use single quotes (and replace them automatically)
402
                // replace `int[]` with `[ :: int ]`
403
                $content = preg_replace("/(\w+)\[\]$cutOffInQuotes/", '[ :: $1 ]', $content);
404
                // replace `nested: {}` with `"nested": {}`
405
                $content = preg_replace("/(\w+)\s*:[^:]$cutOffInQuotes/", '"$1": ', $content);
406
                // replace `year :: int` with `"year": {"$ref": "int"}`
407
                $content = preg_replace("/(\w+)\s*::\s*(\w+)$cutOffInQuotes/", '"$1": {"$ref": "$2"}', $content);
408
                // replace `:: int` with `{"$ref": "int"}`
409
                $content = preg_replace("/\s*::\s*(\w+)$cutOffInQuotes/", '{"$ref": "$1"}', $content);
410
                // replace `:: {}` with `{"$ref": {}}`
411
                $content = preg_replace("/::\s*{(.*)}$cutOffInQuotes/", '{"$ref": {$1}}', $content);
412
413
                $content = json_decode($content, true);
414
415
                if(! $content) {
416
                    throw new InvalidTagFormat('Response tag format is invalid, JSON decode error: ' . json_last_error_msg());
417
                }
418
419
                return $content;
420
            })
421
            ->filter()
422
            ->toArray();
423
    }
424
425
    /**
426
     * Checks if the route is hidden from docs by annotation.
427
     *
428
     * @return bool
429
     */
430
    public function isHiddenFromDocs()
431
    {
432
        return $this->getDocBlocks()
433
            ->contains(function (DocBlockWrapper $docBlock) {
434
                return $docBlock->hasDocTag('docsHide');
435
            });
436
    }
437
438
    /**
439
     * Returns describe tags parser.
440
     *
441
     * @return DescribeParameterTagsParser
442
     */
443
    public function getDescribeTagsParser()
444
    {
445
        if (! $this->describeTagsParser) {
446
            return $this->describeTagsParser = new DescribeParameterTagsParser($this->getDocBlocksArray());
447
        }
448
449
        return $this->describeTagsParser;
450
    }
451
452
    /**
453
     * Returns default tags parser.
454
     *
455
     * @return DefaultParameterTagsParser
456
     */
457
    public function getDefaultTagsParser()
458
    {
459
        if (! $this->defaultTagsParser) {
460
            return $this->defaultTagsParser = new DefaultParameterTagsParser($this->getDocBlocksArray());
461
        }
462
463
        return $this->defaultTagsParser;
464
    }
465
466
    /**
467
     * Get all doc blocks.
468
     *
469
     * @return Collection|DocBlockWrapper[]
470
     */
471
    protected function getDocBlocks()
472
    {
473
        return collect($this->getDocBlocksArray());
474
    }
475
476
    /**
477
     * Get all doc blocks as array.
478
     *
479
     * @return DocBlockWrapper[]
480
     */
481
    protected function getDocBlocksArray()
482
    {
483
        return [$this->getMethodDocBlock(), $this->getControllerDocBlock()];
484
    }
485
486
    /**
487
     * Returns DocBlock for route method.
488
     *
489
     * @return DocBlockWrapper
490
     */
491
    protected function getMethodDocBlock()
492
    {
493
        if (! $this->methodDocBlock) {
494
            return $this->methodDocBlock = new DocBlockWrapper($this->getMethodReflection());
495
        }
496
497
        return $this->methodDocBlock;
498
    }
499
500
    /**
501
     * Returns DocBlock for the controller.
502
     *
503
     * @return DocBlockWrapper
504
     */
505
    protected function getControllerDocBlock()
506
    {
507
        if (! $this->controllerDockBlock) {
508
            return $this->controllerDockBlock = new DocBlockWrapper($this->getControllerReflection());
509
        }
510
511
        return $this->controllerDockBlock;
512
    }
513
514
    /**
515
     * Returns route's FormRequest reflection if exists.
516
     *
517
     * @return ReflectionClass
518
     */
519
    protected function getFormRequestClassReflection()
520
    {
521
        if (! $this->parsedFormRequest) {
522
            $methodParameter = $this->getMethodParameterByType(FormRequest::class);
523
524
            if (! $methodParameter) {
525
                return null;
526
            }
527
528
            return $this->parsedFormRequest = $methodParameter->getClass();
529
        }
530
531
        return $this->parsedFormRequest;
532
    }
533
534
    /**
535
     * Returns method parameter by type (single).
536
     *
537
     * @param string $filter
538
     *
539
     * @return ReflectionParameter
540
     */
541
    protected function getMethodParameterByType($filter)
542
    {
543
        $formRequestParameters = $this->getMethodParametersByType($filter);
544
545
        if (empty($formRequestParameters)) {
546
            return null;
547
        }
548
549
        return array_first($formRequestParameters);
550
    }
551
552
    /**
553
     * Returns route method's parameters filtered by type.
554
     *
555
     * @param string $filter A parameter type to filter
556
     *
557
     * @return ReflectionParameter[]
558
     */
559
    protected function getMethodParametersByType($filter)
560
    {
561
        return $this->getMethodParameters(function (ReflectionParameter $parameter) use ($filter) {
562
            if (! ($type = $parameter->getType())) {
563
                return false;
564
            }
565
566
            if ($type->isBuiltin()) {
567
                return strval($type) === $filter;
568
            }
569
570
            return ($class = $parameter->getClass()) && $class->isSubclassOf($filter);
571
        });
572
    }
573
574
    /**
575
     * Returns route method's parameters filtered by name (ignore case).
576
     *
577
     * TODO: get reflections into other class
578
     *
579
     * @param string $name
580
     *
581
     * @return ReflectionParameter
582
     */
583
    public function getMethodParameterByName($name)
584
    {
585
        return $this->getMethodParameter(function (ReflectionParameter $parameter) use ($name) {
586
            return strtolower($parameter->getName()) === strtolower($name);
587
        });
588
    }
589
590
    /**
591
     * Returns route method's parameter filtered by callable.
592
     *
593
     * @param callable $filter A callable returning bool
594
     *
595
     * @return ReflectionParameter
596
     */
597
    protected function getMethodParameter(callable $filter)
598
    {
599
        return array_first($this->getMethodParameters($filter));
600
    }
601
602
    /**
603
     * Returns route method's parameters filtered by callable.
604
     *
605
     * @param callable $filter A callable returning bool
606
     *
607
     * @return ReflectionParameter[]
608
     */
609
    protected function getMethodParameters(callable $filter = null)
610
    {
611
        $parameters = $this->getMethodReflection()->getParameters();
612
613
        if ($filter === null) {
614
            return $parameters;
615
        }
616
617
        return array_filter($parameters, $filter);
618
    }
619
620
    /**
621
     * Returns route method's reflection.
622
     *
623
     * @return ReflectionFunctionAbstract
624
     */
625
    protected function getMethodReflection()
626
    {
627
        return $this->getControllerReflection()->getMethod($this->parseAction()[1]);
628
    }
629
630
    /**
631
     * Returns controller class reflection.
632
     *
633
     * @return ReflectionClass
634
     */
635
    protected function getControllerReflection()
636
    {
637
        return new ReflectionClass($this->parseAction()[0]);
638
    }
639
}
640