Yesterday, I showed how to register a custom validation rule as a string, but I didn't really explain how the solution works. Today I'm going to dive in and explain how validation takes a string rule and resolves it to a specific class or method containing validation logic. Along the way, I'll include links to relevant bits of source code if you want to follow along.
The framework method (as of Laravel 12) I'm going to focus on today is validateAttribute
.
This method accepts two parameters, the attribute and the rule:
protected function validateAttribute($attribute, $rule)
{
$this->currentRule = $rule;
[$rule, $parameters] = ValidationRuleParser::parse($rule);
// truncated...
}
When dealing with string-based rules, the parse
function takes a string-based rule like max:255
and splits it into the string Max
and a parameters array with one value, 255.
Some rules have no parameters, some have multiple, but the concept is the same.
There is something very subtle here that you may have missed: notice it sets $rule
to Max
and not max
.
Keep that fact in mind as we go through the rest of the code.
What if our rule was a custom rule object, like new PhoneNumber()
?
In that case, it returns the rule object itself and an empty array of parameters.
With that understood, let's look at our next relevant chunk of code in the validateAttribute
function:
if ($rule instanceof RuleContract) {
return $validatable
? $this->validateUsingCustomRule($attribute, $value, $rule)
: null;
}
$method = "validate{$rule}";
if ($validatable && ! $this->$method($attribute, $value, $parameters, $this)) {
$this->addFailure($attribute, $rule, $parameters);
}
If we have a class-based rule, we will call that class directly and return the result.
But if it's a string-based rule, it prefixes the rule with validate
and calls that method on the validator instance.
Here is why it matters that the rule is Max
and not max
.
The validator includes a ValidatesAttributes
trait that has a bunch of methods like validateMax
, validateString
, and so on.
By converting our snake_case
string rule to StudlyCase
, it can call the method directly.
But what happens if you specify a string rule that doesn't have a matching method in this trait?
Let's say you add 'phone_number'
to your validation rules.
It then defers to treating it as a dynamic method via the __call
method on the validator instance.
There it reverses StudlyCase
to snake_case
and strips off the validate
prefix.
But then it looks for an "extension" registered on the validator with a matching name:
if (isset($this->extensions[$rule])) {
return $this->callExtension($rule, $parameters);
}
That "extension" is what we registered in the app service provider yesterday by chaining extend('phone_number', ...)
onto our validator instance.
It was a bit of a journey, but that's how the native string rules work (methods on a giant trait) and how it falls back to the extension system if you registered your own string-based rule.
Tomorrow, we will explain the last piece of the puzzle: how the closure we pass to that extension works.
Here to help,
Joel
P.S. Clearly I'm not afraid to roll up my sleeves and figure out how Laravel works. I can do the same with your code if you ever get stuck and need help.