GenerateDocumentation::processRoutes()   B
last analyzed

Complexity

Conditions 7
Paths 8

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 8.3306
c 0
b 0
f 0
cc 7
nc 8
nop 2
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
            if ($this->isClosureRoute($route->getAction())) {
104
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath) . ': Closure routes are not supported.');
105
                continue;
106
            }
107
108
            $routeControllerAndMethod = Utils::getRouteClassAndMethodNames($route->getAction());
109
            if (! $this->isValidRoute($routeControllerAndMethod)) {
110
                $this->warn(sprintf($messageFormat, 'Skipping invalid', $routeMethods, $routePath));
111
                continue;
112
            }
113
114
            if (! $this->doesControllerMethodExist($routeControllerAndMethod)) {
115
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath) . ': Controller method does not exist.');
116
                continue;
117
            }
118
119
            if (! $this->isRouteVisibleForDocumentation($routeControllerAndMethod)) {
120
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath) . ': @hideFromAPIDocumentation was specified.');
121
                continue;
122
            }
123
124
            try {
125
                $parsedRoutes[] = $generator->processRoute($route, $routeItem->getRules());
126
                $this->info(sprintf($messageFormat, 'Processed', $routeMethods, $routePath));
127
            } catch (\Exception $exception) {
128
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath) . '- Exception ' . get_class($exception) . ' encountered : ' . $exception->getMessage());
129
            }
130
        }
131
132
        return $parsedRoutes;
133
    }
134
135
    /**
136
     * @param array $routeControllerAndMethod
137
     *
138
     * @return bool
139
     */
140
    private function isValidRoute(array $routeControllerAndMethod = null)
141
    {
142
        if (is_array($routeControllerAndMethod)) {
143
            $routeControllerAndMethod = implode('@', $routeControllerAndMethod);
144
        }
145
146
        return ! is_callable($routeControllerAndMethod) && ! is_null($routeControllerAndMethod);
147
    }
148
149
    /**
150
     * @param array $routeAction
151
     *
152
     * @return bool
153
     */
154
    private function isClosureRoute(array $routeAction)
155
    {
156
        return $routeAction['uses'] instanceof \Closure;
157
    }
158
159
    /**
160
     * @param array $routeControllerAndMethod
161
     *
162
     * @throws ReflectionException
163
     *
164
     * @return bool
165
     */
166
    private function doesControllerMethodExist(array $routeControllerAndMethod)
167
    {
168
        [$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...
169
        $reflection = new ReflectionClass($class);
170
171
        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...
172
            return false;
173
        }
174
175
        return true;
176
    }
177
178
    /**
179
     * @param array $routeControllerAndMethod
180
     *
181
     * @throws ReflectionException
182
     *
183
     * @return bool
184
     */
185
    private function isRouteVisibleForDocumentation(array $routeControllerAndMethod)
186
    {
187
        [$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...
188
        $reflection = new ReflectionClass($class);
189
190
        $tags = collect();
191
192
        foreach (
193
            array_filter([
194
                $reflection->getDocComment(),
195
                $reflection->getMethod($method)->getDocComment()
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...
196
            ]) as $comment
197
        ) {
198
            $phpdoc = new DocBlock($comment);
199
            $tags = $tags->concat($phpdoc->getTags());
200
        }
201
202
        return $tags->filter(function ($tag) {
203
            return $tag->getName() === 'hideFromAPIDocumentation';
204
        })->isEmpty();
205
    }
206
}
207