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

requiredWith and "value" #327

Open
dedece35 opened this issue Jan 10, 2021 · 1 comment
Open

requiredWith and "value" #327

dedece35 opened this issue Jan 10, 2021 · 1 comment

Comments

@dedece35
Copy link

dedece35 commented Jan 10, 2021

Hello,

I want to validate the "field2" only if "field1" is present and if "field1" value is equal to "myvalue1".
Indeed, "field1" is a radio button and can have three different values : "myvalue1", "myvalue2" and "myvalue3".

Is this question a new feature or can you help me to do this ?

thank you.

@ezequiel6arrido
Copy link

ezequiel6arrido commented Feb 12, 2022

This is particularly useful for validating Conditional Fields.

Problem

Imagine you have a very simple payment information form, here's the initial premise.

You can either pay with a Credit Card, or with your Bank Account.
- If you pay with your Credit Card, you'd need to submit your Credit Card Number.
- If you pay with your Bank Account, you'd need to submit your Bank Account Alias.

Now here's some very simple markup to visualize it:

<form method="post">
    <select name="payment_method" required>
        <option value="">Payment Method</option>
        <option value="credit_card" selected>Credit Card</option>
        <option value="bank_account">Bank Account</option>
    </select>
    <input name="credit_card_number" placeholder="Credit Card Number" value="1" required>
    <input name="bank_account_alias" placeholder="Bank Account Alias" value="" required>
    <input type="submit" value="submit">
</form>

Now ideally, you wouldn't show the Bank Account Alias field when you selected Credit Card as your payment method and viceversa. You'd probably use some JavaScript, either vanilla or most probably a frontend framework to show/hide fields according to the Payment Method selection.

Here's a very simple example using AlpineJS just to prove a point.

AlpineJS implementation
<script src="//unpkg.com/alpinejs" defer></script>

<form method="post" x-data="{payment_method: ''}">
    <select name="payment_method" required x-model="payment_method">
        <option value="">Payment Method</option>
        <option value="credit_card" selected>Credit Card</option>
        <option value="bank_account">Bank Account</option>
    </select>
    <input name="credit_card_number" placeholder="Credit Card Number" value="1" required x-show="payment_method == 'credit_card'">
    <input name="bank_account_alias" placeholder="Bank Account Alias" value="" required x-show="payment_method == 'bank_account'">
    <input type="submit" value="submit">
</form>

The problem then becomes validating the initial premise with Valitron.

See Credit Card Number and Bank Account Alias can't be required at the same time, so you'd need another either built in or custom validation rule to handle their validation.

Solution A - Within Valitron ❌

Baked in

⚠ This solution does not work..but it could with a PR

The ideal solution for me would be something baked into Valitron, a requiredWhen validation rule that takes in a field, and multiple conditions (other fields with the desired value).

View full example in PHPSandbox.

$rules = [
    'required' => [
        'payment_method',
    ],
    'requiredWhen' => [
        [
            'credit_card_number',
            [ 'payment_method' => 'credit_card' ]
        ],
        [
            'bank_account_alias',
            [ 'payment_method' => 'bank_account' ]
        ]
    ],
    'creditCard' => ['credit_card_number'],
    'regex' => [
        [ 'bank_account_alias','/[a-zA-Z0-9]{1,30}/']
    ]
];

Custom Validation Rule

⚠ This solution does not work..but it could with a PR

However, since its not currently baked in, the next best thing would be a Custom Validation Rule, lets explore this.

View full example in PHPSandbox.

This is the custom validation rule I came up with, it obeys the same format as the previous solution.

Valitron\Validator::addRule('requiredWhen', function($field, $value, array $params, array $fields) {
    $conditions = $params[0];
    foreach( $conditions as $condition_field => $condition_value ) {
        if( $fields[$condition_field] == $condition_value
            && empty($value) ) {
                return false;
            }
    }
    return true;
}, 'is required.');

The problem is, imagine recieving incorrect $_POST data, like this:

$_POST = array(
  payment_method	=> 'credit_card',
  credit_card_number => '',	
  bank_account_alias =>	'wrong field is filled out, should be credit_card_number'
);

Since credit_card_number is empty, it will not be processed by Valitron therefore the requiredWhen rule does not fire, so we end up with an incorrect validation. credit_card_number is not validated and therefore the incorrect submission is treated as valid.

Now we could solve this with the second parameter of the required rule, if true, it will become a "soft" required and only require the key to exists, allowing it to be empty, but then if we had other rules, say like a regexp for the bank_account_alias field, they'd trigger too, and fail, because it's an empty value (but they shouldn't, because we don't care about bank_account_alias when the selected payment method is Credit Card).

Another way to solve this would be for requiredWith and requiredWithout to have "soft" versions or parameters where they don't check the contents of the data key, just check that its there (similar to the required paramater). That way additional rules won't trigger unless the field is actually being required.

Solution B - Outside Valitron ✔

View full example in PHPSandbox.

Sadly, this was a requirement in one of my projects, so I was forced to solve it outside of Valitron.

My solution involves a simple function which takes in the validator (for now, its just to call the data() function and get the same data, since the Valitron\Validator constructor may filter data based on the $fields parameter), some conditions (if rules, required), some rules to output in case those conditions are met (then rules, required), and some rules to output in case those conditions are not met (else rules, optional).

This is heavily inspired in the v::when rule in Respect\Validation, and since it has both "then", and "else" rules can even be chained into complex patterns.

function when( $validator, $conditions, $then_rules, $else_rules = [] ) {

    $condition_validator = new Valitron\Validator( $validator->data() );
    $condition_validator->rules( $conditions );
    $conditions_passed = $condition_validator->validate();

    if( $conditions_passed ) {
        return $then_rules;
    }

    return $else_rules;

}

The usage is pretty straightforward, we will merge regular rules with rules created by the when() recursively and pass that in to the rules() function.

$v = new Valitron\Validator( $_POST );

$rules = array_merge_recursive(
    [
        'required' => ['payment_method']
    ],
    when(
        $v,
        [
            'required' => [ 'payment_method' ],
            'regex' => [
                [ 'payment_method', '/^credit_card$/' ]
            ],
        ],
        [
            'required' => ['credit_card_number'],
            'creditCard' => ['credit_card_number']
        ]
    ),
    when(
        $v,
        [
            'required' => [ 'payment_method' ],
            'regex' => [
                [ 'payment_method', '/^bank_account$/' ]
            ]
        ],
        [
            'required' => ['bank_account_alias'],
            'regex' => [
                [ 'bank_account_alias','/[A-z\.]{6,30}/']
            ]
        ]
    )
);

$v->rules( $rules );

Note: Remember to add required inside your if conditions since other rules that depend on that field won't be processed if that's not the case (explained in detail in the previous solutions in this comment).

So, for example, for this $_POST request:

$_POST = array(
  payment_method	=> 'credit_card',
  credit_card_number => '',	
  bank_account_alias =>	'wrong field is filled out, should be credit_card_number'
);

The final rules array passed in to the validator would be:

array(2) { 
    ["required"]=> array(2) { 
         [0]=> string(14) "payment_method" 
         [1]=> string(18) "credit_card_number" 
    } 
    ["creditCard"]=> array(1) { 
         [0]=> string(18) "credit_card_number" 
     } 
}

The required rules have been merged, and the creditCard validation rule has been added, preventing the $_POST'd output from validating since its wrong.

Thoughts?

Id love to hear @vlucas and @willemwollebrants thoughts on this, I'd be happy to work on a PR to implement either a baked in solution or a way to allow custom validation rules to validate empty fields (optional is not enough for this use case), or modifying the behaviour of existing require* functions (requiredWith is too strict for this use case), or even incorporate my "outside valitron" solution as a when rule, with conditions and resulting rules, similar to v::when in Respect\Validation.

That being said, I have no idea if this use case is within the scope of the project or not, I'd argue it is, but I'm willing to be convinced otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants