Writer.php 8.8 KB

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