Writer.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. <?php
  2. namespace Knuckles\Scribe\Writing;
  3. use Illuminate\Support\Facades\Storage;
  4. use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
  5. use Knuckles\Scribe\Tools\DocumentationConfig;
  6. use Knuckles\Scribe\Tools\Globals;
  7. use Knuckles\Scribe\Tools\Utils;
  8. use Symfony\Component\Yaml\Yaml;
  9. class Writer
  10. {
  11. private DocumentationConfig $config;
  12. private bool $isStatic;
  13. private string $markdownOutputPath = '.scribe';
  14. private string $staticTypeOutputPath;
  15. private string $laravelTypeOutputPath = 'resources/views/scribe';
  16. protected array $generatedFiles = [
  17. 'postman' => null,
  18. 'openapi' => null,
  19. 'html' => null,
  20. 'blade' => null,
  21. 'assets' => [
  22. 'js' => null,
  23. 'css' => null,
  24. 'images' => null,
  25. ]
  26. ];
  27. private string $laravelAssetsPath;
  28. public function __construct(DocumentationConfig $config = null)
  29. {
  30. // If no config is injected, pull from global. Makes testing easier.
  31. $this->config = $config ?: new DocumentationConfig(config('scribe'));
  32. $this->isStatic = $this->config->get('type') === 'static';
  33. $this->staticTypeOutputPath = rtrim($this->config->get('static.output_path', 'public/docs'), '/');
  34. $this->laravelAssetsPath = $this->config->get('laravel.assets_directory')
  35. ? '/' . $this->config->get('laravel.assets_directory')
  36. : '/vendor/scribe';
  37. }
  38. /**
  39. * @param array[] $groupedEndpoints
  40. */
  41. public function writeDocs(array $groupedEndpoints)
  42. {
  43. // The static assets (js/, css/, and images/) always go in public/docs/.
  44. // For 'static' docs, the output files (index.html, collection.json) go in public/docs/.
  45. // For 'laravel' docs, the output files (index.blade.php, collection.json)
  46. // go in resources/views/scribe/ and storage/app/scribe/ respectively.
  47. $this->writeHtmlDocs($groupedEndpoints);
  48. $this->writePostmanCollection($groupedEndpoints);
  49. $this->writeOpenAPISpec($groupedEndpoints);
  50. $this->runAfterGeneratingHook();
  51. }
  52. protected function writePostmanCollection(array $groups): void
  53. {
  54. if ($this->config->get('postman.enabled', true)) {
  55. c::info('Generating Postman collection');
  56. $collection = $this->generatePostmanCollection($groups);
  57. if ($this->isStatic) {
  58. $collectionPath = "{$this->staticTypeOutputPath}/collection.json";
  59. file_put_contents($collectionPath, $collection);
  60. } else {
  61. Storage::disk('local')->put('scribe/collection.json', $collection);
  62. $collectionPath = 'storage/app/scribe/collection.json';
  63. }
  64. c::success("Wrote Postman collection to: {$collectionPath}");
  65. $this->generatedFiles['postman'] = realpath($collectionPath);
  66. }
  67. }
  68. protected function writeOpenAPISpec(array $parsedRoutes): void
  69. {
  70. if ($this->config->get('openapi.enabled', false)) {
  71. c::info('Generating OpenAPI specification');
  72. $spec = $this->generateOpenAPISpec($parsedRoutes);
  73. if ($this->isStatic) {
  74. $specPath = "{$this->staticTypeOutputPath}/openapi.yaml";
  75. file_put_contents($specPath, $spec);
  76. } else {
  77. Storage::disk('local')->put('scribe/openapi.yaml', $spec);
  78. $specPath = 'storage/app/scribe/openapi.yaml';
  79. }
  80. c::success("Wrote OpenAPI specification to: {$specPath}");
  81. $this->generatedFiles['openapi'] = realpath($specPath);
  82. }
  83. }
  84. /**
  85. * Generate Postman collection JSON file.
  86. *
  87. * @param array[] $groupedEndpoints
  88. *
  89. * @return string
  90. */
  91. public function generatePostmanCollection(array $groupedEndpoints): string
  92. {
  93. /** @var PostmanCollectionWriter $writer */
  94. $writer = app()->makeWith(PostmanCollectionWriter::class, ['config' => $this->config]);
  95. $collection = $writer->generatePostmanCollection($groupedEndpoints);
  96. $overrides = $this->config->get('postman.overrides', []);
  97. if (count($overrides)) {
  98. foreach ($overrides as $key => $value) {
  99. data_set($collection, $key, $value);
  100. }
  101. }
  102. return json_encode($collection, JSON_PRETTY_PRINT);
  103. }
  104. /**
  105. * @param array[] $groupedEndpoints
  106. *
  107. * @return string
  108. */
  109. public function generateOpenAPISpec(array $groupedEndpoints): string
  110. {
  111. /** @var OpenAPISpecWriter $writer */
  112. $writer = app()->makeWith(OpenAPISpecWriter::class, ['config' => $this->config]);
  113. $spec = $writer->generateSpecContent($groupedEndpoints);
  114. $overrides = $this->config->get('openapi.overrides', []);
  115. if (count($overrides)) {
  116. foreach ($overrides as $key => $value) {
  117. data_set($spec, $key, $value);
  118. }
  119. }
  120. return Yaml::dump($spec, 20, 2, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE | Yaml::DUMP_OBJECT_AS_MAP);
  121. }
  122. protected function performFinalTasksForLaravelType(): void
  123. {
  124. if (!is_dir($this->laravelTypeOutputPath)) {
  125. mkdir($this->laravelTypeOutputPath, 0777, true);
  126. }
  127. $publicDirectory = app()->get('path.public');
  128. if (!is_dir($publicDirectory . $this->laravelAssetsPath)) {
  129. mkdir($publicDirectory . $this->laravelAssetsPath, 0777, true);
  130. }
  131. // Transform output HTML to a Blade view
  132. rename("{$this->staticTypeOutputPath}/index.html", "$this->laravelTypeOutputPath/index.blade.php");
  133. // Move assets from public/docs to public/vendor/scribe or config('laravel.assets_directory')
  134. // We need to do this delete first, otherwise move won't work if folder exists
  135. Utils::deleteDirectoryAndContents($publicDirectory . $this->laravelAssetsPath);
  136. rename("{$this->staticTypeOutputPath}/", $publicDirectory . $this->laravelAssetsPath);
  137. $contents = file_get_contents("$this->laravelTypeOutputPath/index.blade.php");
  138. // Rewrite asset links to go through Laravel
  139. $contents = preg_replace('#href="\.\./docs/css/(.+?)"#', 'href="{{ asset("' . $this->laravelAssetsPath . '/css/$1") }}"', $contents);
  140. $contents = preg_replace('#src="\.\./docs/(js|images)/(.+?)"#', 'src="{{ asset("' . $this->laravelAssetsPath . '/$1/$2") }}"', $contents);
  141. $contents = str_replace('href="../docs/collection.json"', 'href="{{ route("scribe.postman") }}"', $contents);
  142. $contents = str_replace('href="../docs/openapi.yaml"', 'href="{{ route("scribe.openapi") }}"', $contents);
  143. file_put_contents("$this->laravelTypeOutputPath/index.blade.php", $contents);
  144. }
  145. public function writeHtmlDocs(array $groupedEndpoints): void
  146. {
  147. c::info('Writing HTML docs...');
  148. // Then we convert them to HTML, and throw in the endpoints as well.
  149. /** @var HtmlWriter $writer */
  150. $writer = app()->makeWith(HtmlWriter::class, ['config' => $this->config]);
  151. $writer->generate($groupedEndpoints, $this->markdownOutputPath, $this->staticTypeOutputPath);
  152. if (!$this->isStatic) {
  153. $this->performFinalTasksForLaravelType();
  154. }
  155. if ($this->isStatic) {
  156. $outputPath = rtrim($this->staticTypeOutputPath, '/') . '/';
  157. c::success("Wrote HTML docs to: $outputPath");
  158. $this->generatedFiles['html'] = realpath("{$outputPath}index.html");
  159. $assetsOutputPath = $outputPath;
  160. } else {
  161. $outputPath = rtrim($this->laravelTypeOutputPath, '/') . '/';
  162. c::success("Wrote Blade docs to: $outputPath");
  163. $this->generatedFiles['blade'] = realpath("{$outputPath}index.blade.php");
  164. $assetsOutputPath = app()->get('path.public').$this->laravelAssetsPath;
  165. c::success("Wrote Laravel assets to: " . realpath($assetsOutputPath));
  166. }
  167. $this->generatedFiles['assets']['js'] = realpath("{$assetsOutputPath}js");
  168. $this->generatedFiles['assets']['css'] = realpath("{$assetsOutputPath}css");
  169. $this->generatedFiles['assets']['images'] = realpath("{$assetsOutputPath}images");
  170. }
  171. protected function runAfterGeneratingHook()
  172. {
  173. if (is_callable(Globals::$__afterGenerating)) {
  174. c::info("Running `afterGenerating()` hook...");
  175. call_user_func_array(Globals::$__afterGenerating, [$this->generatedFiles]);
  176. }
  177. }
  178. }