Completed
Pull Request — master (#672)
by Guy
01:40
created

GenerateDocumentation::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
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
     * @return void
53
     */
54
    public function handle(RouteMatcherInterface $routeMatcher)
55
    {
56
        // Using a global static variable here, so fuck off if you don't like it.
57
        // Also, the --verbose option is included with all Artisan commands.
58
        Flags::$shouldBeVerbose = $this->option('verbose');
59
60
        $this->docConfig = new DocumentationConfig(config('apidoc'));
61
        $this->baseUrl = $this->docConfig->get('base_url') ?? config('app.url');
62
63
        URL::forceRootUrl($this->baseUrl);
64
65
        $routes = $routeMatcher->getRoutes($this->docConfig->get('routes'), $this->docConfig->get('router'));
66
67
        $generator = new Generator($this->docConfig);
68
        $parsedRoutes = $this->processRoutes($generator, $routes);
69
70
        $groupedRoutes = collect($parsedRoutes)
71
            ->groupBy('metadata.groupName')
72
            ->sortBy(static function ($group) {
73
                /* @var $group Collection */
74
                return $group->first()['metadata']['groupName'];
75
            }, SORT_NATURAL);
76
        $writer = new Writer(
77
            $this,
78
            $this->docConfig,
79
            $this->option('force')
80
        );
81
        $writer->writeDocs($groupedRoutes);
82
    }
83
84
    /**
85
     * @param \Mpociot\ApiDoc\Extracting\Generator $generator
86
     * @param Match[] $routes
87
     *
88
     * @return array
89
     */
90
    private function processRoutes(Generator $generator, array $routes)
91
    {
92
        $parsedRoutes = [];
93
        foreach ($routes as $routeItem) {
94
            $route = $routeItem->getRoute();
95
            /** @var Route $route */
96
            $messageFormat = '%s route: [%s] %s';
97
            $routeMethods = implode(',', $generator->getMethods($route));
98
            $routePath = $generator->getUri($route);
99
100
            if (! $this->isValidRoute($route) || ! $this->isRouteVisibleForDocumentation($route->getAction())) {
101
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath));
102
                continue;
103
            }
104
105
            try {
106
                $parsedRoutes[] = $generator->processRoute($route, $routeItem->getRules());
107
                $this->info(sprintf($messageFormat, 'Processed', $routeMethods, $routePath));
108
            } catch (\Exception $exception) {
109
                $this->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath).' - '.$exception->getMessage());
110
            }
111
        }
112
113
        return $parsedRoutes;
114
    }
115
116
    /**
117
     * @param Route $route
118
     *
119
     * @return bool
120
     */
121
    private function isValidRoute(Route $route)
122
    {
123
        $action = Utils::getRouteClassAndMethodNames($route->getAction());
124
        if (is_array($action)) {
125
            $action = implode('@', $action);
126
        }
127
128
        return ! is_callable($action) && ! is_null($action);
129
    }
130
131
    /**
132
     * @param array $action
133
     *
134
     * @throws ReflectionException
135
     *
136
     * @return bool
137
     */
138
    private function isRouteVisibleForDocumentation(array $action)
139
    {
140
        list($class, $method) = Utils::getRouteClassAndMethodNames($action);
141
        $reflection = new ReflectionClass($class);
142
143
        if (! $reflection->hasMethod($method)) {
144
            return false;
145
        }
146
147
        $comment = $reflection->getMethod($method)->getDocComment();
148
149
        if ($comment) {
150
            $phpdoc = new DocBlock($comment);
151
152
            return collect($phpdoc->getTags())
153
                ->filter(function ($tag) {
154
                    return $tag->getName() === 'hideFromAPIDocumentation';
155
                })
156
                ->isEmpty();
157
        }
158
159
        return true;
160
    }
161
}
162