`} "; protected $description = 'Generate API documentation from your Laravel/Dingo routes.'; protected DocumentationConfig $docConfig; protected bool $shouldExtract; protected bool $forcing; protected PathConfig $paths; public function handle(RouteMatcherInterface $routeMatcher, GroupedEndpointsFactory $groupedEndpointsFactory): void { $this->bootstrap(); if (!empty($this->docConfig->get("default_group"))) { $this->warn("It looks like you just upgraded to Scribe v4."); $this->warn("Please run the upgrade command first: `php artisan scribe:upgrade`."); exit(1); } // Extraction stage - extract endpoint info either from app or existing Camel files (previously extracted data) $groupedEndpointsInstance = $groupedEndpointsFactory->make($this, $routeMatcher, $this->paths); $extractedEndpoints = $groupedEndpointsInstance->get(); $userDefinedEndpoints = Camel::loadUserDefinedEndpoints(Camel::camelDir($this->paths)); $groupedEndpoints = $this->mergeUserDefinedEndpoints($extractedEndpoints, $userDefinedEndpoints); // Output stage $configFileOrder = $this->docConfig->get('groups.order', []); $groupedEndpoints = Camel::prepareGroupedEndpointsForOutput($groupedEndpoints, $configFileOrder); if (!count($userDefinedEndpoints)) { // Update the example custom file if there were no custom endpoints $this->writeExampleCustomEndpoint(); } /** @var Writer $writer */ $writer = app(Writer::class, ['config' => $this->docConfig, 'paths' => $this->paths]); $writer->writeDocs($groupedEndpoints); $this->upgradeConfigFileIfNeeded(); $this->sayGoodbye(errored: $groupedEndpointsInstance->hasEncounteredErrors()); } public function isForcing(): bool { return $this->forcing; } public function shouldExtract(): bool { return $this->shouldExtract; } public function getDocConfig(): DocumentationConfig { return $this->docConfig; } protected function runBootstrapHook() { if (is_callable(Globals::$__bootstrap)) { call_user_func_array(Globals::$__bootstrap, [$this]); } } public function bootstrap(): void { // The --verbose option is included with all Artisan commands. Globals::$shouldBeVerbose = $this->option('verbose'); c::bootstrapOutput($this->output); $configName = $this->option('config'); if (!config($configName)) { throw new \InvalidArgumentException("The specified config (config/{$configName}.php) doesn't exist."); } $this->paths = new PathConfig($configName); if ($this->hasOption('scribe-dir') && !empty($this->option('scribe-dir'))) { $this->paths = new PathConfig( $configName, scribeDir: $this->option('scribe-dir') ); } $this->docConfig = new DocumentationConfig(config($this->paths->configName)); // Force root URL so it works in Postman collection $baseUrl = $this->docConfig->get('base_url') ?? config('app.url'); URL::forceRootUrl($baseUrl); $this->forcing = $this->option('force'); $this->shouldExtract = !$this->option('no-extraction'); if ($this->forcing && !$this->shouldExtract) { throw new \InvalidArgumentException("Can't use --force and --no-extraction together."); } $this->runBootstrapHook(); } protected function mergeUserDefinedEndpoints(array $groupedEndpoints, array $userDefinedEndpoints): array { foreach ($userDefinedEndpoints as $endpoint) { $indexOfGroupWhereThisEndpointShouldBeAdded = Arr::first(array_keys($groupedEndpoints), function ($key) use ($groupedEndpoints, $endpoint) { $group = $groupedEndpoints[$key]; return $group['name'] === ($endpoint['metadata']['groupName'] ?? $this->docConfig->get('groups.default', '')); }); if ($indexOfGroupWhereThisEndpointShouldBeAdded !== null) { $groupedEndpoints[$indexOfGroupWhereThisEndpointShouldBeAdded]['endpoints'][] = $endpoint; } else { $newGroup = [ 'name' => $endpoint['metadata']['groupName'] ?? $this->docConfig->get('groups.default', ''), 'description' => $endpoint['metadata']['groupDescription'] ?? null, 'endpoints' => [$endpoint], ]; $groupedEndpoints[$newGroup['name']] = $newGroup; } } return $groupedEndpoints; } protected function writeExampleCustomEndpoint(): void { // We add an example to guide users in case they need to add a custom endpoint. copy(__DIR__ . '/../../resources/example_custom_endpoint.yaml', Camel::camelDir($this->paths) . '/custom.0.yaml'); } protected function upgradeConfigFileIfNeeded(): void { if ($this->option('no-upgrade-check')) return; $this->info("Checking for any pending upgrades to your config file..."); try { if (!$this->laravel['files']->exists( $this->laravel->configPath($this->paths->configFileName()) ) ) { $this->info("No config file to upgrade."); return; } $upgrader = Upgrader::ofConfigFile( userOldConfigRelativePath: "config/{$this->paths->configFileName()}", sampleNewConfigAbsolutePath: __DIR__ . '/../../config/scribe.php' ) ->dontTouch( 'routes', 'example_languages', 'database_connections_to_transact', 'strategies', 'laravel.middleware', 'postman.overrides', 'openapi.overrides', 'groups', 'examples.models_source', 'external.html_attributes' ); $changes = $upgrader->dryRun(); if (!empty($changes)) { $this->newLine(); $this->warn("You're using an updated version of Scribe, which added new items to the config file."); $this->info("Here are the changes:"); foreach ($changes as $change) { $this->info($change["description"]); } if (!$this->input->isInteractive()) { $this->info("Run `php artisan scribe:upgrade` from an interactive terminal to update your config file automatically."); $this->info(sprintf("Or see the full changelog at: https://github.com/knuckleswtf/scribe/blob/%s/CHANGELOG.md,", Scribe::VERSION)); return; } if ($this->confirm("Let's help you update your config file. Accept changes?")) { $upgrader->upgrade(); $this->info(sprintf("✔ Updated. See the full changelog: https://github.com/knuckleswtf/scribe/blob/%s/CHANGELOG.md", Scribe::VERSION)); } } } catch (\Throwable $e) { $this->warn("Check failed with error:"); e::dumpExceptionIfVerbose($e); $this->warn("This did not affect your docs. Please report this issue in the project repo: https://github.com/knuckleswtf/scribe"); } } protected function sayGoodbye(bool $errored = false): void { $message = 'All done. '; if ($this->docConfig->outputRoutedThroughLaravel()) { if ($this->docConfig->get('laravel.add_routes')) { $message .= 'Visit your docs at ' . url($this->docConfig->get('laravel.docs_url')); } } else if (Str::endsWith(base_path('public'), 'public') && Str::startsWith($this->docConfig->get('static.output_path'), 'public/')) { $message = 'Visit your docs at ' . url(str_replace('public/', '', $this->docConfig->get('static.output_path'))); } $this->newLine(); c::success($message); if ($errored) { c::warn('Generated docs, but encountered some errors while processing routes.'); c::warn('Check the output above for details.'); if (empty($_SERVER["SCRIBE_TESTS"])) { exit(2); } } } }