Completed
Pull Request — master (#761)
by Mike P.
01:31
created

Writer::getSourceOutputPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
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 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->getSourceOutputPath() . '/source/index.md';
95
        $compareFile = $this->getSourceOutputPath() . '/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->getSourceOutputPath());
150
151
        if (! is_dir($this->getSourceOutputPath())) {
152
            $documentarian = new Documentarian();
153
            $documentarian->create($this->getSourceOutputPath());
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->getSourceOutputPath());
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->getOutputPath()}/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->getSourceOutputPath() . '/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->getSourceOutputPath() . '/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 = base_path($this->config->get('output_folder') ?? base_path('public/docs'));
261
        if (! is_dir($publicPath)) {
262
            mkdir($publicPath, 0777, true);
263
        }
264
265
        if (! is_dir("{$publicPath}/css")) {
266
            mkdir("{$publicPath}/css", 0777, true);
267
        }
268
269
        if (! is_dir("{$publicPath}/js")) {
270
            mkdir("{$publicPath}/js", 0777, true);
271
        }
272
273
        copy("{$this->getSourceOutputPath()}/js/all.js", "{$publicPath}/js/all.js");
274
        rcopy("{$this->getSourceOutputPath()}/images", "{$publicPath}/images");
275
        rcopy("{$this->getSourceOutputPath()}/css", "{$publicPath}/css");
276
277
        if ($logo = $this->config->get('logo')) {
278
            copy($logo, "{$publicPath}/images/logo.png");
279
        }
280
    }
281
282
    protected function getSourceOutputPath(): string {
283
        return base_path($this->sourceOutputPath);
284
    }
285
286
    protected function moveOutputFromSourceFolderToTargetFolder(): void
287
    {
288
        if ($this->isStatic) {
289
            // Move output (index.html, css/style.css and js/all.js) to public/docs
290
            rename("{$this->getSourceOutputPath()}/index.html", "{$this->getOutputPath()}/index.html");
291
        } else {
292
            // Move output to resources/views
293
            if (! is_dir($this->getOutputPath())) {
294
                mkdir($this->getOutputPath());
295
            }
296
            rename("{$this->getSourceOutputPath()}/index.html", "$this->getOutputPath()/index.blade.php");
0 ignored issues
show
Bug introduced by
The property getOutputPath does not seem to exist. Did you mean output?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
297
            $contents = file_get_contents("$this->getOutputPath()/index.blade.php");
0 ignored issues
show
Bug introduced by
The property getOutputPath does not seem to exist. Did you mean output?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
298
            //
299
            $contents = str_replace('href="css/style.css"', 'href="{{ asset(\'/docs/css/style.css\') }}"', $contents);
300
            $contents = str_replace('src="js/all.js"', 'src="{{ asset(\'/docs/js/all.js\') }}"', $contents);
301
            $contents = str_replace('src="images/', 'src="/docs/images/', $contents);
302
            $contents = preg_replace('#href="https?://.+?/docs/collection.json"#', 'href="{{ route("apidoc.json") }}"', $contents);
303
            file_put_contents("$this->getOutputPath()/index.blade.php", $contents);
0 ignored issues
show
Bug introduced by
The property getOutputPath does not seem to exist. Did you mean output?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
304
        }
305
    }
306
307
    public function writeHtmlDocs(): void
308
    {
309
        $this->output->info('Generating API HTML code');
310
311
        $this->documentarian->generate($this->getSourceOutputPath());
312
313
        // Move assets to public folder
314
        $this->copyAssetsFromSourceFolderToPublicFolder();
315
316
        $this->moveOutputFromSourceFolderToTargetFolder();
317
318
        $this->output->info("Wrote HTML documentation to: {$this->getOutputPath()}");
319
    }
320
321
    protected function getOutputPath(): string {
322
        return base_path($this->outputPath);
323
    }
324
}
325