Completed
Pull Request — master (#690)
by
unknown
02:14 queued 52s
created

GenerateDocumentation::isValidRoute()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 4
nc 5
nop 1
1
<?php
2
3
namespace Mpociot\ApiDoc\Commands;
4
5
use Illuminate\Console\Command;
6
use Illuminate\Routing\Route;
7
use Illuminate\Support\Collection;
8
use Illuminate\Support\Facades\URL;
9
use Mpociot\ApiDoc\Extracting\Generator;
10
use Mpociot\ApiDoc\Matching\RouteMatcher\Match;
11
use Mpociot\ApiDoc\Matching\RouteMatcherInterface;
12
use Mpociot\ApiDoc\Tools\DocumentationConfig;
13
use Mpociot\ApiDoc\Tools\Flags;
14
use Mpociot\ApiDoc\Tools\Utils;
15
use Mpociot\ApiDoc\Writing\Writer;
16
use Mpociot\Reflection\DocBlock;
17
use ReflectionClass;
18
use ReflectionException;
19
20
class GenerateDocumentation extends Command
21
{
22
    /**
23
     * The name and signature of the console command.
24
     *
25
     * @var string
26
     */
27
    protected $signature = 'apidoc:generate
28
                            {--force : Force rewriting of existing routes}
29
    ';
30
31
    /**
32
     * The console command description.
33
     *
34
     * @var string
35
     */
36
    protected $description = 'Generate your API documentation from existing Laravel routes.';
37
38
    /**
39
     * @var DocumentationConfig
40
     */
41
    private $docConfig;
42
43
    /**
44
     * @var string
45
     */
46
    private $baseUrl;
47
48
    /**
49
     * Execute the console command.
50
     *
51
     * @param RouteMatcherInterface $routeMatcher
52
     *
53
     * @return void
54
     */
55
    public function handle(RouteMatcherInterface $routeMatcher)
56
    {
57
        // Using a global static variable here, so fuck off if you don't like it.
58
        // Also, the --verbose option is included with all Artisan commands.
59
        Flags::$shouldBeVerbose = $this->option('verbose');
60
61
        $this->docConfig = new DocumentationConfig(config('apidoc'));
62
        $this->baseUrl = $this->docConfig->get('base_url') ?? config('app.url');
63
64
        URL::forceRootUrl($this->baseUrl);
65
66
        $routes = $routeMatcher->getRoutes($this->docConfig->get('routes'), $this->docConfig->get('router'));
67
68
        $generator = new Generator($this->docConfig);
69
        $parsedRoutes = $this->processRoutes($generator, $routes);
70
71
        $groupedRoutes = collect($parsedRoutes)
72
            ->groupBy('metadata.groupName')
73
            ->sortBy(static function ($group) {
74
                /* @var $group Collection */
75
                return $group->first()['metadata']['groupName'];
76
            }, SORT_NATURAL);
77
        $writer = new Writer(
78
            $this,
79
            $this->docConfig,
80
            $this->option('force')
81
        );
82
        $writer->writeDocs($groupedRoutes);
83
    }
84
85
    /**
86
     * @param \Mpociot\ApiDoc\Extracting\Generator $generator
87
     * @param Match[] $routes
88
     *
89
     * @throws \ReflectionException
90
     *
91
     * @return array
92
     */
93
    private function processRoutes(Generator $generator, array $routes)
94
    {
95
        $parsedRoutes = [];
96
        foreach ($routes as $routeItem) {
97
            $route = $routeItem->getRoute();
98
            /** @var Route $route */
99
            $messageFormat = '%s route: [%s] %s';
100
            $routeMethods = implode(',', $generator->getMethods($route));
101
            $routePath = $generator->getUri($route);
102
103
            $routeControllerAndMethod = Utils::getRouteClassAndMethodNames($route->getAction());
104
            if (! $this->isValidRoute($routeControllerAndMethod)) {
105
                $this->warn(sprintf($messageFormat, 'Skipping invalid', $routeMethods, $routePath));
106
                continue;
107
            }
108
109
            if (! $this->doesControllerMethodExist($routeControllerAndMethod)) {
110
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath).': Controller method does not exist.');
111
                continue;
112
            }
113
114
            if (! $this->isRouteVisibleForDocumentation($routeControllerAndMethod)) {
115
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath).': @hideFromAPIDocumentation was specified.');
116
                continue;
117
            }
118
119
            try {
120
                $parsedRoutes[] = $generator->processRoute($route, $routeItem->getRules());
121
                $this->info(sprintf($messageFormat, 'Processed', $routeMethods, $routePath));
122
            } catch (\Exception $exception) {
123
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath).'- Exception '.get_class($exception).' encountered : '.$exception->getMessage());
124
            }
125
        }
126
127
        return $parsedRoutes;
128
    }
129
130
    /**
131
     * @param array $routeControllerAndMethod
132
     *
133
     * @return bool
134
     */
135
    private function isValidRoute(array $routeControllerAndMethod = null)
136
    {
137
        if (is_array($routeControllerAndMethod)) {
138
            [$classOrObject, $method] = $routeControllerAndMethod;
0 ignored issues
show
Bug introduced by
The variable $classOrObject does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $method does not exist. Did you mean $routeControllerAndMethod?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
139
            if (Utils::isInvokableObject($classOrObject)) {
140
                return true;
141
            }
142
            $routeControllerAndMethod = $classOrObject.'@'.$method;
0 ignored issues
show
Bug introduced by
The variable $method does not exist. Did you mean $routeControllerAndMethod?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
143
        }
144
145
        return ! is_callable($routeControllerAndMethod) && ! is_null($routeControllerAndMethod);
146
    }
147
148
    /**
149
     * @param array $routeControllerAndMethod
150
     *
151
     * @throws ReflectionException
152
     *
153
     * @return bool
154
     */
155
    private function doesControllerMethodExist(array $routeControllerAndMethod)
156
    {
157
        [$class, $method] = $routeControllerAndMethod;
0 ignored issues
show
Bug introduced by
The variable $class does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $method does not exist. Did you mean $routeControllerAndMethod?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
158
        $reflection = new ReflectionClass($class);
159
160
        if (! $reflection->hasMethod($method)) {
0 ignored issues
show
Bug introduced by
The variable $method does not exist. Did you mean $routeControllerAndMethod?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
161
            return false;
162
        }
163
164
        return true;
165
    }
166
167
    /**
168
     * @param array $routeControllerAndMethod
169
     *
170
     * @throws ReflectionException
171
     *
172
     * @return bool
173
     */
174
    private function isRouteVisibleForDocumentation(array $routeControllerAndMethod)
175
    {
176
        $comment = Utils::reflectRouteMethod($routeControllerAndMethod)->getDocComment();
177
178
        if ($comment) {
179
            $phpdoc = new DocBlock($comment);
180
181
            return collect($phpdoc->getTags())
182
                ->filter(function ($tag) {
183
                    return $tag->getName() === 'hideFromAPIDocumentation';
184
                })
185
                ->isEmpty();
186
        }
187
188
        return true;
189
    }
190
}
191