Completed
Pull Request — master (#608)
by
unknown
12:02
created

Writer::generateMarkdownOutputForEachRoute()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
cc 3
nc 1
nop 2
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\Documentarian\Documentarian;
9
use Mpociot\ApiDoc\Tools\DocumentationConfig;
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
                $route['output'] = (string) view('apidoc::partials.route')
0 ignored issues
show
Bug introduced by
The method with does only exist in Illuminate\View\View, but not in Illuminate\Contracts\View\Factory.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
176
                    ->with('route', $route)
177
                    ->with('settings', $settings)
178
                    ->with('baseUrl', $this->baseUrl)
179
                    ->render();
180
181
                return $route;
182
            });
183
        });
184
185
        return $parsedRouteOutput;
186
    }
187
188
    protected function writePostmanCollection(Collection $parsedRoutes, string $outputPath, bool $isStatic): void
189
    {
190
        if ($this->shouldGeneratePostmanCollection) {
191
            $this->output->info('Generating Postman collection');
192
193
            $collection = $this->generatePostmanCollection($parsedRoutes);
194
            if ($isStatic) {
195
                $collectionPath = "{$outputPath}/collection.json";
196
                file_put_contents($collectionPath, $collection);
197
            } else {
198
                Storage::disk('local')->put('apidoc/collection.json', $collection);
199
                $collectionPath = 'storage/app/apidoc/collection.json';
200
            }
201
202
            $this->output->info("Wrote Postman collection to: {$collectionPath}");
203
        }
204
    }
205
206
    /**
207
     * Generate Postman collection JSON file.
208
     *
209
     * @param Collection $routes
210
     *
211
     * @return string
212
     */
213
    public function generatePostmanCollection(Collection $routes)
214
    {
215
        $writer = new PostmanCollectionWriter($routes, $this->baseUrl);
216
217
        return $writer->getCollection();
218
    }
219
220
    /**
221
     * @param string $sourceOutputPath
222
     *
223
     * @return string
224
     */
225
    protected function getMarkdownToPrepend(string $sourceOutputPath): string
226
    {
227
        $prependFile = $sourceOutputPath.'/source/prepend.md';
228
        $prependFileContents = file_exists($prependFile)
229
            ? file_get_contents($prependFile)."\n" : '';
230
231
        return $prependFileContents;
232
    }
233
234
    /**
235
     * @param string $sourceOutputPath
236
     *
237
     * @return string
238
     */
239
    protected function getMarkdownToAppend(string $sourceOutputPath): string
240
    {
241
        $appendFile = $sourceOutputPath.'/source/append.md';
242
        $appendFileContents = file_exists($appendFile)
243
            ? "\n".file_get_contents($appendFile) : '';
244
245
        return $appendFileContents;
246
    }
247
248
    protected function copyAssetsFromSourceFolderToPublicFolder(string $sourceOutputPath, bool $isStatic = true): void
249
    {
250
        $publicPath = 'public/docs';
251
        if (! is_dir($publicPath)) {
252
            mkdir($publicPath, 0777, true);
253
            mkdir("{$publicPath}/css");
254
            mkdir("{$publicPath}/js");
255
        }
256
        copy("{$sourceOutputPath}/js/all.js", "{$publicPath}/js/all.js");
257
        rcopy("{$sourceOutputPath}/images", "{$publicPath}/images");
258
        rcopy("{$sourceOutputPath}/css", "{$publicPath}/css");
259
260
        if ($logo = $this->config->get('logo')) {
261
            if ($isStatic) {
262
                copy($logo, "{$publicPath}/images/logo.png");
263
            }
264
        }
265
    }
266
267
    protected function moveOutputFromSourceFolderToTargetFolder(string $sourceOutputPath, string $outputPath, bool $isStatic): void
268
    {
269
        if ($isStatic) {
270
            // Move output (index.html, css/style.css and js/all.js) to public/docs
271
            rename("{$sourceOutputPath}/index.html", "{$outputPath}/index.html");
272
        } else {
273
            // Move output to resources/views
274
            if (! is_dir($outputPath)) {
275
                mkdir($outputPath);
276
            }
277
            rename("{$sourceOutputPath}/index.html", "$outputPath/index.blade.php");
278
            $contents = file_get_contents("$outputPath/index.blade.php");
279
            //
280
            $contents = str_replace('href="css/style.css"', 'href="/docs/css/style.css"', $contents);
281
            $contents = str_replace('src="js/all.js"', 'src="/docs/js/all.js"', $contents);
282
            $contents = str_replace('src="images/', 'src="/docs/images/', $contents);
283
            $contents = preg_replace('#href="http://.+?/docs/collection.json"#', 'href="{{ route("apidoc", ["format" => ".json"]) }}"', $contents);
284
            file_put_contents("$outputPath/index.blade.php", $contents);
285
        }
286
    }
287
288
    /**
289
     * @param string $sourceOutputPath
290
     * @param string $outputPath
291
     * @param bool $isStatic
292
     */
293
    protected function writeHtmlDocs(string $sourceOutputPath, string $outputPath, bool $isStatic): void
294
    {
295
        $this->output->info('Generating API HTML code');
296
297
        $this->documentarian->generate($sourceOutputPath);
298
299
        // Move assets to public folder
300
        $this->copyAssetsFromSourceFolderToPublicFolder($sourceOutputPath, $isStatic);
301
302
        $this->moveOutputFromSourceFolderToTargetFolder($sourceOutputPath, $outputPath, $isStatic);
303
304
        $this->output->info("Wrote HTML documentation to: {$outputPath}");
305
    }
306
}
307