Validation - shomisha/laravel-console-wizard GitHub Wiki

Validation

With any system that allows users to input values, it is important to be able to validate the user provided input. This package provides you with simple but powerful mechanisms for validating the answers provided by your users. There are three strategies for validating user input:

  1. Validating on a per step basis
  2. Automatically repeating steps with invalid answers
  3. Validating the complete wizard

1. Per step validation

You can perform validation on each step separately as soon as that step is answered by your user. To do this, have your wizard implement the Shomisha\LaravelConsoleWizard\Contracts\ValidatesWizardSteps interface. This interface prescribes the getRules(): array method and requires your wizard to implement it. Under the hood, validation relies on Laravel's Validator, therefore the getRules() method should return an array keyed by the names of your steps that contain validation rules in the same way it would for Laravel validation.

Please note that you are not required to provide validation rules for every step in your wizard. If you omit a rule for a certain step, that step will simply not get validated. That way you can control which step gets validated and which doesn't in addition to controlling the validation rules.

Validation in this strategy is performed immediately after the user inputs the answer, and even before the modifier method for that step, if there is one. This is by design as validation should be used to validate user input, and any developer directed manipulations should be performed upon valid input.

So what happens when the validation fails? You can implement validation handlers, methods that will be invoked once validation for a specific answer is invalid. These methods work in the same way as modifier methods, i.e. if you declare a validation handler it will be invoked, however if you don't declare one it simply won't be invoked. However, please keep in mind that as a safety precaution a validation exception will be thrown if validation fails and you do not have a validation handler declared, effectively terminating your command.

To create a validation handler simply declare a method named in the following convention onInvalid<YourStepNameInCamelCase>($answer, array $errors) and within it handle the failed validation. The first parameter passed to this method is the answer provided by the user, and the second one is an array of Laravel standard errors which describe why the answer is invalid. Inside this method you could abort the wizard, output whatever you need to the user or simply let the wizard continue.

Here is an example of a wizard that performs per-step validation:

<?php

namespace App\Console\Commands;

use Shomisha\LaravelConsoleWizard\Command\Wizard;
use Shomisha\LaravelConsoleWizard\Contracts\ValidatesWizardSteps;
use Shomisha\LaravelConsoleWizard\Steps\TextStep;


class WizardTest extends Wizard implements ValidatesWizardSteps
{
    protected $signature = 'wizard:test';

    protected $description = 'Wizard description';

    function getSteps(): array
    {
        return [
            'name' => new TextStep('What is your name?'),
            'country' => new TextStep('Where do you come from?'),
            'age' => new TextStep('How old are you?'),
        ];
    }

    public function getRules(): array
    {
        return [
            'country' => 'string|in:serbia,england,croatia,japan',
            'age' => ['integer', 'min:18', 'max:85'],
        ];
    }

    public function onInvalidCountry($answer, array $errors)
    {
        $this->error($errors[0]);
    }

    public function onInvalidAge($answer, array $errors)
    {
        $this->abort('This is unacceptable.');
    }

    function completed()
    {
        dd($this->answers->all());
    }
}

And here is the output it produces:

shomisha:laravel-console-wizard shomisha$ php artisan wizard:test

 What is your name?:
 > Misa

 Where do you come from?:
 > Dallas

The selected country is invalid.

 How old are you?:
 > 15

"This is unacceptable."
shomisha:laravel-console-wizard shomisha$ 

Take a look at the order of the output produced. Notice how the The selected country is invalid. message is displayed right after the Where do you come from? step? This illustrates that the answers from that step are validated right after it has been completed, and the validation handler is invoked right there when the validation fails.

Because the onInvalidCountry() handler does not terminate the wizard in any manner, it simply continues and initiates the next step, asking the user for their age. Since the answer to that step is invalid too, the string That is unacceptable. is printed out, and because the abort() method is used the wizard is terminated, which is why it didn't get to the completed() method and dump all of the answers.

Finally, please direct your attention to the fact that the getRules() method returns no rules for the name step. This is perfectly legitimate and will simply result in that step not being validated.

2. Repeating steps with invalid answers

This strategy performs validation on the answer as soon as a step is answered, much as the previous one. It will also invoke a validation failed handler as the previous one, run validation before the modifier method and skip validating steps that contain no rules specified.

The main difference, however, is that instead of throwing an exception if no handler is defined, it will automatically repeat the invalidly answered step. It will even automatically output the first validation error message.

In order to achieve this behavior, instead of having your wizard implement the Shomisha\LaravelConsoleWizard\Contracts\ValidatesWizardSteps contract you should have it implement the Shomisha\LaravelConsoleWizard\Contracts\RepeatsInvalidSteps contract. This contract requires you to define the getRules(): array method, just as the step validation one does, and that's all, everything else is handled automatically. You may define validation handlers, but if you don't the steps will get repeated automatically.

Take a look at how you can to this:

<?php

namespace App\Console\Commands;

use Illuminate\Validation\Rule;
use Shomisha\LaravelConsoleWizard\Command\Wizard;
use Shomisha\LaravelConsoleWizard\Contracts\RepeatsInvalidSteps;
use Shomisha\LaravelConsoleWizard\Steps\TextStep;

class WizardTest extends Wizard implements RepeatsInvalidSteps
{
    protected $signature = 'wizard:repeat-steps-wizard';

    protected $description = 'Wizard description';

    function getSteps(): array
    {
        return [
            'name' => new TextStep('Enter your name'),
            'age' => new TextStep('Enter your age'),
            'gender' => new TextStep('Enter your gender'),
        ];
    }

    public function getRules(): array
    {
        return [
            'name' => ['required', 'string', 'min:3'],
            'age' => ['required', 'integer', 'min:18', 'max:50'],
            'gender' => ['required', 'string', Rule::in(['m', 'f'])],
        ];
    }

    public function onInvalidAge($age, array $errors)
    {
        $this->line("Ooops, looks like you don't fit our target audience.");
    }

    function completed()
    {
        dd($this->answers->all());
    }
}

And here is the output the above Wizard would produce:

shomisha:laravel-console-wizard shomisha$ php artisan wizard:repeat-steps-wizard

 Enter your name:
 > Mi

The name must be at least 3 characters.

 Enter your name:
 > Misa

 Enter your age:
 > 55

Ooops, looks like you don't fit our target audience.

 Enter your gender:
 > male

The selected gender is invalid.

 Enter your gender:
 > female

The selected gender is invalid.

 Enter your gender:
 > m

array:3 [
  "name" => "Misa"
  "age" => "55"
  "gender" => "m"
]
shomisha:laravel-console-wizard shomisha$ 

Notice how providing invalid answers to the name and gender steps outputted validation errors and repeated steps, despite those steps not having any invalid validation handlers defined. Similarly, the age step was not repeated, but a line was printed onto the console because it has a validation handler defined and it was invoked instead of automatically repeating the step.

3. Validating the complete wizard

The other strategy is validating the complete wizard after all of the steps have been taken. With this approach, all of the steps are validated at the same time, and after all of them have been taken. Unlike the other strategy, the complete wizard flow including modifier method is performed prior to validation here.

To perform validation in this manner have your wizard implement the Shomisha\LaravelConsoleWizard\Contracts\ValidatesWizard interface. This interface prescribes two methods: getRules(): array and onWizardInvalid(array $errors). The first method serves the same purpose as does the getRules() in ValidatesWizardSteps, while the onWizardInvalid() method is the one that gets invoked should there be a validation error, and the argument it receives is an array of all of the errors keyed by step names. At this point the wizard flow is complete and steps cannot be retaken afterwards.

Here is an example of a wizard that performs wizard-based validation:

<?php

namespace App\Console\Commands;

use Shomisha\LaravelConsoleWizard\Command\Wizard;
use Shomisha\LaravelConsoleWizard\Contracts\ValidatesWizard;
use Shomisha\LaravelConsoleWizard\Steps\TextStep;


class WizardTest extends Wizard implements ValidatesWizard
{
    protected $signature = 'wizard:test';

    protected $description = 'Wizard description';

    function getSteps(): array
    {
        return [
            'name' => new TextStep('What is your name?'),
            'country' => new TextStep('Where do you come from?'),
            'age' => new TextStep('How old are you?'),
        ];
    }

    public function onWizardInvalid(array $errors)
    {
        dd($errors);
    }

    public function getRules(): array
    {
        return [
            'country' => 'string|in:serbia,england,croatia,japan',
            'age' => ['integer', 'min:18', 'max:85'],
        ];
    }

    function completed()
    {
        dd($this->answers->all());
    }
}

And here is the output it produces:

shomisha:laravel-console-wizard shomisha$ php artisan wizard:test

 What is your name?:
 > Misa

 Where do you come from?:
 > Jamaica

 How old are you?:
 > 13

array:2 [
  "country" => array:1 [
    0 => "The selected country is invalid."
  ]
  "age" => array:1 [
    0 => "The age must be at least 18."
  ]
]
shomisha:laravel-console-wizard shomisha$ 

Pay attention to how the errors are dumped after all of the steps were taken. This is due to the validation being performed after all of the steps were taken and the onWizardInvalid() method getting invoked after validation has determined the data is invalid. In the same way, the wizard answers were not dumped becuase onWizardInvalid() was executed before completed() and its dd() statement terminated the wizard.