Mike Rockétt

Artisan Console Input Validation

A cleaner way to validate console input.

#Laravel — 20 December 2020

Have you ever run an Artisan command that takes in a bunch of input, only to find that you get kicked out of the process because one whole entire answer was invalid?

Bet you have.

It’s happened to me once or twice, and so the thought struck: why can’t console input be validated on-demand? If I enter something incorrectly, I should be told as such and given the immediate opportunity to try again.

There are a few ways to go about this. One is to use a while-loop, the other is to use those godforsaken labels. (Yes, I’ve used them, once upon a time.)

Those two options are there, but there’s a better one: a recursive method. This does two things. Firstly, it reduces the amount of loops you use to precisely zero. And secondly, it keeps the code clean and easy to understand.

Here’s a trait I put together some time back, based on this gem.

Update, December 2020: I’ve gone ahead and cleaned this up a little.

<?php
namespace App\Console\Traits;
 
use Illuminate\Support\Facades\Validator;
 
trait ValidatesConsoleInput
{
protected function withValidation(callable $callable, $rules, array $messages = [])
{
$input = $callable();
 
$validator = Validator::make(
['input' => $input],
['input' => $rules],
(array) collect($messages)->mapWithKeys(
static fn (
string $message,
string $rule
): string => ["input.$rule" => $message]
)
);
 
if ($validator->fails()) {
$this->warn($validator->errors()->first());
$input = $this->withValidation($callable, $rules, $messages);
}
 
return is_string($input) && $input === '' ? null : $input;
}
}

Here, we’re adding a method called withValidation to the command. This takes a callable function, such as a Closure, a set of rules and an array of optional messages.

It starts off by invoking the callable, which must return the user’s input. It then defines a callable that maps out any messages you provide in such a way that you don’t need to provide the name of the input being validated (see the example below), and creates and runs a validator which, if fails, simply re-invokes the callable. Its final job is to return the input – if it’s an empty string, it returns null, otherwise it returns it as-is.

Here’s how you use it:

Update, December 2020: I’ve gone ahead and cleaned this up a little, too.

use ValidatesConsoleInput;
 
$user->email = $this->withValidation(
fn => $this->ask('Email'),
['required', 'email:rfc,dns', 'unique:users,email'],
[
'required' => 'The email address is required.',
'unique' => 'That email address has been used.'
]
);