Completed
Push — master ( c3ccef...6f6146 )
by
unknown
01:25
created

GenerateDocumentation::handle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 9.456
c 0
b 0
f 0
cc 1
nc 1
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
            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
     *
161
     * @param array $routeControllerAndMethod
162
     *
163
     * @throws ReflectionException
164
     *
165
     * @return bool
166
     */
167
    private function doesControllerMethodExist(array $routeControllerAndMethod)
168
    {
169
        [$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...
170
        $reflection = new ReflectionClass($class);
171
172
        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...
173
            return false;
174
        }
175
176
        return true;
177
    }
178
179
    /**
180
     * @param array $routeControllerAndMethod
181
     *
182
     * @throws ReflectionException
183
     *
184
     * @return bool
185
     */
186
    private function isRouteVisibleForDocumentation(array $routeControllerAndMethod)
187
    {
188
        [$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...
189
        $reflection = new ReflectionClass($class);
190
191
        $comment = $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...
192
193
        if ($comment) {
194
            $phpdoc = new DocBlock($comment);
195
196
            return collect($phpdoc->getTags())
197
                ->filter(function ($tag) {
198
                    return $tag->getName() === 'hideFromAPIDocumentation';
199
                })
200
                ->isEmpty();
201
        }
202
203
        return true;
204
    }
205
}
206