Writer   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 299
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 34
lcom 1
cbo 2
dl 0
loc 299
rs 9.68
c 0
b 0
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 3
A writeDocs() 0 14 1
C writeMarkdownAndSourceFiles() 0 82 9
A generateMarkdownOutputForEachRoute() 0 23 5
A writePostmanCollection() 0 22 4
A generatePostmanCollection() 0 10 1
A getMarkdownToPrepend() 0 8 2
A getMarkdownToAppend() 0 8 2
A copyAssetsFromSourceFolderToPublicFolder() 0 16 3
A moveOutputFromSourceFolderToTargetFolder() 0 20 3
A writeHtmlDocs() 0 13 1
1
<?php
2
3
namespace Mpociot\ApiDoc\Writing;
4
5
use Illuminate\Console\Command;
6
use Illuminate\Support\Collection;
7
use Illuminate\Support\Facades\Storage;
8
use Mpociot\ApiDoc\Tools\DocumentationConfig;
9
use Mpociot\Documentarian\Documentarian;
10
11
class Writer
12
{
13
    /**
14
     * @var Command
15
     */
16
    protected $output;
17
18
    /**
19
     * @var DocumentationConfig
20
     */
21
    private $config;
22
23
    /**
24
     * @var string
25
     */
26
    private $baseUrl;
27
28
    /**
29
     * @var bool
30
     */
31
    private $forceIt;
32
33
    /**
34
     * @var bool
35
     */
36
    private $shouldGeneratePostmanCollection = true;
37
38
    /**
39
     * @var Documentarian
40
     */
41
    private $documentarian;
42
43
    /**
44
     * @var bool
45
     */
46
    private $isStatic;
47
48
    /**
49
     * @var string
50
     */
51
    private $sourceOutputPath;
52
53
    /**
54
     * @var string
55
     */
56
    private $outputPath;
57
58
    public function __construct(Command $output, DocumentationConfig $config = null, bool $forceIt = false)
59
    {
60
        // If no config is injected, pull from global
61
        $this->config = $config ?: new DocumentationConfig(config('apidoc'));
62
        $this->baseUrl = $this->config->get('base_url') ?? config('app.url');
63
        $this->forceIt = $forceIt;
64
        $this->output = $output;
65
        $this->shouldGeneratePostmanCollection = $this->config->get('postman.enabled', false);
66
        $this->documentarian = new Documentarian();
67
        $this->isStatic = $this->config->get('type') === 'static';
68
        $this->sourceOutputPath = 'resources/docs';
69
        $this->outputPath = $this->isStatic ? ($this->config->get('output_folder') ?? 'public/docs') : 'resources/views/apidoc';
70
    }
71
72
    public function writeDocs(Collection $routes)
73
    {
74
        // The source files (index.md, js/, css/, and images/) always go in resources/docs/source.
75
        // The static assets (js/, css/, and images/) always go in public/docs/.
76
        // For 'static' docs, the output files (index.html, collection.json) go in public/docs/.
77
        // For 'laravel' docs, the output files (index.blade.php, collection.json)
78
        // go in resources/views/apidoc/ and storage/app/apidoc/ respectively.
79
80
        $this->writeMarkdownAndSourceFiles($routes);
81
82
        $this->writeHtmlDocs();
83
84
        $this->writePostmanCollection($routes);
85
    }
86
87
    /**
88
     * @param  Collection $parsedRoutes
89
     *
90
     * @return void
91
     */
92
    public function writeMarkdownAndSourceFiles(Collection $parsedRoutes)
93
    {
94
        $targetFile = $this->sourceOutputPath . '/source/index.md';
95
        $compareFile = $this->sourceOutputPath . '/source/.compare.md';
96
97
        $infoText = view('apidoc::partials.info')
98
            ->with('outputPath', 'docs')
99
            ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection);
100
101
        $settings = ['languages' => $this->config->get('example_languages')];
102
        // Generate Markdown for each route
103
        $parsedRouteOutput = $this->generateMarkdownOutputForEachRoute($parsedRoutes, $settings);
104
105
        $frontmatter = view('apidoc::partials.frontmatter')
106
            ->with('settings', $settings);
107
108
        /*
109
         * If the target file already exists,
110
         * we check if the documentation was modified
111
         * and skip the modified parts of the routes.
112
         */
113
        if (file_exists($targetFile) && file_exists($compareFile)) {
114
            $generatedDocumentation = file_get_contents($targetFile);
115
            $compareDocumentation = file_get_contents($compareFile);
116
117
            $parsedRouteOutput->transform(function (Collection $routeGroup) use ($generatedDocumentation, $compareDocumentation) {
118
                return $routeGroup->transform(function (array $route) use ($generatedDocumentation, $compareDocumentation) {
119
                    if (preg_match('/<!-- START_' . $route['id'] . ' -->(.*)<!-- END_' . $route['id'] . ' -->/is', $generatedDocumentation, $existingRouteDoc)) {
120
                        $routeDocumentationChanged = (preg_match('/<!-- START_' . $route['id'] . ' -->(.*)<!-- END_' . $route['id'] . ' -->/is', $compareDocumentation, $lastDocWeGeneratedForThisRoute) && $lastDocWeGeneratedForThisRoute[1] !== $existingRouteDoc[1]);
121
                        if ($routeDocumentationChanged === false || $this->forceIt) {
122
                            if ($routeDocumentationChanged) {
123
                                $this->output->warn('Discarded manual changes for route [' . implode(',', $route['methods']) . '] ' . $route['uri']);
124
                            }
125
                        } else {
126
                            $this->output->warn('Skipping modified route [' . implode(',', $route['methods']) . '] ' . $route['uri']);
127
                            $route['modified_output'] = $existingRouteDoc[0];
128
                        }
129
                    }
130
131
                    return $route;
132
                });
133
            });
134
        }
135
136
        $prependFileContents = $this->getMarkdownToPrepend();
137
        $appendFileContents = $this->getMarkdownToAppend();
138
139
        $markdown = view('apidoc::documentarian')
140
            ->with('writeCompareFile', false)
141
            ->with('frontmatter', $frontmatter)
142
            ->with('infoText', $infoText)
143
            ->with('prependMd', $prependFileContents)
144
            ->with('appendMd', $appendFileContents)
145
            ->with('outputPath', $this->config->get('output'))
146
            ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection)
147
            ->with('parsedRoutes', $parsedRouteOutput);
148
149
        $this->output->info('Writing index.md and source files to: ' . $this->sourceOutputPath);
150
151
        if (! is_dir($this->sourceOutputPath)) {
152
            $documentarian = new Documentarian();
153
            $documentarian->create($this->sourceOutputPath);
154
        }
155
156
        // Write output file
157
        file_put_contents($targetFile, $markdown);
158
159
        // Write comparable markdown file
160
        $compareMarkdown = view('apidoc::documentarian')
161
            ->with('writeCompareFile', true)
162
            ->with('frontmatter', $frontmatter)
163
            ->with('infoText', $infoText)
164
            ->with('prependMd', $prependFileContents)
165
            ->with('appendMd', $appendFileContents)
166
            ->with('outputPath', $this->config->get('output'))
167
            ->with('showPostmanCollectionButton', $this->shouldGeneratePostmanCollection)
168
            ->with('parsedRoutes', $parsedRouteOutput);
169
170
        file_put_contents($compareFile, $compareMarkdown);
171
172
        $this->output->info('Wrote index.md and source files to: ' . $this->sourceOutputPath);
173
    }
174
175
    public function generateMarkdownOutputForEachRoute(Collection $parsedRoutes, array $settings): Collection
176
    {
177
        $parsedRouteOutput = $parsedRoutes->map(function (Collection $routeGroup) use ($settings) {
178
            return $routeGroup->map(function (array $route) use ($settings) {
179
                if (count($route['cleanBodyParameters']) && ! isset($route['headers']['Content-Type'])) {
180
                    // Set content type if the user forgot to set it
181
                    $route['headers']['Content-Type'] = 'application/json';
182
                }
183
184
                $hasRequestOptions = ! empty($route['headers']) || ! empty($route['cleanQueryParameters']) || ! empty($route['cleanBodyParameters']);
185
                $route['output'] = (string) view('apidoc::partials.route')
186
                    ->with('hasRequestOptions', $hasRequestOptions)
187
                    ->with('route', $route)
188
                    ->with('settings', $settings)
189
                    ->with('baseUrl', $this->baseUrl)
190
                    ->render();
191
192
                return $route;
193
            });
194
        });
195
196
        return $parsedRouteOutput;
197
    }
198
199
    protected function writePostmanCollection(Collection $parsedRoutes): void
200
    {
201
        if ($this->shouldGeneratePostmanCollection) {
202
            $this->output->info('Generating Postman collection');
203
204
            $collection = $this->generatePostmanCollection($parsedRoutes);
205
            if ($this->isStatic) {
206
                $collectionPath = "{$this->outputPath}/collection.json";
207
                file_put_contents($collectionPath, $collection);
208
            } else {
209
                $storageInstance = Storage::disk($this->config->get('storage'));
210
                $storageInstance->put('apidoc/collection.json', $collection, 'public');
211
                if ($this->config->get('storage') == 'local') {
212
                    $collectionPath = 'storage/app/apidoc/collection.json';
213
                } else {
214
                    $collectionPath = $storageInstance->url('collection.json');
215
                }
216
            }
217
218
            $this->output->info("Wrote Postman collection to: {$collectionPath}");
219
        }
220
    }
221
222
    /**
223
     * Generate Postman collection JSON file.
224
     *
225
     * @param Collection $routes
226
     *
227
     * @return string
228
     */
229
    public function generatePostmanCollection(Collection $routes)
230
    {
231
        /** @var PostmanCollectionWriter $writer */
232
        $writer = app()->makeWith(
233
            PostmanCollectionWriter::class,
234
            ['routeGroups' => $routes, 'baseUrl' => $this->baseUrl]
235
        );
236
237
        return $writer->getCollection();
238
    }
239
240
    protected function getMarkdownToPrepend(): string
241
    {
242
        $prependFile = $this->sourceOutputPath . '/source/prepend.md';
243
        $prependFileContents = file_exists($prependFile)
244
            ? file_get_contents($prependFile) . "\n" : '';
245
246
        return $prependFileContents;
247
    }
248
249
    protected function getMarkdownToAppend(): string
250
    {
251
        $appendFile = $this->sourceOutputPath . '/source/append.md';
252
        $appendFileContents = file_exists($appendFile)
253
            ? "\n" . file_get_contents($appendFile) : '';
254
255
        return $appendFileContents;
256
    }
257
258
    protected function copyAssetsFromSourceFolderToPublicFolder(): void
259
    {
260
        $publicPath = $this->config->get('output_folder') ?? 'public/docs';
261
        if (! is_dir($publicPath)) {
262
            mkdir($publicPath, 0777, true);
263
            mkdir("{$publicPath}/css");
264
            mkdir("{$publicPath}/js");
265
        }
266
        copy("{$this->sourceOutputPath}/js/all.js", "{$publicPath}/js/all.js");
267
        rcopy("{$this->sourceOutputPath}/images", "{$publicPath}/images");
268
        rcopy("{$this->sourceOutputPath}/css", "{$publicPath}/css");
269
270
        if ($logo = $this->config->get('logo')) {
271
            copy($logo, "{$publicPath}/images/logo.png");
272
        }
273
    }
274
275
    protected function moveOutputFromSourceFolderToTargetFolder(): void
276
    {
277
        if ($this->isStatic) {
278
            // Move output (index.html, css/style.css and js/all.js) to public/docs
279
            rename("{$this->sourceOutputPath}/index.html", "{$this->outputPath}/index.html");
280
        } else {
281
            // Move output to resources/views
282
            if (! is_dir($this->outputPath)) {
283
                mkdir($this->outputPath);
284
            }
285
            rename("{$this->sourceOutputPath}/index.html", "$this->outputPath/index.blade.php");
286
            $contents = file_get_contents("$this->outputPath/index.blade.php");
287
            //
288
            $contents = str_replace('href="css/style.css"', 'href="{{ asset(\'/docs/css/style.css\') }}"', $contents);
289
            $contents = str_replace('src="js/all.js"', 'src="{{ asset(\'/docs/js/all.js\') }}"', $contents);
290
            $contents = str_replace('src="images/', 'src="/docs/images/', $contents);
291
            $contents = preg_replace('#href="https?://.+?/docs/collection.json"#', 'href="{{ route("apidoc.json") }}"', $contents);
292
            file_put_contents("$this->outputPath/index.blade.php", $contents);
293
        }
294
    }
295
296
    public function writeHtmlDocs(): void
297
    {
298
        $this->output->info('Generating API HTML code');
299
300
        $this->documentarian->generate($this->sourceOutputPath);
301
302
        // Move assets to public folder
303
        $this->copyAssetsFromSourceFolderToPublicFolder();
304
305
        $this->moveOutputFromSourceFolderToTargetFolder();
306
307
        $this->output->info("Wrote HTML documentation to: {$this->outputPath}");
308
    }
309
}
310