Completed
Push — master ( 2ebe64...c5c478 )
by
unknown
08:06 queued 06:18
created

GenerateDocumentation   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 233
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 10
dl 0
loc 233
rs 10
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A handle() 0 21 3
C writeMarkdown() 0 103 13
A processRoutes() 0 15 4
A isValidRoute() 0 4 2
A isRouteVisibleForDocumentation() 0 17 2
A generatePostmanCollection() 0 6 1
1
<?php
2
3
namespace Mpociot\ApiDoc\Commands;
4
5
use Mpociot\ApiDoc\Tools\RouteMatcher;
6
use ReflectionClass;
7
use Illuminate\Routing\Route;
8
use Illuminate\Console\Command;
9
use Mpociot\Reflection\DocBlock;
10
use Illuminate\Support\Collection;
11
use Mpociot\Documentarian\Documentarian;
12
use Mpociot\ApiDoc\Postman\CollectionWriter;
13
use Mpociot\ApiDoc\Generators\DingoGenerator;
14
use Mpociot\ApiDoc\Generators\LaravelGenerator;
15
use Mpociot\ApiDoc\Generators\AbstractGenerator;
16
17
class GenerateDocumentation extends Command
18
{
19
    /**
20
     * The name and signature of the console command.
21
     *
22
     * @var string
23
     */
24
    protected $signature = 'apidoc:generate
25
                            {--force : Force rewriting of existing routes}
26
    ';
27
28
    /**
29
     * The console command description.
30
     *
31
     * @var string
32
     */
33
    protected $description = 'Generate your API documentation from existing Laravel routes.';
34
35
36
    private $routeMatcher;
37
38
    public function __construct(RouteMatcher $routeMatcher)
39
    {
40
        parent::__construct();
41
        $this->routeMatcher = $routeMatcher;
42
    }
43
44
    /**
45
     * Execute the console command.
46
     *
47
     * @return false|null
48
     */
49
    public function handle()
50
    {
51
        $routes = config('apidoc.router') == 'dingo'
52
            ? $this->routeMatcher->getDingoRoutesToBeDocumented(config('apidoc.routes'))
53
            : $this->routeMatcher->getLaravelRoutesToBeDocumented(config('apidoc.routes'));
54
55
        if ($this->option('router') === 'laravel') {
56
            $generator = new LaravelGenerator();
57
        } else {
58
            $generator = new DingoGenerator();
59
        }
60
61
62
        $parsedRoutes = $this->processRoutes($generator, $routes);
63
        $parsedRoutes = collect($parsedRoutes)->groupBy('resource')
64
            ->sort(function ($a, $b) {
65
                return strcmp($a->first()['resource'], $b->first()['resource']);
66
        });
67
68
        $this->writeMarkdown($parsedRoutes);
69
    }
70
71
    /**
72
     * @param  Collection $parsedRoutes
73
     *
74
     * @return void
75
     */
76
    private function writeMarkdown($parsedRoutes)
77
    {
78
        $outputPath = $this->option('output');
79
        $targetFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'index.md';
80
        $compareFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'.compare.md';
81
        $prependFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'prepend.md';
82
        $appendFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'append.md';
83
84
        $infoText = view('apidoc::partials.info')
85
            ->with('outputPath', ltrim($outputPath, 'public/'))
86
            ->with('showPostmanCollectionButton', ! $this->option('noPostmanCollection'));
87
88
        $parsedRouteOutput = $parsedRoutes->map(function ($routeGroup) {
89
            return $routeGroup->map(function ($route) {
90
                $route['output'] = (string) view('apidoc::partials.route')->with('parsedRoute', $route)->render();
91
92
                return $route;
93
            });
94
        });
95
96
        $frontmatter = view('apidoc::partials.frontmatter');
97
        /*
98
         * In case the target file already exists, we should check if the documentation was modified
99
         * and skip the modified parts of the routes.
100
         */
101
        if (file_exists($targetFile) && file_exists($compareFile)) {
102
            $generatedDocumentation = file_get_contents($targetFile);
103
            $compareDocumentation = file_get_contents($compareFile);
104
105
            if (preg_match('/---(.*)---\\s<!-- START_INFO -->/is', $generatedDocumentation, $generatedFrontmatter)) {
106
                $frontmatter = trim($generatedFrontmatter[1], "\n");
107
            }
108
109
            $parsedRouteOutput->transform(function ($routeGroup) use ($generatedDocumentation, $compareDocumentation) {
110
                return $routeGroup->transform(function ($route) use ($generatedDocumentation, $compareDocumentation) {
111
                    if (preg_match('/<!-- START_'.$route['id'].' -->(.*)<!-- END_'.$route['id'].' -->/is', $generatedDocumentation, $routeMatch)) {
112
                        $routeDocumentationChanged = (preg_match('/<!-- START_'.$route['id'].' -->(.*)<!-- END_'.$route['id'].' -->/is', $compareDocumentation, $compareMatch) && $compareMatch[1] !== $routeMatch[1]);
113
                        if ($routeDocumentationChanged === false || $this->option('force')) {
114
                            if ($routeDocumentationChanged) {
115
                                $this->warn('Discarded manual changes for route ['.implode(',', $route['methods']).'] '.$route['uri']);
116
                            }
117
                        } else {
118
                            $this->warn('Skipping modified route ['.implode(',', $route['methods']).'] '.$route['uri']);
119
                            $route['modified_output'] = $routeMatch[0];
120
                        }
121
                    }
122
123
                    return $route;
124
                });
125
            });
126
        }
127
128
        $prependFileContents = file_exists($prependFile)
129
            ? file_get_contents($prependFile)."\n" : '';
130
        $appendFileContents = file_exists($appendFile)
131
            ? "\n".file_get_contents($appendFile) : '';
132
133
        $documentarian = new Documentarian();
134
135
        $markdown = view('apidoc::documentarian')
136
            ->with('writeCompareFile', false)
137
            ->with('frontmatter', $frontmatter)
138
            ->with('infoText', $infoText)
139
            ->with('prependMd', $prependFileContents)
140
            ->with('appendMd', $appendFileContents)
141
            ->with('outputPath', $this->option('output'))
142
            ->with('showPostmanCollectionButton', ! $this->option('noPostmanCollection'))
143
            ->with('parsedRoutes', $parsedRouteOutput);
144
145
        if (! is_dir($outputPath)) {
146
            $documentarian->create($outputPath);
147
        }
148
149
        // Write output file
150
        file_put_contents($targetFile, $markdown);
151
152
        // Write comparable markdown file
153
        $compareMarkdown = view('apidoc::documentarian')
154
            ->with('writeCompareFile', true)
155
            ->with('frontmatter', $frontmatter)
156
            ->with('infoText', $infoText)
157
            ->with('prependMd', $prependFileContents)
158
            ->with('appendMd', $appendFileContents)
159
            ->with('outputPath', $this->option('output'))
160
            ->with('showPostmanCollectionButton', ! $this->option('noPostmanCollection'))
161
            ->with('parsedRoutes', $parsedRouteOutput);
162
163
        file_put_contents($compareFile, $compareMarkdown);
164
165
        $this->info('Wrote index.md to: '.$outputPath);
166
167
        $this->info('Generating API HTML code');
168
169
        $documentarian->generate($outputPath);
170
171
        $this->info('Wrote HTML documentation to: '.$outputPath.'/index.html');
172
173
        if ($this->option('noPostmanCollection') !== true) {
174
            $this->info('Generating Postman collection');
175
176
            file_put_contents($outputPath.DIRECTORY_SEPARATOR.'collection.json', $this->generatePostmanCollection($parsedRoutes));
177
        }
178
    }
179
180
181
    /**
182
     * @param AbstractGenerator $generator
183
     * @param array $routes
184
     * @return array
185
     *
186
     */
187
    private function processRoutes(AbstractGenerator $generator, array $routes)
188
    {
189
        $parsedRoutes = [];
190
        foreach ($routes as ['route' => $route, 'apply' => $apply]) {
191
            /** @var Route $route */
192
                if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route->getAction()['uses'])) {
0 ignored issues
show
Bug introduced by
The variable $route does not exist. Did you mean $routes?

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...
193
                    $parsedRoutes[] = $generator->processRoute($route, $apply);
0 ignored issues
show
Bug introduced by
The variable $route does not exist. Did you mean $routes?

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...
Bug introduced by
The variable $apply 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...
194
                    $this->info('Processed route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));
0 ignored issues
show
Bug introduced by
The variable $route does not exist. Did you mean $routes?

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...
195
                } else {
196
                    $this->warn('Skipping route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));
0 ignored issues
show
Bug introduced by
The variable $route does not exist. Did you mean $routes?

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...
197
                }
198
        }
199
200
        return $parsedRoutes;
201
    }
202
203
    /**
204
     * @param $route
205
     *
206
     * @return bool
207
     */
208
    private function isValidRoute(Route $route)
209
    {
210
        return ! is_callable($route->getAction()['uses']) && ! is_null($route->getAction()['uses']);
211
    }
212
213
    /**
214
     * @param $route
215
     *
216
     * @return bool
217
     */
218
    private function isRouteVisibleForDocumentation($route)
219
    {
220
        list($class, $method) = explode('@', $route);
221
        $reflection = new ReflectionClass($class);
222
        $comment = $reflection->getMethod($method)->getDocComment();
223
        if ($comment) {
224
            $phpdoc = new DocBlock($comment);
225
226
            return collect($phpdoc->getTags())
227
                ->filter(function ($tag) use ($route) {
228
                    return $tag->getName() === 'hideFromAPIDocumentation';
229
                })
230
                ->isEmpty();
231
        }
232
233
        return true;
234
    }
235
236
    /**
237
     * Generate Postman collection JSON file.
238
     *
239
     * @param Collection $routes
240
     *
241
     * @return string
242
     */
243
    private function generatePostmanCollection(Collection $routes)
244
    {
245
        $writer = new CollectionWriter($routes);
246
247
        return $writer->getCollection();
248
    }
249
}
250