Viewing file: DocsCommand.php (13.46 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
<?php
namespace 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; } }
|