Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.x] Prompts #46772

Merged
merged 31 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
03a9d5f
Configure prompt fallbacks
jessarcher Apr 11, 2023
adcbe06
Update built-in prompts
jessarcher Apr 11, 2023
3a2e7d1
Add placeholder values to make commands
jessarcher Apr 11, 2023
e7991d8
Improve docs command when page not found
jessarcher Apr 12, 2023
d3255e4
Formatting
jessarcher Apr 12, 2023
166f24b
Show all docs matches when nothing entered
jessarcher Apr 12, 2023
4868139
Update option labels
jessarcher Apr 12, 2023
9a2a972
Tweak vendor:publish compatibility with both prompts
jessarcher Apr 12, 2023
928a89d
Fallback to Symfony when running tests
jessarcher Apr 12, 2023
2bdc345
formatting and fixes
taylorotwell Apr 14, 2023
1110a57
print newline if using prompts on confirmable trait
taylorotwell Apr 14, 2023
4f3bed5
Apply fixes from StyleCI
StyleCIBot Apr 14, 2023
3b039da
Improving spacing between components
jessarcher Apr 19, 2023
4a1c3bc
Update OutputStyle.php
taylorotwell May 22, 2023
04a0943
Adds missing dependency
nunomaduro May 22, 2023
a997347
Adjusts visibitily
nunomaduro May 22, 2023
8bb43e8
Fixes return type
nunomaduro May 22, 2023
44803ad
Add missing deprecation tag
jessarcher May 23, 2023
c194be3
Ensure newline state is preserved correctly
jessarcher May 23, 2023
93d4431
Formatting
jessarcher May 23, 2023
434c1ea
Fix example for `make:command`
jessarcher Jul 4, 2023
bb6387e
Prompt for missing args with the arg name when no description provided
jessarcher Jul 5, 2023
e5a9d62
Allow passing a closure when customizing missing argument prompts
jessarcher Jul 5, 2023
486dd71
Add missing secret component
jessarcher Jul 7, 2023
b5f7561
Add `required` support to prompt fallbacks
jessarcher Jul 7, 2023
1e2f8d3
Simply `select` fallback
jessarcher Jul 7, 2023
f2674c7
Add fallback for search prompt
jessarcher Jul 26, 2023
4b8fd9f
Update prompts dependency
jessarcher Jul 26, 2023
6799a1b
Update prompts version
jessarcher Jul 27, 2023
f97a714
Fix installation of prompts in CI
jessarcher Jul 28, 2023
436a825
Fix tests
jessarcher Jul 28, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/databases.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
Comment on lines +32 to +33
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hate having to do this.

The issue is that laravel/prompts depends on illuminate/collections ^10.0, which creates a circular dependency. This would ordinarily be fine because the composer.json in this project specifies that it provides illuminate/collections at self.version. However, when doing a shallow clone in CI, none of the tags exist, so self.version resolves to the hash of the commit, which doesn't satisfy the dependency.

Alternatives are:

  • Make laravel/prompts depend on * instead of ^10.0 but remove the ability for composer to force a known compatible version.
  • Remove the dependency from laravel/prompts and rely on built-in PHP methods.
  • Add the top-level version key to the composer.json on this repo and maintain it going forward.
  • Add an extra step to CI on this project to automatically add the version key to composer.json just for the duration of the build, scraping it from Application.php. We'd need to be careful with any actions that make commits (e.g. update changelog) to make sure they don't include the change.

I went with this option as it felt the least hacky and problematic. The downside is that the clone is slower and bigger (an additional ~20MB of git history). If this isn't acceptable, I think I'd opt for removing collections from laravel/prompts.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Illuminate\Support\Collection is only used for type-hint and $collect instanceof Collection I believe we can skip it from laravel/prompts as it shouldn't cause any issue.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prompts is using collections functionality in a few places, not just the types :(


- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down Expand Up @@ -72,6 +74,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down Expand Up @@ -115,6 +119,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down Expand Up @@ -159,6 +165,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down Expand Up @@ -201,6 +209,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/facades.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down Expand Up @@ -128,6 +130,8 @@ jobs:

- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Setup PHP
uses: shivammathur/setup-php@v2
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"egulias/email-validator": "^3.2.1|^4.0",
"fruitcake/php-cors": "^1.2",
"guzzlehttp/uri-template": "^1.0",
"laravel/prompts": "^0.1",
"laravel/serializable-closure": "^1.3",
"league/commonmark": "^2.2.1",
"league/flysystem": "^3.8.0",
Expand Down
3 changes: 3 additions & 0 deletions src/Illuminate/Console/Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
class Command extends SymfonyCommand
{
use Concerns\CallsCommands,
Concerns\ConfiguresPrompts,
Concerns\HasParameters,
Concerns\InteractsWithIO,
Concerns\InteractsWithSignals,
Expand Down Expand Up @@ -173,6 +174,8 @@ public function run(InputInterface $input, OutputInterface $output): int

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

$this->configurePrompts($input);

try {
return parent::run(
$this->input = $input, $this->output
Expand Down
122 changes: 122 additions & 0 deletions src/Illuminate/Console/Concerns/ConfiguresPrompts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?php

namespace Illuminate\Console\Concerns;

use Laravel\Prompts\ConfirmPrompt;
use Laravel\Prompts\MultiSelectPrompt;
use Laravel\Prompts\PasswordPrompt;
use Laravel\Prompts\Prompt;
use Laravel\Prompts\SearchPrompt;
use Laravel\Prompts\SelectPrompt;
use Laravel\Prompts\SuggestPrompt;
use Laravel\Prompts\TextPrompt;
use Symfony\Component\Console\Input\InputInterface;

trait ConfiguresPrompts
{
/**
* Configure the prompt fallbacks.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @return void
*/
protected function configurePrompts(InputInterface $input)
{
Prompt::setOutput($this->output);

Prompt::fallbackWhen(! $input->isInteractive() || windows_os() || $this->laravel->runningUnitTests());

TextPrompt::fallbackUsing(fn (TextPrompt $prompt) => $this->promptUntilValid(
fn () => $this->components->ask($prompt->label, $prompt->default ?: null) ?? '',
$prompt->required,
$prompt->validate
));

PasswordPrompt::fallbackUsing(fn (PasswordPrompt $prompt) => $this->promptUntilValid(
fn () => $this->components->secret($prompt->label) ?? '',
$prompt->required,
$prompt->validate
));

ConfirmPrompt::fallbackUsing(fn (ConfirmPrompt $prompt) => $this->promptUntilValid(
fn () => $this->components->confirm($prompt->label, $prompt->default),
$prompt->required,
$prompt->validate
));

SelectPrompt::fallbackUsing(fn (SelectPrompt $prompt) => $this->promptUntilValid(
fn () => $this->components->choice($prompt->label, $prompt->options, $prompt->default),
false,
$prompt->validate
));

MultiSelectPrompt::fallbackUsing(function (MultiSelectPrompt $prompt) {
if ($prompt->default !== []) {
return $this->promptUntilValid(
fn () => $this->components->choice($prompt->label, $prompt->options, implode(',', $prompt->default), multiple: true),
$prompt->required,
$prompt->validate
);
}

return $this->promptUntilValid(
fn () => collect($this->components->choice($prompt->label, ['' => 'None', ...$prompt->options], 'None', multiple: true))
->reject('')
->all(),
$prompt->required,
$prompt->validate
);
});

SuggestPrompt::fallbackUsing(fn (SuggestPrompt $prompt) => $this->promptUntilValid(
fn () => $this->components->askWithCompletion($prompt->label, $prompt->options, $prompt->default ?: null) ?? '',
$prompt->required,
$prompt->validate
));

SearchPrompt::fallbackUsing(fn (SearchPrompt $prompt) => $this->promptUntilValid(
function () use ($prompt) {
$query = $this->components->ask($prompt->label);

$options = ($prompt->options)($query);

return $this->components->choice($prompt->label, $options);
},
false,
$prompt->validate
));
}

/**
* Prompt the user until the given validation callback passes.
*
* @param \Closure $prompt
* @param bool|string $required
* @param \Closure|null $validate
* @return mixed
*/
protected function promptUntilValid($prompt, $required, $validate)
{
while (true) {
$result = $prompt();

if ($required && ($result === '' || $result === [] || $result === false)) {
$this->components->error(is_string($required) ? $required : 'Required.');

continue;
}

if ($validate) {
$error = $validate($result);

if (is_string($error) && strlen($error) > 0) {
$this->components->error($error);

continue;
}
}

return $result;
}
}
}
49 changes: 21 additions & 28 deletions src/Illuminate/Console/Concerns/PromptsForMissingInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

namespace Illuminate\Console\Concerns;

use Closure;
use Illuminate\Contracts\Console\PromptsForMissingInput as PromptsForMissingInputContract;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

use function Laravel\Prompts\text;

trait PromptsForMissingInput
{
/**
Expand Down Expand Up @@ -36,13 +39,24 @@ protected function promptForMissingArguments(InputInterface $input, OutputInterf
$prompted = collect($this->getDefinition()->getArguments())
->filter(fn ($argument) => $argument->isRequired() && is_null($input->getArgument($argument->getName())))
->filter(fn ($argument) => $argument->getName() !== 'command')
->each(fn ($argument) => $input->setArgument(
$argument->getName(),
$this->askPersistently(
$this->promptForMissingArgumentsUsing()[$argument->getName()] ??
'What is '.lcfirst($argument->getDescription()).'?'
)
))
->each(function ($argument) use ($input) {
$label = $this->promptForMissingArgumentsUsing()[$argument->getName()] ??
'What is '.lcfirst($argument->getDescription() ?: ('the '.$argument->getName())).'?';

if ($label instanceof Closure) {
return $input->setArgument($argument->getName(), $label());
}

if (is_array($label)) {
[$label, $placeholder] = $label;
}

$input->setArgument($argument->getName(), text(
label: $label,
placeholder: $placeholder ?? '',
validate: fn ($value) => empty($value) ? "The {$argument->getName()} is required." : null,
));
})
->isNotEmpty();

if ($prompted) {
Expand Down Expand Up @@ -84,25 +98,4 @@ protected function didReceiveOptions(InputInterface $input)
->reject(fn ($option) => $input->getOption($option->getName()) === $option->getDefault())
->isNotEmpty();
}

/**
* Continue asking a question until an answer is provided.
*
* @param string $question
* @return string
*/
private function askPersistently($question)
{
$answer = null;

while ($answer === null) {
$answer = $this->components->ask($question);

if ($answer === null) {
$this->components->error('The answer is required.');
}
}

return $answer;
}
}
8 changes: 4 additions & 4 deletions src/Illuminate/Console/ConfirmableTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Illuminate\Console;

use function Laravel\Prompts\confirm;

trait ConfirmableTrait
{
/**
Expand All @@ -26,12 +28,10 @@ public function confirmToProceed($warning = 'Application In Production', $callba

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

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

if (! $confirmed) {
$this->newLine();

$this->components->warn('Command canceled.');
$this->components->warn('Command cancelled.');

return false;
}
Expand Down
9 changes: 9 additions & 0 deletions src/Illuminate/Console/Contracts/NewLineAware.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@

interface NewLineAware
{
/**
* How many trailing newlines were written.
*
* @return int
*/
public function newLinesWritten();

/**
* Whether a newline has already been written.
*
* @return bool
*
* @deprecated use newLinesWritten
*/
public function newLineWritten();
}
30 changes: 29 additions & 1 deletion src/Illuminate/Console/GeneratorCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,35 @@ protected function getArguments()
protected function promptForMissingArgumentsUsing()
{
return [
'name' => 'What should the '.strtolower($this->type).' be named?',
'name' => [
'What should the '.strtolower($this->type).' be named?',
match ($this->type) {
'Cast' => 'E.g. Json',
'Channel' => 'E.g. OrderChannel',
'Console command' => 'E.g. SendEmails',
'Component' => 'E.g. Alert',
'Controller' => 'E.g. UserController',
'Event' => 'E.g. PodcastProcessed',
'Exception' => 'E.g. InvalidOrderException',
'Factory' => 'E.g. PostFactory',
'Job' => 'E.g. ProcessPodcast',
'Listener' => 'E.g. SendPodcastNotification',
'Mail' => 'E.g. OrderShipped',
'Middleware' => 'E.g. EnsureTokenIsValid',
'Model' => 'E.g. Flight',
'Notification' => 'E.g. InvoicePaid',
'Observer' => 'E.g. UserObserver',
'Policy' => 'E.g. PostPolicy',
'Provider' => 'E.g. ElasticServiceProvider',
'Request' => 'E.g. StorePodcastRequest',
'Resource' => 'E.g. UserResource',
'Rule' => 'E.g. Uppercase',
'Scope' => 'E.g. TrendingScope',
'Seeder' => 'E.g. UserSeeder',
'Test' => 'E.g. UserTest',
default => '',
},
],
];
}
}
Loading