Skip to content

[11.x] Define global validation logic for Laravel Prompts #49497

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

Merged
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"egulias/email-validator": "^3.2.1|^4.0",
"fruitcake/php-cors": "^1.3",
"guzzlehttp/uri-template": "^1.0",
"laravel/prompts": "^0.1.12",
"laravel/prompts": "^0.1.15",
"laravel/serializable-closure": "^1.3",
"league/commonmark": "^2.2.1",
"league/flysystem": "^3.8.0",
Expand Down
90 changes: 81 additions & 9 deletions src/Illuminate/Console/Concerns/ConfiguresPrompts.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Laravel\Prompts\SelectPrompt;
use Laravel\Prompts\SuggestPrompt;
use Laravel\Prompts\TextPrompt;
use stdClass;
use Symfony\Component\Console\Input\InputInterface;

trait ConfiguresPrompts
Expand All @@ -28,6 +29,8 @@ protected function configurePrompts(InputInterface $input)

Prompt::interactive(($input->isInteractive() && defined('STDIN') && stream_isatty(STDIN)) || $this->laravel->runningUnitTests());

Prompt::validateUsing(fn (Prompt $prompt) => $this->validatePrompt($prompt->value(), $prompt->validate));

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

TextPrompt::fallbackUsing(fn (TextPrompt $prompt) => $this->promptUntilValid(
Expand Down Expand Up @@ -140,24 +143,93 @@ protected function promptUntilValid($prompt, $required, $validate)
}
}

if ($validate) {
$error = $validate($result);
$error = is_callable($validate) ? $validate($result) : $this->validatePrompt($result, $validate);

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

if ($this->laravel->runningUnitTests()) {
throw new PromptValidationException;
} else {
continue;
}
if ($this->laravel->runningUnitTests()) {
throw new PromptValidationException;
} else {
continue;
}
}

return $result;
}
}

/**
* Validate the given prompt value using the validator.
*
* @param mixed $value
* @param mixed $rules
* @return ?string
*/
protected function validatePrompt($value, $rules)
{
if ($rules instanceof stdClass) {
$messages = $rules->messages ?? [];
$attributes = $rules->attributes ?? [];
$rules = $rules->rules ?? null;
}


if (! $rules) {
return;
}

$field = 'answer';

if (is_array($rules) && ! array_is_list($rules)) {
[$field, $rules] = [key($rules), current($rules)];
}

return $this->getPromptValidatorInstance(
$field, $value, $rules, $messages ?? [], $attributes ?? []
)->errors()->first();
}

/**
* Get the validator instance that should be used to validate prompts.
*
* @param string $value
* @param mixed $value
* @param mixed $rules
* @param array $messages
* @param array $attributes
* @return \Illuminate\Validation\Validator
*/
protected function getPromptValidatorInstance($field, $value, $rules, array $messages = [], array $attributes = [])
{
return $this->laravel['validator']->make(
[$field => $value],
[$field => $rules],
empty($messages) ? $this->validationMessages() : $messages,
empty($attributes) ? $this->validationAttributes() : $attributes,
);
}

/**
* Get the validation messages that should be used during prompt validation.
*
* @return array
*/
protected function validationMessages()
{
return [];
}

/**
* Get the validation attributes that should be used during prompt validation.
*
* @return array
*/
protected function validationAttributes()
{
return [];
}

/**
* Restore the prompts output.
*
Expand Down
12 changes: 12 additions & 0 deletions src/Illuminate/Support/helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,18 @@ function filled($value)
}
}

if (! function_exists('literal')) {
/**
* Create a new anonymous object using named arguments.
*
* @return \stdClass
*/
function literal(...$arguments)
{
return (object) $arguments;
}
}

if (! function_exists('object_get')) {
/**
* Get an item from an object using "dot" notation.
Expand Down
77 changes: 74 additions & 3 deletions tests/Integration/Console/PromptsValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,41 @@ class PromptsValidationTest extends TestCase
protected function defineEnvironment($app)
{
$app[Kernel::class]->registerCommand(new DummyPromptsValidationCommand());
$app[Kernel::class]->registerCommand(new DummyPromptsWithLaravelRulesCommand());
$app[Kernel::class]->registerCommand(new DummyPromptsWithLaravelRulesMessagesAndAttributesCommand());
$app[Kernel::class]->registerCommand(new DummyPromptsWithLaravelRulesCommandWithInlineMessagesAndAttibutesCommand());
}

public function testValidationForPrompts()
{
$this
->artisan(DummyPromptsValidationCommand::class)
->expectsQuestion('Test', 'bar')
->expectsOutputToContain('error!');
->expectsQuestion('What is your name?', '')
->expectsOutputToContain('Required!');
}

public function testValidationWithLaravelRulesAndNoCustomization()
{
$this
->artisan(DummyPromptsWithLaravelRulesCommand::class)
->expectsQuestion('What is your name?', '')
->expectsOutputToContain('The answer field is required.');
}

public function testValidationWithLaravelRulesInlineMessagesAndAttributes()
{
$this
->artisan(DummyPromptsWithLaravelRulesCommandWithInlineMessagesAndAttibutesCommand::class)
->expectsQuestion('What is your name?', '')
->expectsOutputToContain('Your full name is mandatory.');
}

public function testValidationWithLaravelRulesMessagesAndAttributes()
{
$this
->artisan(DummyPromptsWithLaravelRulesMessagesAndAttributesCommand::class)
->expectsQuestion('What is your name?', '')
->expectsOutputToContain('Your full name is mandatory.');
}
}

Expand All @@ -30,6 +57,50 @@ class DummyPromptsValidationCommand extends Command

public function handle()
{
text('Test', validate: fn ($value) => $value == 'foo' ? '' : 'error!');
text('What is your name?', validate: fn ($value) => $value == '' ? 'Required!' : null);
}
}

class DummyPromptsWithLaravelRulesCommand extends Command
{
protected $signature = 'prompts-laravel-rules-test';

public function handle()
{
text('What is your name?', validate: 'required');
}
}

class DummyPromptsWithLaravelRulesCommandWithInlineMessagesAndAttibutesCommand extends Command
{
protected $signature = 'prompts-laravel-rules-inline-test';

public function handle()
{
text('What is your name?', validate: literal(
rules: ['name' => 'required'],
messages: ['name.required' => 'Your :attribute is mandatory.'],
attributes: ['name' => 'full name'],
));
}
}

class DummyPromptsWithLaravelRulesMessagesAndAttributesCommand extends Command
{
protected $signature = 'prompts-laravel-rules-messages-attributes-test';

public function handle()
{
text('What is your name?', validate: ['name' => 'required']);
}

protected function validationMessages()
{
return ['name.required' => 'Your :attribute is mandatory.'];
}

protected function validationAttributes()
{
return ['name' => 'full name'];
}
}