Completed
Pull Request — master (#761)
by Mike P.
16:28
created

Writer::getSourceOutputPath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
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')
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...
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
    {
284
        return base_path($this->sourceOutputPath);
285
    }
286
287
    protected function moveOutputFromSourceFolderToTargetFolder(): void
288
    {
289
        $outputPath = $this->getOutputPath();
290
        if ($this->isStatic) {
291
            // Move output (index.html, css/style.css and js/all.js) to public/docs
292
            rename($this->getSourceOutputPath()."/index.html", "{$outputPath}/index.html");
293
        } else {
294
            // Move output to resources/views
295
            if (! is_dir($outputPath)) {
296
                mkdir($outputPath);
297
            }
298
            rename($this->getSourceOutputPath()."/index.html", "$outputPath/index.blade.php");
299
            $contents = file_get_contents("$outputPath/index.blade.php");
300
            //
301
            $contents = str_replace('href="css/style.css"', 'href="{{ asset(\'/docs/css/style.css\') }}"', $contents);
302
            $contents = str_replace('src="js/all.js"', 'src="{{ asset(\'/docs/js/all.js\') }}"', $contents);
303
            $contents = str_replace('src="images/', 'src="/docs/images/', $contents);
304
            $contents = preg_replace('#href="https?://.+?/docs/collection.json"#', 'href="{{ route("apidoc.json") }}"', $contents);
305
            file_put_contents("$outputPath/index.blade.php", $contents);
306
        }
307
    }
308
309
    public function writeHtmlDocs(): void
310
    {
311
        $this->output->info('Generating API HTML code');
312
313
        $this->documentarian->generate($this->getSourceOutputPath());
314
315
        // Move assets to public folder
316
        $this->copyAssetsFromSourceFolderToPublicFolder();
317
318
        $this->moveOutputFromSourceFolderToTargetFolder();
319
320
        $this->output->info("Wrote HTML documentation to: ".$this->getOutputPath());
321
    }
322
323
    protected function getOutputPath(): string
324
    {
325
        return base_path($this->outputPath);
326
    }
327
}
328