Subversion-Projekte lars-tiefland.laravel_shop

Revision

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