Completed
Pull Request — master (#439)
by
unknown
01:31
created

GenerateDocumentation   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 284
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 40
lcom 1
cbo 8
dl 0
loc 284
rs 9.2
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A handle() 0 19 2
C writeMarkdown() 0 110 14
A processRoutes() 0 16 4
A isValidRoute() 0 4 2
C isRouteVisibleForDocumentation() 0 52 14
A skipRouteWithTags() 0 7 2
A generatePostmanCollection() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like GenerateDocumentation often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GenerateDocumentation, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Mpociot\ApiDoc\Commands;
4
5
use ReflectionClass;
6
use ReflectionException;
7
use Illuminate\Routing\Route;
8
use Illuminate\Console\Command;
9
use Mpociot\Reflection\DocBlock;
10
use Illuminate\Support\Collection;
11
use Mpociot\ApiDoc\Tools\Generator;
12
use Mpociot\ApiDoc\Tools\RouteMatcher;
13
use Mpociot\Documentarian\Documentarian;
14
use Mpociot\ApiDoc\Postman\CollectionWriter;
15
16
class GenerateDocumentation extends Command
17
{
18
    /**
19
     * The name and signature of the console command.
20
     *
21
     * @var string
22
     */
23
    protected $signature = 'apidoc:generate
24
                            {--force : Force rewriting of existing routes}
25
                            {--only-tags= : Comma-separated list of tags to generate}
26
                            {--skip-tags= : Comma-separated list of tags to skip}
27
    ';
28
29
    /**
30
     * The console command description.
31
     *
32
     * @var string
33
     */
34
    protected $description = 'Generate your API documentation from existing Laravel routes.';
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 void
48
     */
49
    public function handle()
50
    {
51
        $usingDingoRouter = strtolower(config('apidoc.router')) == 'dingo';
52
        if ($usingDingoRouter) {
53
            $routes = $this->routeMatcher->getDingoRoutesToBeDocumented(config('apidoc.routes'));
54
        } else {
55
            $routes = $this->routeMatcher->getLaravelRoutesToBeDocumented(config('apidoc.routes'));
56
        }
57
58
        $generator = new Generator();
59
        $parsedRoutes = $this->processRoutes($generator, $routes);
60
        $parsedRoutes = collect($parsedRoutes)->groupBy('group')
61
            ->sortBy(static function ($group) {
62
                /* @var $group Collection */
63
                return $group->first()['group'];
64
            }, SORT_NATURAL);
65
66
        $this->writeMarkdown($parsedRoutes);
67
    }
68
69
    /**
70
     * @param  Collection $parsedRoutes
71
     *
72
     * @return void
73
     */
74
    private function writeMarkdown($parsedRoutes)
75
    {
76
        $outputPath = config('apidoc.output');
77
        $targetFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'index.md';
78
        $compareFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'.compare.md';
79
        $prependFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'prepend.md';
80
        $appendFile = $outputPath.DIRECTORY_SEPARATOR.'source'.DIRECTORY_SEPARATOR.'append.md';
81
82
        $infoText = view('apidoc::partials.info')
83
            ->with('outputPath', ltrim($outputPath, 'public/'))
84
            ->with('showPostmanCollectionButton', config('apidoc.postman'));
85
86
        $parsedRouteOutput = $parsedRoutes->map(function ($routeGroup) {
87
            return $routeGroup->map(function ($route) {
88
                $route['output'] = (string) view('apidoc::partials.route')->with('route', $route)->render();
89
90
                return $route;
91
            });
92
        });
93
94
        $frontmatter = view('apidoc::partials.frontmatter');
95
        /*
96
         * In case the target file already exists, we should check if the documentation was modified
97
         * and skip the modified parts of the routes.
98
         */
99
        if (file_exists($targetFile) && file_exists($compareFile)) {
100
            $generatedDocumentation = file_get_contents($targetFile);
101
            $compareDocumentation = file_get_contents($compareFile);
102
103
            if (preg_match('/---(.*)---\\s<!-- START_INFO -->/is', $generatedDocumentation, $generatedFrontmatter)) {
104
                $frontmatter = trim($generatedFrontmatter[1], "\n");
105
            }
106
107
            $parsedRouteOutput->transform(function ($routeGroup) use ($generatedDocumentation, $compareDocumentation) {
108
                return $routeGroup->transform(function ($route) use ($generatedDocumentation, $compareDocumentation) {
109
                    if (preg_match('/<!-- START_'.$route['id'].' -->(.*)<!-- END_'.$route['id'].' -->/is', $generatedDocumentation, $existingRouteDoc)) {
110
                        $routeDocumentationChanged = (preg_match('/<!-- START_'.$route['id'].' -->(.*)<!-- END_'.$route['id'].' -->/is', $compareDocumentation, $lastDocWeGeneratedForThisRoute) && $lastDocWeGeneratedForThisRoute[1] !== $existingRouteDoc[1]);
111
                        if ($routeDocumentationChanged === false || $this->option('force')) {
112
                            if ($routeDocumentationChanged) {
113
                                $this->warn('Discarded manual changes for route ['.implode(',', $route['methods']).'] '.$route['uri']);
114
                            }
115
                        } else {
116
                            $this->warn('Skipping modified route ['.implode(',', $route['methods']).'] '.$route['uri']);
117
                            $route['modified_output'] = $existingRouteDoc[0];
118
                        }
119
                    }
120
121
                    return $route;
122
                });
123
            });
124
        }
125
126
        $prependFileContents = file_exists($prependFile)
127
            ? file_get_contents($prependFile)."\n" : '';
128
        $appendFileContents = file_exists($appendFile)
129
            ? "\n".file_get_contents($appendFile) : '';
130
131
        $documentarian = new Documentarian();
132
133
        $markdown = view('apidoc::documentarian')
134
            ->with('writeCompareFile', false)
135
            ->with('frontmatter', $frontmatter)
136
            ->with('infoText', $infoText)
137
            ->with('prependMd', $prependFileContents)
138
            ->with('appendMd', $appendFileContents)
139
            ->with('outputPath', config('apidoc.output'))
140
            ->with('showPostmanCollectionButton', config('apidoc.postman'))
141
            ->with('parsedRoutes', $parsedRouteOutput);
142
143
        if (! is_dir($outputPath)) {
144
            $documentarian->create($outputPath);
145
        }
146
147
        // Write output file
148
        file_put_contents($targetFile, $markdown);
149
150
        // Write comparable markdown file
151
        $compareMarkdown = view('apidoc::documentarian')
152
            ->with('writeCompareFile', true)
153
            ->with('frontmatter', $frontmatter)
154
            ->with('infoText', $infoText)
155
            ->with('prependMd', $prependFileContents)
156
            ->with('appendMd', $appendFileContents)
157
            ->with('outputPath', config('apidoc.output'))
158
            ->with('showPostmanCollectionButton', config('apidoc.postman'))
159
            ->with('parsedRoutes', $parsedRouteOutput);
160
161
        file_put_contents($compareFile, $compareMarkdown);
162
163
        $this->info('Wrote index.md to: '.$outputPath);
164
165
        $this->info('Generating API HTML code');
166
167
        $documentarian->generate($outputPath);
168
169
        $this->info('Wrote HTML documentation to: '.$outputPath.'/index.html');
170
171
        if (config('apidoc.postman')) {
172
            $this->info('Generating Postman collection');
173
174
            file_put_contents($outputPath.DIRECTORY_SEPARATOR.'collection.json', $this->generatePostmanCollection($parsedRoutes));
175
        }
176
177
        if ($logo = config('apidoc.logo')) {
178
            copy(
179
                $logo,
180
                $outputPath.DIRECTORY_SEPARATOR.'images'.DIRECTORY_SEPARATOR.'logo.png'
181
            );
182
        }
183
    }
184
185
    /**
186
     * @param Generator $generator
187
     * @param array $routes
188
     *
189
     * @return array
190
     */
191
    private function processRoutes(Generator $generator, array $routes)
192
    {
193
        $parsedRoutes = [];
194
        foreach ($routes as $routeItem) {
195
            $route = $routeItem['route'];
196
            /** @var Route $route */
197
            if ($this->isValidRoute($route) && $this->isRouteVisibleForDocumentation($route)) {
198
                $parsedRoutes[] = $generator->processRoute($route, $routeItem['apply']);
199
                $this->info('Processed route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));
200
            } else {
201
                $this->warn('Skipping route: ['.implode(',', $generator->getMethods($route)).'] '.$generator->getUri($route));
202
            }
203
        }
204
205
        return $parsedRoutes;
206
    }
207
208
    /**
209
     * @param $route
210
     *
211
     * @return bool
212
     */
213
    private function isValidRoute(Route $route)
214
    {
215
        return ! is_callable($route->getAction()['uses']) && ! is_null($route->getAction()['uses']);
216
    }
217
218
    /**
219
     * @param $route
220
     *
221
     * @throws ReflectionException
222
     *
223
     * @return bool
224
     */
225
    private function isRouteVisibleForDocumentation($route)
226
    {
227
        list($class, $method) = explode('@', $route->getAction()['uses']);
228
        $reflection = new ReflectionClass($class);
229
230
        if (! $reflection->hasMethod($method)) {
231
            return false;
232
        }
233
234
        $comment = $reflection->getMethod($method)->getDocComment();
235
236
        $allowedTags = str_replace(',,', ',', $this->option('only-tags'));
237
        $disallowedTags = str_replace(',,', ',', $this->option('skip-tags'));
238
239
        $allowedTags = trim($allowedTags) ? explode(',', $allowedTags) : [];
240
        $disallowedTags = trim($disallowedTags) ? explode(',', $disallowedTags) : [];
241
242
        $routeTags = $route->getAction('tags');
243
244
        if ($routeTags) {
245
            if (! is_array($routeTags)) {
246
                $routeTags = [$routeTags];
247
            }
248
            if (! $this->skipRouteWithTags($routeTags, $allowedTags, $disallowedTags)) {
249
                return true;
250
            }
251
        }
252
253
        if ($comment) {
254
            $phpdoc = new DocBlock($comment);
255
256
            if (count($allowedTags) && ! $phpdoc->hasTag('tags')) {
257
                return false;
258
            }
259
260
            return collect($phpdoc->getTags())
261
                ->filter(function ($tag) use ($allowedTags, $disallowedTags) {
262
                    if ((count($allowedTags) || count($disallowedTags)) &&
263
                        $tag->getName() == 'tags') {
264
                        $tags = explode(' ', $tag->getContent());
265
266
                        return $this->skipRouteWithTags($tags, $allowedTags, $disallowedTags);
267
                    }
268
269
                    return $tag->getName() === 'hideFromAPIDocumentation';
270
                })
271
                ->isEmpty();
272
        } elseif (count($allowedTags)) {
273
            return false;
274
        }
275
        return true;
276
    }
277
278
    private function skipRouteWithTags(array $tags, array $allowedTags, array $disallowedTags)
279
    {
280
        $containedAllowedTags = array_intersect($tags, $allowedTags);
281
        $containedDisallowedTags = array_intersect($tags, $disallowedTags);
282
283
        return ! count($containedAllowedTags) || count($containedDisallowedTags);
284
    }
285
286
    /**
287
     * Generate Postman collection JSON file.
288
     *
289
     * @param Collection $routes
290
     *
291
     * @return string
292
     */
293
    private function generatePostmanCollection(Collection $routes)
294
    {
295
        $writer = new CollectionWriter($routes);
296
297
        return $writer->getCollection();
298
    }
299
}
300