Revision 150 | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed
<?phpnamespace Illuminate\Foundation\Console;use Carbon\CarbonInterval;use Illuminate\Console\Command;use Illuminate\Contracts\Cache\Repository as Cache;use Illuminate\Http\Client\Factory as Http;use Illuminate\Support\Arr;use Illuminate\Support\Collection;use Illuminate\Support\Env;use Illuminate\Support\Str;use Symfony\Component\Console\Attribute\AsCommand;use Symfony\Component\Process\Exception\ProcessFailedException;use Symfony\Component\Process\ExecutableFinder;use Symfony\Component\Process\Process;use Throwable;#[AsCommand(name: 'docs')]class DocsCommand extends Command{/*** The name and signature of the console command.** @var string*/protected $signature = 'docs {page? : The documentation page to open} {section? : The section of the page to open}';/*** The name of the console command.** This name is used to identify the command during lazy loading.** @var string|null** @deprecated*/protected static $defaultName = 'docs';/*** The console command description.** @var string*/protected $description = 'Access the Laravel documentation';/*** The console command help text.** @var string*/protected $help = 'If you would like to perform a content search against the documention, you may call: <fg=green>php artisan docs -- </><fg=green;options=bold;>search query here</>';/*** The HTTP client instance.** @var \Illuminate\Http\Client\Factory*/protected $http;/*** The cache repository implementation.** @var \Illuminate\Contracts\Cache\Repository*/protected $cache;/*** The custom URL opener.** @var callable|null*/protected $urlOpener;/*** The custom documentation version to open.** @var string|null*/protected $version;/*** The operating system family.** @var string*/protected $systemOsFamily = PHP_OS_FAMILY;/*** Configure the current command.** @return void*/protected function configure(){parent::configure();if ($this->isSearching()) {$this->ignoreValidationErrors();}}/*** Execute the console command.** @param \Illuminate\Http\Client\Factory $http* @param \Illuminate\Contracts\Cache\Repository $cache* @return int*/public function handle(Http $http, Cache $cache){$this->http = $http;$this->cache = $cache;try {$this->openUrl();} catch (ProcessFailedException $e) {if ($e->getProcess()->getExitCodeText() === 'Interrupt') {return $e->getProcess()->getExitCode();}throw $e;}$this->refreshDocs();return Command::SUCCESS;}/*** Open the documentation URL.** @return void*/protected function openUrl(){with($this->url(), function ($url) {$this->components->info("Opening the docs to: <fg=yellow>{$url}</>");$this->open($url);});}/*** The URL to the documentation page.** @return string*/protected function url(){if ($this->isSearching()) {return "https://laravel.com/docs/{$this->version()}?".Arr::query(['q' => $this->searchQuery(),]);}return with($this->page(), function ($page) {return trim("https://laravel.com/docs/{$this->version()}/{$page}#{$this->section($page)}", '#/');});}/*** The page the user is opening.** @return string*/protected function page(){return with($this->resolvePage(), function ($page) {if ($page === null) {$this->components->warn('Unable to determine the page you are trying to visit.');return '/';}return $page;});}/*** Determine the page to open.** @return string|null*/protected function resolvePage(){if ($this->option('no-interaction') && $this->didNotRequestPage()) {return '/';}return $this->didNotRequestPage()? $this->askForPage(): $this->guessPage();}/*** Determine if the user requested a specific page when calling the command.** @return bool*/protected function didNotRequestPage(){return $this->argument('page') === null;}/*** Ask the user which page they would like to open.** @return string|null*/protected function askForPage(){return $this->askForPageViaCustomStrategy() ?? $this->askForPageViaAutocomplete();}/*** Ask the user which page they would like to open via a custom strategy.** @return string|null*/protected function askForPageViaCustomStrategy(){try {$strategy = require Env::get('ARTISAN_DOCS_ASK_STRATEGY');} catch (Throwable $e) {return null;}if (! is_callable($strategy)) {return null;}return $strategy($this) ?? '/';}/*** Ask the user which page they would like to open using autocomplete.** @return string|null*/protected function askForPageViaAutocomplete(){$choice = $this->components->choice('Which page would you like to open?',$this->pages()->mapWithKeys(fn ($option) => [Str::lower($option['title']) => $option['title'],])->all(),'installation',3);return $this->pages()->filter(fn ($page) => $page['title'] === $choice || Str::lower($page['title']) === $choice)->keys()->first() ?: null;}/*** Guess the page the user is attempting to open.** @return string|null*/protected function guessPage(){return $this->pages()->filter(fn ($page) => str_starts_with(Str::slug($page['title'], ' '),Str::slug($this->argument('page'), ' ')))->keys()->first() ?? $this->pages()->map(fn ($page) => similar_text(Str::slug($page['title'], ' '),Str::slug($this->argument('page'), ' '),))->filter(fn ($score) => $score >= min(3, Str::length($this->argument('page'))))->sortDesc()->keys()->sortByDesc(fn ($slug) => Str::contains(Str::slug($this->pages()[$slug]['title'], ' '),Str::slug($this->argument('page'), ' ')) ? 1 : 0)->first();}/*** The section the user specifically asked to open.** @param string $page* @return string|null*/protected function section($page){return $this->didNotRequestSection()? null: $this->guessSection($page);}/*** Determine if the user requested a specific section when calling the command.** @return bool*/protected function didNotRequestSection(){return $this->argument('section') === null;}/*** Guess the section the user is attempting to open.** @param string $page* @return string|null*/protected function guessSection($page){return $this->sectionsFor($page)->filter(fn ($section) => str_starts_with(Str::slug($section['title'], ' '),Str::slug($this->argument('section'), ' ')))->keys()->first() ?? $this->sectionsFor($page)->map(fn ($section) => similar_text(Str::slug($section['title'], ' '),Str::slug($this->argument('section'), ' '),))->filter(fn ($score) => $score >= min(3, Str::length($this->argument('section'))))->sortDesc()->keys()->sortByDesc(fn ($slug) => Str::contains(Str::slug($this->sectionsFor($page)[$slug]['title'], ' '),Str::slug($this->argument('section'), ' ')) ? 1 : 0)->first();}/*** Open the URL in the user's browser.** @param string $url* @return void*/protected function open($url){($this->urlOpener ?? function ($url) {if (Env::get('ARTISAN_DOCS_OPEN_STRATEGY')) {$this->openViaCustomStrategy($url);} elseif (in_array($this->systemOsFamily, ['Darwin', 'Windows', 'Linux'])) {$this->openViaBuiltInStrategy($url);} else {$this->components->warn('Unable to open the URL on your system. You will need to open it yourself or create a custom opener for your system.');}})($url);}/*** Open the URL via a custom strategy.** @param string $url* @return void*/protected function openViaCustomStrategy($url){try {$command = require Env::get('ARTISAN_DOCS_OPEN_STRATEGY');} catch (Throwable $e) {$command = null;}if (! is_callable($command)) {$this->components->warn('Unable to open the URL with your custom strategy. You will need to open it yourself.');return;}$command($url);}/*** Open the URL via the built in strategy.** @param string $url* @return void*/protected function openViaBuiltInStrategy($url){if ($this->systemOsFamily === 'Windows') {$process = tap(Process::fromShellCommandline(escapeshellcmd("start {$url}")))->run();if (! $process->isSuccessful()) {throw new ProcessFailedException($process);}return;}$binary = Collection::make(match ($this->systemOsFamily) {'Darwin' => ['open'],'Linux' => ['xdg-open', 'wslview'],})->first(fn ($binary) => (new ExecutableFinder)->find($binary) !== null);if ($binary === null) {$this->components->warn('Unable to open the URL on your system. You will need to open it yourself or create a custom opener for your system.');return;}$process = tap(Process::fromShellCommandline(escapeshellcmd("{$binary} {$url}")))->run();if (! $process->isSuccessful()) {throw new ProcessFailedException($process);}}/*** The available sections for the page.** @param string $page* @return \Illuminate\Support\Collection*/public function sectionsFor($page){return new Collection($this->pages()[$page]['sections']);}/*** The pages available to open.** @return \Illuminate\Support\Collection*/public function pages(){return new Collection($this->docs()['pages']);}/*** Get the documentation index as a collection.** @return \Illuminate\Support\Collection*/public function docs(){return $this->cache->remember("artisan.docs.{{$this->version()}}.index",CarbonInterval::months(2),fn () => $this->fetchDocs()->throw()->collect());}/*** Refresh the cached copy of the documentation index.** @return void*/protected function refreshDocs(){with($this->fetchDocs(), function ($response) {if ($response->successful()) {$this->cache->put("artisan.docs.{{$this->version()}}.index", $response->collect(), CarbonInterval::months(2));}});}/*** Fetch the documentation index from the Laravel website.** @return \Illuminate\Http\Client\Response*/protected function fetchDocs(){return $this->http->get("https://laravel.com/docs/{$this->version()}/index.json");}/*** Determine the version of the docs to open.** @return string*/protected function version(){return Str::before($this->version ?? $this->laravel->version(), '.').'.x';}/*** The search query the user provided.** @return string*/protected function searchQuery(){return Collection::make($_SERVER['argv'])->skip(3)->implode(' ');}/*** Determine if the command is intended to perform a search.** @return bool*/protected function isSearching(){return ($_SERVER['argv'][2] ?? null) === '--';}/*** Set the documentation version.** @param string $version* @return $this*/public function setVersion($version){$this->version = $version;return $this;}/*** Set a custom URL opener.** @param callable|null $opener* @return $this*/public function setUrlOpener($opener){$this->urlOpener = $opener;return $this;}/*** Set the system operating system family.** @param string $family* @return $this*/public function setSystemOsFamily($family){$this->systemOsFamily = $family;return $this;}}