Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 150 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php
2
 
3
namespace Illuminate\Foundation\Console;
4
 
5
use Carbon\CarbonInterval;
6
use Illuminate\Console\Command;
7
use Illuminate\Contracts\Cache\Repository as Cache;
8
use Illuminate\Http\Client\Factory as Http;
9
use Illuminate\Support\Arr;
10
use Illuminate\Support\Collection;
11
use Illuminate\Support\Env;
12
use Illuminate\Support\Str;
13
use Symfony\Component\Console\Attribute\AsCommand;
14
use Symfony\Component\Process\Exception\ProcessFailedException;
15
use Symfony\Component\Process\ExecutableFinder;
16
use Symfony\Component\Process\Process;
17
use Throwable;
18
 
19
#[AsCommand(name: 'docs')]
20
class DocsCommand extends Command
21
{
22
    /**
23
     * The name and signature of the console command.
24
     *
25
     * @var string
26
     */
27
    protected $signature = 'docs {page? : The documentation page to open} {section? : The section of the page to open}';
28
 
29
    /**
30
     * The name of the console command.
31
     *
32
     * This name is used to identify the command during lazy loading.
33
     *
34
     * @var string|null
35
     *
36
     * @deprecated
37
     */
38
    protected static $defaultName = 'docs';
39
 
40
    /**
41
     * The console command description.
42
     *
43
     * @var string
44
     */
45
    protected $description = 'Access the Laravel documentation';
46
 
47
    /**
48
     * The console command help text.
49
     *
50
     * @var string
51
     */
52
    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</>';
53
 
54
    /**
55
     * The HTTP client instance.
56
     *
57
     * @var \Illuminate\Http\Client\Factory
58
     */
59
    protected $http;
60
 
61
    /**
62
     * The cache repository implementation.
63
     *
64
     * @var \Illuminate\Contracts\Cache\Repository
65
     */
66
    protected $cache;
67
 
68
    /**
69
     * The custom URL opener.
70
     *
71
     * @var callable|null
72
     */
73
    protected $urlOpener;
74
 
75
    /**
76
     * The custom documentation version to open.
77
     *
78
     * @var string|null
79
     */
80
    protected $version;
81
 
82
    /**
83
     * The operating system family.
84
     *
85
     * @var string
86
     */
87
    protected $systemOsFamily = PHP_OS_FAMILY;
88
 
89
    /**
90
     * Create a new command instance.
91
     *
92
     * @param  \Illuminate\Http\Client\Factory  $http
93
     * @param  \Illuminate\Contracts\Cache\Repository  $cache
94
     * @return void
95
     */
96
    public function __construct(Http $http, Cache $cache)
97
    {
98
        parent::__construct();
99
 
100
        $this->http = $http;
101
        $this->cache = $cache;
102
    }
103
 
104
    /**
105
     * Configure the current command.
106
     *
107
     * @return void
108
     */
109
    protected function configure()
110
    {
111
        parent::configure();
112
 
113
        if ($this->isSearching()) {
114
            $this->ignoreValidationErrors();
115
        }
116
    }
117
 
118
    /**
119
     * Execute the console command.
120
     *
121
     * @return int
122
     */
123
    public function handle()
124
    {
125
        try {
126
            $this->openUrl();
127
        } catch (ProcessFailedException $e) {
128
            if ($e->getProcess()->getExitCodeText() === 'Interrupt') {
129
                return $e->getProcess()->getExitCode();
130
            }
131
 
132
            throw $e;
133
        }
134
 
135
        $this->refreshDocs();
136
 
137
        return Command::SUCCESS;
138
    }
139
 
140
    /**
141
     * Open the documentation URL.
142
     *
143
     * @return void
144
     */
145
    protected function openUrl()
146
    {
147
        with($this->url(), function ($url) {
148
            $this->components->info("Opening the docs to: <fg=yellow>{$url}</>");
149
 
150
            $this->open($url);
151
        });
152
    }
153
 
154
    /**
155
     * The URL to the documentation page.
156
     *
157
     * @return string
158
     */
159
    protected function url()
160
    {
161
        if ($this->isSearching()) {
162
            return "https://laravel.com/docs/{$this->version()}?".Arr::query([
163
                'q' => $this->searchQuery(),
164
            ]);
165
        }
166
 
167
        return with($this->page(), function ($page) {
168
            return trim("https://laravel.com/docs/{$this->version()}/{$page}#{$this->section($page)}", '#/');
169
        });
170
    }
171
 
172
    /**
173
     * The page the user is opening.
174
     *
175
     * @return string
176
     */
177
    protected function page()
178
    {
179
        return with($this->resolvePage(), function ($page) {
180
            if ($page === null) {
181
                $this->components->warn('Unable to determine the page you are trying to visit.');
182
 
183
                return '/';
184
            }
185
 
186
            return $page;
187
        });
188
    }
189
 
190
    /**
191
     * Determine the page to open.
192
     *
193
     * @return ?string
194
     */
195
    protected function resolvePage()
196
    {
197
        if ($this->option('no-interaction') && $this->didNotRequestPage()) {
198
            return '/';
199
        }
200
 
201
        return $this->didNotRequestPage()
202
            ? $this->askForPage()
203
            : $this->guessPage();
204
    }
205
 
206
    /**
207
     * Determine if the user requested a specific page when calling the command.
208
     *
209
     * @return bool
210
     */
211
    protected function didNotRequestPage()
212
    {
213
        return $this->argument('page') === null;
214
    }
215
 
216
    /**
217
     * Ask the user which page they would like to open.
218
     *
219
     * @return ?string
220
     */
221
    protected function askForPage()
222
    {
223
        return $this->askForPageViaCustomStrategy() ?? $this->askForPageViaAutocomplete();
224
    }
225
 
226
    /**
227
     * Ask the user which page they would like to open via a custom strategy.
228
     *
229
     * @return ?string
230
     */
231
    protected function askForPageViaCustomStrategy()
232
    {
233
        try {
234
            $strategy = require Env::get('ARTISAN_DOCS_ASK_STRATEGY');
235
        } catch (Throwable $e) {
236
            return null;
237
        }
238
 
239
        if (! is_callable($strategy)) {
240
            return null;
241
        }
242
 
243
        return $strategy($this) ?? '/';
244
    }
245
 
246
    /**
247
     * Ask the user which page they would like to open using autocomplete.
248
     *
249
     * @return ?string
250
     */
251
    protected function askForPageViaAutocomplete()
252
    {
253
        $choice = $this->components->choice(
254
            'Which page would you like to open?',
255
            $this->pages()->mapWithKeys(fn ($option) => [
256
                Str::lower($option['title']) => $option['title'],
257
            ])->all(),
258
            'installation',
259
            3
260
        );
261
 
262
        return $this->pages()->filter(
263
            fn ($page) => $page['title'] === $choice || Str::lower($page['title']) === $choice
264
        )->keys()->first() ?: null;
265
    }
266
 
267
    /**
268
     * Guess the page the user is attempting to open.
269
     *
270
     * @return ?string
271
     */
272
    protected function guessPage()
273
    {
274
        return $this->pages()
275
            ->filter(fn ($page) => str_starts_with(
276
                Str::slug($page['title'], ' '),
277
                Str::slug($this->argument('page'), ' ')
278
            ))->keys()->first() ?? $this->pages()->map(fn ($page) => similar_text(
279
                Str::slug($page['title'], ' '),
280
                Str::slug($this->argument('page'), ' '),
281
            ))
282
            ->filter(fn ($score) => $score >= min(3, Str::length($this->argument('page'))))
283
            ->sortDesc()
284
            ->keys()
285
            ->sortByDesc(fn ($slug) => Str::contains(
286
                Str::slug($this->pages()[$slug]['title'], ' '),
287
                Str::slug($this->argument('page'), ' ')
288
            ) ? 1 : 0)
289
            ->first();
290
    }
291
 
292
    /**
293
     * The section the user specifically asked to open.
294
     *
295
     * @param  string  $page
296
     * @return ?string
297
     */
298
    protected function section($page)
299
    {
300
        return $this->didNotRequestSection()
301
            ? null
302
            : $this->guessSection($page);
303
    }
304
 
305
    /**
306
     * Determine if the user requested a specific section when calling the command.
307
     *
308
     * @return bool
309
     */
310
    protected function didNotRequestSection()
311
    {
312
        return $this->argument('section') === null;
313
    }
314
 
315
    /**
316
     * Guess the section the user is attempting to open.
317
     *
318
     * @param  string  $page
319
     * @return ?string
320
     */
321
    protected function guessSection($page)
322
    {
323
        return $this->sectionsFor($page)
324
            ->filter(fn ($section) => str_starts_with(
325
                Str::slug($section['title'], ' '),
326
                Str::slug($this->argument('section'), ' ')
327
            ))->keys()->first() ?? $this->sectionsFor($page)->map(fn ($section) => similar_text(
328
                Str::slug($section['title'], ' '),
329
                Str::slug($this->argument('section'), ' '),
330
            ))
331
            ->filter(fn ($score) => $score >= min(3, Str::length($this->argument('section'))))
332
            ->sortDesc()
333
            ->keys()
334
            ->sortByDesc(fn ($slug) => Str::contains(
335
                Str::slug($this->sectionsFor($page)[$slug]['title'], ' '),
336
                Str::slug($this->argument('section'), ' ')
337
            ) ? 1 : 0)
338
            ->first();
339
    }
340
 
341
    /**
342
     * Open the URL in the user's browser.
343
     *
344
     * @param  string  $url
345
     * @return void
346
     */
347
    protected function open($url)
348
    {
349
        ($this->urlOpener ?? function ($url) {
350
            if (Env::get('ARTISAN_DOCS_OPEN_STRATEGY')) {
351
                $this->openViaCustomStrategy($url);
352
            } elseif (in_array($this->systemOsFamily, ['Darwin', 'Windows', 'Linux'])) {
353
                $this->openViaBuiltInStrategy($url);
354
            } else {
355
                $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.');
356
            }
357
        })($url);
358
    }
359
 
360
    /**
361
     * Open the URL via a custom strategy.
362
     *
363
     * @param  string  $url
364
     * @return void
365
     */
366
    protected function openViaCustomStrategy($url)
367
    {
368
        try {
369
            $command = require Env::get('ARTISAN_DOCS_OPEN_STRATEGY');
370
        } catch (Throwable $e) {
371
            $command = null;
372
        }
373
 
374
        if (! is_callable($command)) {
375
            $this->components->warn('Unable to open the URL with your custom strategy. You will need to open it yourself.');
376
 
377
            return;
378
        }
379
 
380
        $command($url);
381
    }
382
 
383
    /**
384
     * Open the URL via the built in strategy.
385
     *
386
     * @param  string  $url
387
     * @return void
388
     */
389
    protected function openViaBuiltInStrategy($url)
390
    {
391
        if ($this->systemOsFamily === 'Windows') {
392
            $process = tap(Process::fromShellCommandline(escapeshellcmd("start {$url}")))->run();
393
 
394
            if (! $process->isSuccessful()) {
395
                throw new ProcessFailedException($process);
396
            }
397
 
398
            return;
399
        }
400
 
401
        $binary = Collection::make(match ($this->systemOsFamily) {
402
            'Darwin' => ['open'],
403
            'Linux' => ['xdg-open', 'wslview'],
404
        })->first(fn ($binary) => (new ExecutableFinder)->find($binary) !== null);
405
 
406
        if ($binary === null) {
407
            $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.');
408
 
409
            return;
410
        }
411
 
412
        $process = tap(Process::fromShellCommandline(escapeshellcmd("{$binary} {$url}")))->run();
413
 
414
        if (! $process->isSuccessful()) {
415
            throw new ProcessFailedException($process);
416
        }
417
    }
418
 
419
    /**
420
     * The available sections for the page.
421
     *
422
     * @param  string  $page
423
     * @return \Illuminate\Support\Collection
424
     */
425
    public function sectionsFor($page)
426
    {
427
        return new Collection($this->pages()[$page]['sections']);
428
    }
429
 
430
    /**
431
     * The pages available to open.
432
     *
433
     * @return \Illuminate\Support\Collection
434
     */
435
    public function pages()
436
    {
437
        return new Collection($this->docs()['pages']);
438
    }
439
 
440
    /**
441
     * Get the documentation index as a collection.
442
     *
443
     * @return \Illuminate\Support\Collection
444
     */
445
    public function docs()
446
    {
447
        return $this->cache->remember(
448
            "artisan.docs.{{$this->version()}}.index",
449
            CarbonInterval::months(2),
450
            fn () => $this->fetchDocs()->throw()->collect()
451
        );
452
    }
453
 
454
    /**
455
     * Refresh the cached copy of the documentation index.
456
     *
457
     * @return void
458
     */
459
    protected function refreshDocs()
460
    {
461
        with($this->fetchDocs(), function ($response) {
462
            if ($response->successful()) {
463
                $this->cache->put("artisan.docs.{{$this->version()}}.index", $response->collect(), CarbonInterval::months(2));
464
            }
465
        });
466
    }
467
 
468
    /**
469
     * Fetch the documentation index from the Laravel website.
470
     *
471
     * @return \Illuminate\Http\Client\Response
472
     */
473
    protected function fetchDocs()
474
    {
475
        return $this->http->get("https://laravel.com/docs/{$this->version()}/index.json");
476
    }
477
 
478
    /**
479
     * Determine the version of the docs to open.
480
     *
481
     * @return string
482
     */
483
    protected function version()
484
    {
485
        return Str::before(($this->version ?? $this->laravel->version()), '.').'.x';
486
    }
487
 
488
    /**
489
     * The search query the user provided.
490
     *
491
     * @return string
492
     */
493
    protected function searchQuery()
494
    {
495
        return Collection::make($_SERVER['argv'])->skip(3)->implode(' ');
496
    }
497
 
498
    /**
499
     * Determine if the command is intended to perform a search.
500
     *
501
     * @return bool
502
     */
503
    protected function isSearching()
504
    {
505
        return ($_SERVER['argv'][2] ?? null) === '--';
506
    }
507
 
508
    /**
509
     * Set the documentation version.
510
     *
511
     * @param  string  $version
512
     * @return $this
513
     */
514
    public function setVersion($version)
515
    {
516
        $this->version = $version;
517
 
518
        return $this;
519
    }
520
 
521
    /**
522
     * Set a custom URL opener.
523
     *
524
     * @param  callable|null  $opener
525
     * @return $this
526
     */
527
    public function setUrlOpener($opener)
528
    {
529
        $this->urlOpener = $opener;
530
 
531
        return $this;
532
    }
533
 
534
    /**
535
     * Set the system operating system family.
536
     *
537
     * @param  string  $family
538
     * @return $this
539
     */
540
    public function setSystemOsFamily($family)
541
    {
542
        $this->systemOsFamily = $family;
543
 
544
        return $this;
545
    }
546
}