Skip to content

Commit f6e9a77

Browse files
jessarchertaylorotwellStyleCIBotnunomaduro
authored
[10.x] Prompts (#46772)
* Configure prompt fallbacks * Update built-in prompts * Add placeholder values to make commands * Improve docs command when page not found * Formatting * Show all docs matches when nothing entered * Update option labels * Tweak vendor:publish compatibility with both prompts * Fallback to Symfony when running tests * formatting and fixes * print newline if using prompts on confirmable trait * Apply fixes from StyleCI * Improving spacing between components * Update OutputStyle.php * Adds missing dependency * Adjusts visibitily * Fixes return type * Add missing deprecation tag * Ensure newline state is preserved correctly * Formatting * Fix example for `make:command` * Prompt for missing args with the arg name when no description provided * Allow passing a closure when customizing missing argument prompts * Add missing secret component * Add `required` support to prompt fallbacks * Simply `select` fallback * Add fallback for search prompt * Update prompts dependency * Update prompts version * Fix installation of prompts in CI * Fix tests --------- Co-authored-by: Taylor Otwell <[email protected]> Co-authored-by: StyleCI Bot <[email protected]> Co-authored-by: Nuno Maduro <[email protected]>
1 parent c13e443 commit f6e9a77

35 files changed

+434
-142
lines changed

.github/workflows/databases.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ jobs:
2929
steps:
3030
- name: Checkout code
3131
uses: actions/checkout@v3
32+
with:
33+
fetch-depth: 0
3234

3335
- name: Setup PHP
3436
uses: shivammathur/setup-php@v2
@@ -72,6 +74,8 @@ jobs:
7274
steps:
7375
- name: Checkout code
7476
uses: actions/checkout@v3
77+
with:
78+
fetch-depth: 0
7579

7680
- name: Setup PHP
7781
uses: shivammathur/setup-php@v2
@@ -115,6 +119,8 @@ jobs:
115119
steps:
116120
- name: Checkout code
117121
uses: actions/checkout@v3
122+
with:
123+
fetch-depth: 0
118124

119125
- name: Setup PHP
120126
uses: shivammathur/setup-php@v2
@@ -159,6 +165,8 @@ jobs:
159165
steps:
160166
- name: Checkout code
161167
uses: actions/checkout@v3
168+
with:
169+
fetch-depth: 0
162170

163171
- name: Setup PHP
164172
uses: shivammathur/setup-php@v2
@@ -201,6 +209,8 @@ jobs:
201209
steps:
202210
- name: Checkout code
203211
uses: actions/checkout@v3
212+
with:
213+
fetch-depth: 0
204214

205215
- name: Setup PHP
206216
uses: shivammathur/setup-php@v2

.github/workflows/static-analysis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ jobs:
2121
steps:
2222
- name: Checkout code
2323
uses: actions/checkout@v3
24+
with:
25+
fetch-depth: 0
2426

2527
- name: Setup PHP
2628
uses: shivammathur/setup-php@v2

.github/workflows/tests.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ jobs:
4747
steps:
4848
- name: Checkout code
4949
uses: actions/checkout@v3
50+
with:
51+
fetch-depth: 0
5052

5153
- name: Setup PHP
5254
uses: shivammathur/setup-php@v2
@@ -128,6 +130,8 @@ jobs:
128130
129131
- name: Checkout code
130132
uses: actions/checkout@v3
133+
with:
134+
fetch-depth: 0
131135

132136
- name: Setup PHP
133137
uses: shivammathur/setup-php@v2

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"egulias/email-validator": "^3.2.1|^4.0",
3131
"fruitcake/php-cors": "^1.2",
3232
"guzzlehttp/uri-template": "^1.0",
33+
"laravel/prompts": "^0.1",
3334
"laravel/serializable-closure": "^1.3",
3435
"league/commonmark": "^2.2.1",
3536
"league/flysystem": "^3.8.0",

src/Illuminate/Console/Command.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
class Command extends SymfonyCommand
1414
{
1515
use Concerns\CallsCommands,
16+
Concerns\ConfiguresPrompts,
1617
Concerns\HasParameters,
1718
Concerns\InteractsWithIO,
1819
Concerns\InteractsWithSignals,
@@ -173,6 +174,8 @@ public function run(InputInterface $input, OutputInterface $output): int
173174

174175
$this->components = $this->laravel->make(Factory::class, ['output' => $this->output]);
175176

177+
$this->configurePrompts($input);
178+
176179
try {
177180
return parent::run(
178181
$this->input = $input, $this->output
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace Illuminate\Console\Concerns;
4+
5+
use Laravel\Prompts\ConfirmPrompt;
6+
use Laravel\Prompts\MultiSelectPrompt;
7+
use Laravel\Prompts\PasswordPrompt;
8+
use Laravel\Prompts\Prompt;
9+
use Laravel\Prompts\SearchPrompt;
10+
use Laravel\Prompts\SelectPrompt;
11+
use Laravel\Prompts\SuggestPrompt;
12+
use Laravel\Prompts\TextPrompt;
13+
use Symfony\Component\Console\Input\InputInterface;
14+
15+
trait ConfiguresPrompts
16+
{
17+
/**
18+
* Configure the prompt fallbacks.
19+
*
20+
* @param \Symfony\Component\Console\Input\InputInterface $input
21+
* @return void
22+
*/
23+
protected function configurePrompts(InputInterface $input)
24+
{
25+
Prompt::setOutput($this->output);
26+
27+
Prompt::fallbackWhen(! $input->isInteractive() || windows_os() || $this->laravel->runningUnitTests());
28+
29+
TextPrompt::fallbackUsing(fn (TextPrompt $prompt) => $this->promptUntilValid(
30+
fn () => $this->components->ask($prompt->label, $prompt->default ?: null) ?? '',
31+
$prompt->required,
32+
$prompt->validate
33+
));
34+
35+
PasswordPrompt::fallbackUsing(fn (PasswordPrompt $prompt) => $this->promptUntilValid(
36+
fn () => $this->components->secret($prompt->label) ?? '',
37+
$prompt->required,
38+
$prompt->validate
39+
));
40+
41+
ConfirmPrompt::fallbackUsing(fn (ConfirmPrompt $prompt) => $this->promptUntilValid(
42+
fn () => $this->components->confirm($prompt->label, $prompt->default),
43+
$prompt->required,
44+
$prompt->validate
45+
));
46+
47+
SelectPrompt::fallbackUsing(fn (SelectPrompt $prompt) => $this->promptUntilValid(
48+
fn () => $this->components->choice($prompt->label, $prompt->options, $prompt->default),
49+
false,
50+
$prompt->validate
51+
));
52+
53+
MultiSelectPrompt::fallbackUsing(function (MultiSelectPrompt $prompt) {
54+
if ($prompt->default !== []) {
55+
return $this->promptUntilValid(
56+
fn () => $this->components->choice($prompt->label, $prompt->options, implode(',', $prompt->default), multiple: true),
57+
$prompt->required,
58+
$prompt->validate
59+
);
60+
}
61+
62+
return $this->promptUntilValid(
63+
fn () => collect($this->components->choice($prompt->label, ['' => 'None', ...$prompt->options], 'None', multiple: true))
64+
->reject('')
65+
->all(),
66+
$prompt->required,
67+
$prompt->validate
68+
);
69+
});
70+
71+
SuggestPrompt::fallbackUsing(fn (SuggestPrompt $prompt) => $this->promptUntilValid(
72+
fn () => $this->components->askWithCompletion($prompt->label, $prompt->options, $prompt->default ?: null) ?? '',
73+
$prompt->required,
74+
$prompt->validate
75+
));
76+
77+
SearchPrompt::fallbackUsing(fn (SearchPrompt $prompt) => $this->promptUntilValid(
78+
function () use ($prompt) {
79+
$query = $this->components->ask($prompt->label);
80+
81+
$options = ($prompt->options)($query);
82+
83+
return $this->components->choice($prompt->label, $options);
84+
},
85+
false,
86+
$prompt->validate
87+
));
88+
}
89+
90+
/**
91+
* Prompt the user until the given validation callback passes.
92+
*
93+
* @param \Closure $prompt
94+
* @param bool|string $required
95+
* @param \Closure|null $validate
96+
* @return mixed
97+
*/
98+
protected function promptUntilValid($prompt, $required, $validate)
99+
{
100+
while (true) {
101+
$result = $prompt();
102+
103+
if ($required && ($result === '' || $result === [] || $result === false)) {
104+
$this->components->error(is_string($required) ? $required : 'Required.');
105+
106+
continue;
107+
}
108+
109+
if ($validate) {
110+
$error = $validate($result);
111+
112+
if (is_string($error) && strlen($error) > 0) {
113+
$this->components->error($error);
114+
115+
continue;
116+
}
117+
}
118+
119+
return $result;
120+
}
121+
}
122+
}

src/Illuminate/Console/Concerns/PromptsForMissingInput.php

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
namespace Illuminate\Console\Concerns;
44

5+
use Closure;
56
use Illuminate\Contracts\Console\PromptsForMissingInput as PromptsForMissingInputContract;
67
use Symfony\Component\Console\Input\InputInterface;
78
use Symfony\Component\Console\Output\OutputInterface;
89

10+
use function Laravel\Prompts\text;
11+
912
trait PromptsForMissingInput
1013
{
1114
/**
@@ -36,13 +39,24 @@ protected function promptForMissingArguments(InputInterface $input, OutputInterf
3639
$prompted = collect($this->getDefinition()->getArguments())
3740
->filter(fn ($argument) => $argument->isRequired() && is_null($input->getArgument($argument->getName())))
3841
->filter(fn ($argument) => $argument->getName() !== 'command')
39-
->each(fn ($argument) => $input->setArgument(
40-
$argument->getName(),
41-
$this->askPersistently(
42-
$this->promptForMissingArgumentsUsing()[$argument->getName()] ??
43-
'What is '.lcfirst($argument->getDescription()).'?'
44-
)
45-
))
42+
->each(function ($argument) use ($input) {
43+
$label = $this->promptForMissingArgumentsUsing()[$argument->getName()] ??
44+
'What is '.lcfirst($argument->getDescription() ?: ('the '.$argument->getName())).'?';
45+
46+
if ($label instanceof Closure) {
47+
return $input->setArgument($argument->getName(), $label());
48+
}
49+
50+
if (is_array($label)) {
51+
[$label, $placeholder] = $label;
52+
}
53+
54+
$input->setArgument($argument->getName(), text(
55+
label: $label,
56+
placeholder: $placeholder ?? '',
57+
validate: fn ($value) => empty($value) ? "The {$argument->getName()} is required." : null,
58+
));
59+
})
4660
->isNotEmpty();
4761

4862
if ($prompted) {
@@ -84,25 +98,4 @@ protected function didReceiveOptions(InputInterface $input)
8498
->reject(fn ($option) => $input->getOption($option->getName()) === $option->getDefault())
8599
->isNotEmpty();
86100
}
87-
88-
/**
89-
* Continue asking a question until an answer is provided.
90-
*
91-
* @param string $question
92-
* @return string
93-
*/
94-
private function askPersistently($question)
95-
{
96-
$answer = null;
97-
98-
while ($answer === null) {
99-
$answer = $this->components->ask($question);
100-
101-
if ($answer === null) {
102-
$this->components->error('The answer is required.');
103-
}
104-
}
105-
106-
return $answer;
107-
}
108101
}

src/Illuminate/Console/ConfirmableTrait.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Illuminate\Console;
44

5+
use function Laravel\Prompts\confirm;
6+
57
trait ConfirmableTrait
68
{
79
/**
@@ -26,12 +28,10 @@ public function confirmToProceed($warning = 'Application In Production', $callba
2628

2729
$this->components->alert($warning);
2830

29-
$confirmed = $this->components->confirm('Do you really wish to run this command?');
31+
$confirmed = confirm('Are you sure you want to run this command?', default: false);
3032

3133
if (! $confirmed) {
32-
$this->newLine();
33-
34-
$this->components->warn('Command canceled.');
34+
$this->components->warn('Command cancelled.');
3535

3636
return false;
3737
}

src/Illuminate/Console/Contracts/NewLineAware.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@
44

55
interface NewLineAware
66
{
7+
/**
8+
* How many trailing newlines were written.
9+
*
10+
* @return int
11+
*/
12+
public function newLinesWritten();
13+
714
/**
815
* Whether a newline has already been written.
916
*
1017
* @return bool
18+
*
19+
* @deprecated use newLinesWritten
1120
*/
1221
public function newLineWritten();
1322
}

src/Illuminate/Console/GeneratorCommand.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,7 +485,35 @@ protected function getArguments()
485485
protected function promptForMissingArgumentsUsing()
486486
{
487487
return [
488-
'name' => 'What should the '.strtolower($this->type).' be named?',
488+
'name' => [
489+
'What should the '.strtolower($this->type).' be named?',
490+
match ($this->type) {
491+
'Cast' => 'E.g. Json',
492+
'Channel' => 'E.g. OrderChannel',
493+
'Console command' => 'E.g. SendEmails',
494+
'Component' => 'E.g. Alert',
495+
'Controller' => 'E.g. UserController',
496+
'Event' => 'E.g. PodcastProcessed',
497+
'Exception' => 'E.g. InvalidOrderException',
498+
'Factory' => 'E.g. PostFactory',
499+
'Job' => 'E.g. ProcessPodcast',
500+
'Listener' => 'E.g. SendPodcastNotification',
501+
'Mail' => 'E.g. OrderShipped',
502+
'Middleware' => 'E.g. EnsureTokenIsValid',
503+
'Model' => 'E.g. Flight',
504+
'Notification' => 'E.g. InvoicePaid',
505+
'Observer' => 'E.g. UserObserver',
506+
'Policy' => 'E.g. PostPolicy',
507+
'Provider' => 'E.g. ElasticServiceProvider',
508+
'Request' => 'E.g. StorePodcastRequest',
509+
'Resource' => 'E.g. UserResource',
510+
'Rule' => 'E.g. Uppercase',
511+
'Scope' => 'E.g. TrendingScope',
512+
'Seeder' => 'E.g. UserSeeder',
513+
'Test' => 'E.g. UserTest',
514+
default => '',
515+
},
516+
],
489517
];
490518
}
491519
}

0 commit comments

Comments
 (0)