Framework: [5.3] Validation with Nullable and required_if

Created on 4 Dec 2016  Â·  20Comments  Â·  Source: laravel/framework

  • Laravel Version: 5.3.26
  • PHP Version: 7.0.13
  • Database Driver & Version: -

Description:

In 5.2.x, I could make the following rules:

./artisan tinker
Psy Shell v0.7.2 (PHP 7.0.13-1+deb.sury.org~trusty+1 — cli) by Justin Hileman
>>> $rules = ['a' => 'boolean', 'b' => 'string|required_if:a,true'];
=> [
     "a" => "boolean",
     "b" => "string|required_if:a,true",
   ]

The dependency worked:

>>> Validator::make(['a' => true], $rules)->errors()->all();
=> [
     "The b field is required when a is 1.",
   ]

And null values where accepted too:

>>> Validator::make(['a' => null], $rules)->errors()->all();
=> []
>>> Validator::make(['b' => null], $rules)->errors()->all();
=> []

The change in Laravel 5.3

Starting with the same ruleset:

./artisan tinker
Psy Shell v0.7.2 (PHP 7.0.13-1+deb.sury.org~trusty+1 — cli) by Justin Hileman
>>> $rules = ['a' => 'boolean', 'b' => 'string|required_if:a,true'];
=> [
     "a" => "boolean",
     "b" => "string|required_if:a,true",
   ]

The dependency works:

>>> Validator::make(['a' => true], $rules)->errors()->all();
=> [
     "The b field is required when a is 1.",
   ]

But null values are not accepted anymore:

>>> Validator::make(['a' => null], $rules)->errors()->all();
=> [
     "The a field must be true or false.",
   ]
>>> Validator::make(['b' => null], $rules)->errors()->all();
=> [
     "The b must be a string value.",
   ]

From the upgrade guide:

Nullable Primitives

When validating arrays, booleans, integers, numerics, and strings, null will no longer be considered a valid value unless the rule set contains the new nullable rule:
Validate::make($request->all(), [
'field' => 'nullable|max:5',
]);

So I added nullable:

>>> $rules = ['a' => 'nullable|boolean', 'b' => 'nullable|string|required_if:a,true'];
=> [
     "a" => "nullable|boolean",
     "b" => "nullable|string|required_if:a,true",
   ]

But then, the dependency doesn't work anymore:

>>> Validator::make(['a' => true], $rules)->errors()->all();
=> []

null values are accepted again, however:

>>> Validator::make(['a' => null], $rules)->errors()->all();
=> []
>>> Validator::make(['b' => null], $rules)->errors()->all();
=> []

What is the solution to achieve the same effect in 5.3 as in 5.2?

Most helpful comment

The only economical solution I found so far is to:

  • not use nullable for fields which have any of the required_* rules
  • replace any addition rule within a rule which contains required_* with a custom one, which accepts null too

E.g. if my rule is supposed to be string|required_if:a,true I need to:

  • add my own my_string rule, which works like the built-in string but returns true for null
  • adapt the rule to my_string|required_if:a,true

Since I've quite some required_* based rules, I ended up creating such shims for string, integer, boolean, min, max, date_format, in, after ...

At this point it's arguable if I should just have extended the Validator class itself, but

  • the more I tackle internals of a Laravel class, the more work I may have with future updates
  • these workarounds were only required for rules which contain any of the required_* directives. Although it seems much, 90% of my validation does not include them so it makes sense to leverage the existing Laravel infrastructure as much as possible

This is now a perspective from a mid-sized project I guess, which currently has >1600 tests.

When I implemented the first parts roughly a year ago, on 5.1, the implicit acceptance of null for rules like string felt weird (after all, this is a pure Json API based project), but over time I saw it's sense and I only needed a few custom rules to make it through.

Now after 5.3, only for this update I had to write (shim) more custom rules than we ever had before.

This is not the only thing, but something (gut feeling) tells me although the change from 5.2 to 5.3 looks good on paper (even to me!), in practice especially the nullable+required_* combination fells somehow wrong. There were other odd things but this one stood out.

All 20 comments

This behaviour is caused by the fact that if b doesn't exist in the request, it is considered to be null and since b is specified as nullable the validator ignores this field altogether and doesn't return any errors.
Check out the isValidatable method on Illuminate\Validation\Validator to fully understand it.

That said, I can't think of a set of rules which would provide the same behaviour as in 5.2.

One way this would work, if you specify b only as required_if:a,true.
Test cases you provided which are:

['a' => true],
['a' => null],
['b' => null],

would produce following errors:

The b field is required when a is 1.
<passes>
<passes>

If you don't really need the string validation, this would be the way to go.

Thanks for the reply

Check out the isValidatable method on Illuminate\Validation\Validator to fully understand it.

I actually did, that's why I turned here because I realized that the behaviour with the nullable is intrinsically not compatible with required_if in that way.

If you don't really need the string validation, this would be the way to go.

I require this.

Guess I need to come up with another way to fix this. Current options I'm exploring:

  • see if $validator->sometimes() can replace somehow the nullable|required_if-problem
  • extend the built-in Validator and change it's inner working (not preferred)

see if $validator->sometimes() can replace somehow the nullable|required_if-problem

You can fiddle with that, but I think, even if you succeed, the validation rules would be quite unreadable.

I would probably create a custom validation rule. That way you're not dependant on the built-in logic(which might change once again) and you can perfectly describe your use case with this bespoke rule.

I would probably create a custom validation rule. That way you're not dependant on the built-in logic(which might change once again) and you can perfectly describe your use case with this bespoke rule.

Thanks, yes, this sounds like a good idea.

I was thinking about this but actually I'm not sure how to approach this particular case of the dependent fields.

I made a most simplistic case with field b depending on a. In reality, I've to fields (b and c) with their own validation rules and both also having required_if=a,true.

How can I make a custom rule which spans multiple fields? I wrote custom validation rules but I only always see them dealing with a single field.

hmmm if nullable is an acceptable value then I guess the Validator should be ok about it as a value and never checks the required rule, if the value is null and you say it's ok to be null then why would it ever fail the required rule.

I guess using $validator->sometimes('required', function(){ return request('a') }) is what you need, no?

I think my previous were not good enough, here's another which better highlights the problem with the change made for 5.3:

>>> $rules = ['a' => 'boolean', 'b' => 'integer|required_if:a,true'];
=> [
     "a" => "boolean",
     "b" => "integer|required_if:a,true",
   ]
>>> Validator::make(['a' => false, 'b' => null], $rules)->errors()->all()
=> [
     "The b must be an integer.",
   ]

Why would b required to be an integer, if it has a required_if rule.

I guess in 5.2, absent fields and null values were treated in a more similar way, which changed in 5.3.

Perhaps good on one side, but not sure of the other one.

The only economical solution I found so far is to:

  • not use nullable for fields which have any of the required_* rules
  • replace any addition rule within a rule which contains required_* with a custom one, which accepts null too

E.g. if my rule is supposed to be string|required_if:a,true I need to:

  • add my own my_string rule, which works like the built-in string but returns true for null
  • adapt the rule to my_string|required_if:a,true

Since I've quite some required_* based rules, I ended up creating such shims for string, integer, boolean, min, max, date_format, in, after ...

At this point it's arguable if I should just have extended the Validator class itself, but

  • the more I tackle internals of a Laravel class, the more work I may have with future updates
  • these workarounds were only required for rules which contain any of the required_* directives. Although it seems much, 90% of my validation does not include them so it makes sense to leverage the existing Laravel infrastructure as much as possible

This is now a perspective from a mid-sized project I guess, which currently has >1600 tests.

When I implemented the first parts roughly a year ago, on 5.1, the implicit acceptance of null for rules like string felt weird (after all, this is a pure Json API based project), but over time I saw it's sense and I only needed a few custom rules to make it through.

Now after 5.3, only for this update I had to write (shim) more custom rules than we ever had before.

This is not the only thing, but something (gut feeling) tells me although the change from 5.2 to 5.3 looks good on paper (even to me!), in practice especially the nullable+required_* combination fells somehow wrong. There were other odd things but this one stood out.

I agree with @mfn. You may want to re-open this issue as I think it needs to be addressed as well.

When using the required_without validation rule, the documentation states the following:

The field under validation must be present and not empty _only when_ any of the other specified fields are not present.

Note that _only_when_ is emphasized in the above sentence.

Now, when the field is not required because the required_without condition is false, then it means (according to the documentation) that the field _must not_ be present and _may_ be empty. If this is combined with other validation rules (like numeric), it may lead to a condition that states that a field _may_ be empty but _must_ be an number. This means that the field _must_ be a number and intrinsically is not empty.

Using nullable in this case is not an option, as it will defeat the purpose of the required_without rule.

Using sometimes is not an option either, as the field may be present with a null value and will therefore trigger the validation rules on that field.

I agree with @mfn. You may want to re-open this issue as I think it needs to be addressed as well.

I agree.
I closed it because there was not much movement going on and I found an economically viable workaround.

I think your example with required_without seems even better to show the problem of the new Validator design.

@lowerends Can you share a code that should pass but fails or fails but passes? I don't seem to understand the issue here, what are you trying to do?

This passes:

Validator::make(
    ["username" => null],
    ['username' => 'nullable|required_without:email']
);

This fails:

Validator::make(
    ["username" => null],
    ['username' => 'required_without:email']
);

Which is expected, where's the issue?

@themsaid Let's say you want a user to enter at least a username or an email address.

This correctly fails, as I want one of the fields to be required:

Validator::make(
        [
            'username' => null,
            'email' => null
        ],
        [
            'username' => 'required_without:email',
            'email' => 'required_without:username|email',
        ]
    );

This fails as well, as the email needs to be an email field:

Validator::make(
        [
            'username' => 'test',
            'email' => null
        ],
        [
            'username' => 'required_without:email',
            'email' => 'required_without:username|email',
        ]
    );

So in order to solve this now, you need to add nullable, which correctly passes:

Validator::make(
        [
            'username' => 'test',
            'email' => null
        ],
        [
            'username' => 'nullable|required_without:email',
            'email' => 'nullable|required_without:username|email',
        ]
    );

But then the following incorrectly passes again, as I want one of the fields to be required:

Validator::make(
        [
            'username' => null,
            'email' => null
        ],
        [
            'username' => 'nullable|required_without:email',
            'email' => 'nullable|required_without:username|email',
        ]
    );

By introducing the nullable validation rule, there is no way to require at least the username or the email address field without writing custom validation logic. Does this example clarify the issue?

ahhh, now I see. In the original case:

Validator::make(
        [
            'username' => 'test',
            'email' => null
        ],
        [
            'username' => 'required_without:email',
            'email' => 'required_without:username|email',
        ]
    );

The problem is that you expect the email rule not to run if the required_without check fails, so that if the field is not required then we stop validating further rules, right?

But this is actually not possible, the email rule knows nothing about the previous required_without rule result so it'll run even if the field is not required.

The way to go is to use sometimes

$validator->sometimes('email', 'email', function(){
  return ! request('username');
})

This way the email rule won't run unless there's no username given.

So it still requires a custom validation function?

Using sometimes, the following still fails:

Validator::make(
        [
            'username' => 'test',
            'email' => null
        ],
        [
            'username' => 'required_without:email',
            'email' => 'sometimes|required_without:username|email',
        ]
    );

sometimes() is not a custom function, it's built into the Validator for these cases of complex conditions. It's very useful for situations like this.

I'm closing this issue since it's not actually an issue in the core, please check the sometimes() method and give it a go & feel free to ping me if you need any help.

Here's a workaround from #17032, in case it helps anyone else. In this example we want optional_value to be required if some_bool is truthy:

$rules = [
    'optional_value' => 'boolean',
];
$rules['optional_value'] .= $request->input('some_bool') ? '|required' : '|nullable';
$this->validate($request, $rules);

For anyone who's still pulling their hair out with this, you can just create a custom validator class, see gist:

We've just updated from Laravel 5.2 to 5.5 and come across this issue, however I believe I was able to get around it using the bail rule.

Taking the example in first post

"a" => "boolean",
"b" => "string|required_if:a,true",

becomes

"a" => "boolean",
"b" => "bail|required_if:a,true|nullable|string",

My understanding is it'll check if a is set to true first, if so then the field is required and a null (or empty as per docs) value won't work for b. If that passes, it moves on to the next rule which allows a null value (doesn't actually do any validation), this only applies when a is not true as otherwise the rules contradict each other. Finally if it reaches the third rule, it'll check that b is a string.

Note that it'll check if b is a string if it's not null regardless of the value of a, but this replicates how it worked before.

Hope this helps someone!

Edit: In hindsight, I'm not sure the bail rule is even needed, it's more the ordering of the rules. This seems to work for the cases that we have that were no longer working after the update.

For anyone running into this issue still. I solved this using exclude_if

Using the example above

"a" => "boolean",
"b" => "exclude_if:a,false|string|required_if:a,true",

My exact usage
If the is_upgrade checkbox is checked, then I want to require the upgrade price.

'is_upgrade' => 'required|boolean',
'upgrade_price' => 'exclude_if:is_upgrade,false|required_if:is_upgrade,true|numeric',
Was this page helpful?
0 / 5 - 0 ratings

Related issues

PhiloNL picture PhiloNL  Â·  3Comments

RomainSauvaire picture RomainSauvaire  Â·  3Comments

SachinAgarwal1337 picture SachinAgarwal1337  Â·  3Comments

jackmu95 picture jackmu95  Â·  3Comments

shopblocks picture shopblocks  Â·  3Comments