Completed
Pull Request — master (#690)
by
unknown
01:34
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
use ReflectionFunction;
20
21
class GenerateDocumentation extends Command
22
{
23
    /**
24
     * The name and signature of the console command.
25
     *
26
     * @var string
27
     */
28
    protected $signature = 'apidoc:generate
29
                            {--force : Force rewriting of existing routes}
30
    ';
31
32
    /**
33
     * The console command description.
34
     *
35
     * @var string
36
     */
37
    protected $description = 'Generate your API documentation from existing Laravel routes.';
38
39
    /**
40
     * @var DocumentationConfig
41
     */
42
    private $docConfig;
43
44
    /**
45
     * @var string
46
     */
47
    private $baseUrl;
48
49
    /**
50
     * Execute the console command.
51
     *
52
     * @param RouteMatcherInterface $routeMatcher
53
     *
54
     * @return void
55
     */
56
    public function handle(RouteMatcherInterface $routeMatcher)
57
    {
58
        // Using a global static variable here, so fuck off if you don't like it.
59
        // Also, the --verbose option is included with all Artisan commands.
60
        Flags::$shouldBeVerbose = $this->option('verbose');
61
62
        $this->docConfig = new DocumentationConfig(config('apidoc'));
63
        $this->baseUrl = $this->docConfig->get('base_url') ?? config('app.url');
64
65
        URL::forceRootUrl($this->baseUrl);
66
67
        $routes = $routeMatcher->getRoutes($this->docConfig->get('routes'), $this->docConfig->get('router'));
68
69
        $generator = new Generator($this->docConfig);
70
        $parsedRoutes = $this->processRoutes($generator, $routes);
71
72
        $groupedRoutes = collect($parsedRoutes)
73
            ->groupBy('metadata.groupName')
74
            ->sortBy(static function ($group) {
75
                /* @var $group Collection */
76
                return $group->first()['metadata']['groupName'];
77
            }, SORT_NATURAL);
78
        $writer = new Writer(
79
            $this,
80
            $this->docConfig,
81
            $this->option('force')
82
        );
83
        $writer->writeDocs($groupedRoutes);
84
    }
85
86
    /**
87
     * @param \Mpociot\ApiDoc\Extracting\Generator $generator
88
     * @param Match[] $routes
89
     *
90
     * @throws \ReflectionException
91
     *
92
     * @return array
93
     */
94
    private function processRoutes(Generator $generator, array $routes)
95
    {
96
        $parsedRoutes = [];
97
        foreach ($routes as $routeItem) {
98
            $route = $routeItem->getRoute();
99
            /** @var Route $route */
100
            $messageFormat = '%s route: [%s] %s';
101
            $routeMethods = implode(',', $generator->getMethods($route));
102
            $routePath = $generator->getUri($route);
103
104
            $routeControllerAndMethod = Utils::getRouteClassAndMethodNames($route->getAction());
105
            if (! $this->isValidRoute($routeControllerAndMethod)) {
106
                $this->warn(sprintf($messageFormat, 'Skipping invalid', $routeMethods, $routePath));
107
                continue;
108
            }
109
110
            if (! $this->doesControllerMethodExist($routeControllerAndMethod)) {
111
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath).': Controller method does not exist.');
112
                continue;
113
            }
114
115
            if (! $this->isRouteVisibleForDocumentation($routeControllerAndMethod)) {
116
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath).': @hideFromAPIDocumentation was specified.');
117
                continue;
118
            }
119
120
            try {
121
                $parsedRoutes[] = $generator->processRoute($route, $routeItem->getRules());
122
                $this->info(sprintf($messageFormat, 'Processed', $routeMethods, $routePath));
123
            } catch (\Exception $exception) {
124
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath).'- Exception '.get_class($exception).' encountered : '.$exception->getMessage());
125
            }
126
        }
127
128
        return $parsedRoutes;
129
    }
130
131
    /**
132
     * @param array $routeControllerAndMethod
133
     *
134
     * @return bool
135
     */
136
    private function isValidRoute(array $routeControllerAndMethod = null)
137
    {
138
        if (is_array($routeControllerAndMethod)) {
139
            [$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...
140
            if (Utils::isInvokableObject($classOrObject)) {
141
                return true;
142
            }
143
            $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...
144
        }
145
        return ! is_callable($routeControllerAndMethod)
146
            && ! is_null($routeControllerAndMethod);
147
    }
148
149
    /**
150
     * @param array $routeControllerAndMethod
151
     *
152
     * @throws ReflectionException
153
     *
154
     * @return bool
155
     */
156
    private function doesControllerMethodExist(array $routeControllerAndMethod)
157
    {
158
        return method_exists(...$routeControllerAndMethod);
159
    }
160
161
    /**
162
     * @param array $routeControllerAndMethod
163
     *
164
     * @throws ReflectionException
165
     *
166
     * @return bool
167
     */
168
    private function isRouteVisibleForDocumentation(array $routeControllerAndMethod)
169
    {
170
        [$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...
171
172
        if ($class instanceof \Closure) {
173
            $reflection = new ReflectionFunction($class);
174
            $comment = $reflection->getDocComment();
175
        } else {
176
            $reflection = new ReflectionClass($class);
177
            $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...
178
        }
179
180
        if ($comment) {
181
            $phpdoc = new DocBlock($comment);
182
183
            return collect($phpdoc->getTags())
184
                ->filter(function ($tag) {
185
                    return $tag->getName() === 'hideFromAPIDocumentation';
186
                })
187
                ->isEmpty();
188
        }
189
190
        return true;
191
    }
192
}
193