Completed
Push — master ( 0ed20a...e8c824 )
by
unknown
10:05 queued 08:45
created

Writer::writeDocs()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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