Skip to content

Commit

Permalink
[5.5] Support custom validation rule objects. (#19155)
Browse files Browse the repository at this point in the history
* Support custom validation rule objects.

This is a slightly different approach to use custom validation objects
compared to the current Validator::extend approach. With this addition,
you can define an object that implements
Illuminate\Validation\ValidationRule and then include it in your
validation rules like so: ‘name’ => [new ValidName].

This provides a quick way to define custom validation rules without
needing to call Validator::extend at all.

* Apply fixes from StyleCI (#19156)

* Remove unused property.

* support implicit validation rules

* Apply fixes from StyleCI (#19158)

* move contracts

* Apply fixes from StyleCI (#19159)
  • Loading branch information
taylorotwell authored May 11, 2017
1 parent 8adbaa7 commit 6704b2a
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 6 deletions.
8 changes: 8 additions & 0 deletions src/Illuminate/Contracts/Validation/ImplicitRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Illuminate\Contracts\Validation;

interface ImplicitRule extends Rule
{
//
}
22 changes: 22 additions & 0 deletions src/Illuminate/Contracts/Validation/Rule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Illuminate\Contracts\Validation;

interface Rule
{
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value);

/**
* Get the validation error message.
*
* @return string
*/
public function message();
}
70 changes: 70 additions & 0 deletions src/Illuminate/Validation/ClosureValidationRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Illuminate\Validation;

use Illuminate\Contracts\Validation\Rule as RuleContract;

class ClosureValidationRule implements RuleContract
{
/**
* The callback that validates the attribute.
*
* @var \Closure
*/
public $callback;

/**
* Indicates if the validation callback failed.
*
* @var bool
*/
public $failed = false;

/**
* The validation error message.
*
* @var string|null
*/
public $message;

/**
* Create a new Closure based validation rule.
*
* @param \Closure $callback
* @return void
*/
public function __construct($callback)
{
$this->callback = $callback;
}

/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
$this->failed = false;

$this->callback->__invoke($attribute, $value, function ($message) {
$this->failed = true;

$this->message = $message;
});

return ! $this->failed;
}

/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return $this->message;
}
}
11 changes: 11 additions & 0 deletions src/Illuminate/Validation/ValidationRuleParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace Illuminate\Validation;

use Closure;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Exists;
use Illuminate\Validation\Rules\Unique;
use Illuminate\Contracts\Validation\Rule as RuleContract;

class ValidationRuleParser
{
Expand Down Expand Up @@ -98,7 +100,12 @@ protected function explodeExplicitRule($rule)
*/
protected function prepareRule($rule)
{
if ($rule instanceof Closure) {
$rule = new ClosureValidationRule($rule);
}

if (! is_object($rule) ||
$rule instanceof RuleContract ||
($rule instanceof Exists && $rule->queryCallbacks()) ||
($rule instanceof Unique && $rule->queryCallbacks())) {
return $rule;
Expand Down Expand Up @@ -184,6 +191,10 @@ protected function mergeRulesForAttribute($results, $attribute, $rules)
*/
public static function parse($rules)
{
if ($rules instanceof RuleContract) {
return [$rules, []];
}

if (is_array($rules)) {
$rules = static::parseArrayRule($rules);
} else {
Expand Down
39 changes: 33 additions & 6 deletions src/Illuminate/Validation/Validator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
use Illuminate\Support\MessageBag;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Translation\Translator;
use Illuminate\Contracts\Validation\ImplicitRule;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Illuminate\Contracts\Validation\Rule as RuleContract;
use Illuminate\Contracts\Validation\Validator as ValidatorContract;

class Validator implements ValidatorContract
Expand Down Expand Up @@ -333,6 +335,10 @@ protected function validateAttribute($attribute, $rule)
// attribute is invalid and we will add a failure message for this failing attribute.
$validatable = $this->isValidatable($rule, $attribute, $value);

if ($validatable && $rule instanceof RuleContract) {
return $this->validateUsingCustomRule($attribute, $value, $rule);
}

$method = "validate{$rule}";

if ($validatable && ! $this->$method($attribute, $value, $parameters, $this)) {
Expand Down Expand Up @@ -408,7 +414,7 @@ protected function replaceAsterisksInParameters(array $parameters, array $keys)
/**
* Determine if the attribute is validatable.
*
* @param string $rule
* @param object|string $rule
* @param string $attribute
* @param mixed $value
* @return bool
Expand All @@ -424,7 +430,7 @@ protected function isValidatable($rule, $attribute, $value)
/**
* Determine if the field is present, or the rule implies required.
*
* @param string $rule
* @param object|string $rule
* @param string $attribute
* @param mixed $value
* @return bool
Expand All @@ -435,18 +441,20 @@ protected function presentOrRuleIsImplicit($rule, $attribute, $value)
return $this->isImplicit($rule);
}

return $this->validatePresent($attribute, $value) || $this->isImplicit($rule);
return $this->validatePresent($attribute, $value) ||
$this->isImplicit($rule);
}

/**
* Determine if a given rule implies the attribute is required.
*
* @param string $rule
* @param object|string $rule
* @return bool
*/
protected function isImplicit($rule)
{
return in_array($rule, $this->implicitRules);
return $rule instanceof ImplicitRule ||
in_array($rule, $this->implicitRules);
}

/**
Expand Down Expand Up @@ -476,7 +484,7 @@ protected function passesOptionalCheck($attribute)
*/
protected function isNotNullIfMarkedAsNullable($rule, $attribute)
{
if (in_array($rule, $this->implicitRules) || ! $this->hasRule($attribute, ['Nullable'])) {
if ($this->isImplicit($rule) || ! $this->hasRule($attribute, ['Nullable'])) {
return true;
}

Expand All @@ -497,6 +505,25 @@ protected function hasNotFailedPreviousRuleIfPresenceRule($rule, $attribute)
return in_array($rule, ['Unique', 'Exists']) ? ! $this->messages->has($attribute) : true;
}

/**
* Validate an attribute using a custom rule object.
*
* @param string $attribute
* @param mixed $value
* @param \Illuminate\Contracts\Validation\Rule $rule
* @return void
*/
protected function validateUsingCustomRule($attribute, $value, $rule)
{
if (! $rule->passes($attribute, $value)) {
$this->failedRules[$attribute][get_class($rule)] = [];

$this->messages->add($attribute, $this->makeReplacements(
$rule->message(), $attribute, get_class($rule), []
));
}
}

/**
* Check if we should stop further validations on a given attribute.
*
Expand Down
136 changes: 136 additions & 0 deletions tests/Validation/ValidationValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
use Illuminate\Validation\Validator;
use Illuminate\Validation\Rules\Exists;
use Illuminate\Validation\Rules\Unique;
use Illuminate\Contracts\Validation\Rule;
use Symfony\Component\HttpFoundation\File\File;
use Illuminate\Contracts\Validation\ImplicitRule;

class ValidationValidatorTest extends TestCase
{
Expand Down Expand Up @@ -3395,6 +3397,140 @@ public function testFileUploads()
$this->assertFalse($v->passes());
}

public function testCustomValidationObject()
{
// Test passing case...
$v = new Validator(
$this->getIlluminateArrayTranslator(),
['name' => 'taylor'],
['name' => new class implements Rule {
public function passes($attribute, $value)
{
return $value === 'taylor';
}

public function message()
{
return ':attribute must be taylor';
}
}]
);

$this->assertTrue($v->passes());

// Test failing case...
$v = new Validator(
$this->getIlluminateArrayTranslator(),
['name' => 'adam'],
['name' => [new class implements Rule {
public function passes($attribute, $value)
{
return $value === 'taylor';
}

public function message()
{
return ':attribute must be taylor';
}
}]]
);

$this->assertTrue($v->fails());
$this->assertEquals('name must be taylor', $v->errors()->all()[0]);

// Test passing case with Closure...
$v = new Validator(
$this->getIlluminateArrayTranslator(),
['name' => 'taylor'],
['name.*' => function ($attribute, $value, $fail) {
if ($value !== 'taylor') {
$fail(':attribute was '.$value.' instead of taylor');
}
}]
);

$this->assertTrue($v->passes());

// Test failing case with Closure...
$v = new Validator(
$this->getIlluminateArrayTranslator(),
['name' => 'adam'],
['name' => function ($attribute, $value, $fail) {
if ($value !== 'taylor') {
$fail(':attribute was '.$value.' instead of taylor');
}
}]
);

$this->assertTrue($v->fails());
$this->assertEquals('name was adam instead of taylor', $v->errors()->all()[0]);

// Test complex failing case...
$v = new Validator(
$this->getIlluminateArrayTranslator(),
['name' => 'taylor', 'states' => ['AR', 'TX'], 'number' => 9],
[
'states.*' => new class implements Rule {
public function passes($attribute, $value)
{
return in_array($value, ['AK', 'HI']);
}

public function message()
{
return ':attribute must be AR or TX';
}
},
'name' => function ($attribute, $value, $fail) {
if ($value !== 'taylor') {
$fail(':attribute must be taylor');
}
},
'number' => [
'required',
'integer',
function ($attribute, $value, $fail) {
if ($value % 4 !== 0) {
$fail(':attribute must be divisible by 4');
}
},
],
]
);

$this->assertFalse($v->passes());
$this->assertEquals('states.0 must be AR or TX', $v->errors()->get('states.0')[0]);
$this->assertEquals('states.1 must be AR or TX', $v->errors()->get('states.1')[0]);
$this->assertEquals('number must be divisible by 4', $v->errors()->get('number')[0]);
}

public function testImplicitCustomValidationObjects()
{
// Test passing case...
$v = new Validator(
$this->getIlluminateArrayTranslator(),
['name' => ''],
['name' => $rule = new class implements ImplicitRule {
public $called = false;

public function passes($attribute, $value)
{
$this->called = true;

return true;
}

public function message()
{
return 'message';
}
}]
);

$this->assertTrue($v->passes());
$this->assertTrue($rule->called);
}

protected function getTranslator()
{
return m::mock('Illuminate\Contracts\Translation\Translator');
Expand Down

0 comments on commit 6704b2a

Please sign in to comment.