From 7a5fd669906258d66a4657d543dbc84a7f120c25 Mon Sep 17 00:00:00 2001 From: Matteo Kloiber Date: Sun, 6 Nov 2016 01:46:25 +0100 Subject: [PATCH] Validator::rule now supports closures Validator::rule now allows the rule to be a closure. This allows us to add an anonymous rule that is only valid for the specified field(s): $someObject = new MyObject(); $v->rule(function($field, $value) use ($someObject) return $value === $someObject->someState(); }, ["shared_secret", "other"]) ->label("invalid state!"); This is especially useful if you have either a) have a rule which you are not likely to be reusing or b) you need access to some object which cannot be easily connstructed (because it requires IO access, etc). Either way, this is really only convenience method because it saves you a call to Validator::addInstanceRule. Internally, it does exactly that: it 1) determines a valid rule name, 2) calls addInstanceRule() with the name determined in 1) and the Closure, and then 3) replaces the $rule parameter (which is the Closure object) by the name of the rule. 1) is determined like this: if multiple parameters were given, they are joined together into one string glued with a underscore. '_rule' is then appended so that the name becomes all_params_rule. If the name is already used by any other rule, it will append up to 5 pseudo-random digits to the rule. In the unlikely event that this rule is also used, step 1) will be repeated until a unique rule name is found. --- src/Valitron/Validator.php | 48 ++++++++++++++++--- .../Valitron/ValidateAddInstanceRuleTest.php | 47 +++++++++++++++++- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/src/Valitron/Validator.php b/src/Valitron/Validator.php index f57cc08..54f6bf5 100644 --- a/src/Valitron/Validator.php +++ b/src/Valitron/Validator.php @@ -1019,7 +1019,7 @@ class Validator * @param string $message * @throws \InvalidArgumentException */ - public function addInstanceRule($name, $callback, $message = self::ERROR_DEFAULT) + public function addInstanceRule($name, $callback, $message = null) { static::assertRuleCallback($callback); @@ -1035,24 +1035,63 @@ class Validator * @param string $message * @throws \InvalidArgumentException */ - public static function addRule($name, $callback, $message = self::ERROR_DEFAULT) + public static function addRule($name, $callback, $message = null) { + if ($message === null) + { + $message = static::ERROR_DEFAULT; + } + static::assertRuleCallback($callback); static::$_rules[$name] = $callback; static::$_ruleMessages[$name] = $message; } + public function getUniqueRuleName($fields) + { + if (is_array($fields)) + { + $fields = implode("_", $fields); + } + + $orgName = "{$fields}_rule"; + $name = $orgName; + $rules = $this->getRules(); + while (isset($rules[$name])) + { + $name = $orgName . "_" . rand(0, 10000); + } + + return $name; + } + /** * Convenience method to add a single validation rule * - * @param string $rule + * @param string|callback $rule * @param array $fields * @return $this * @throws \InvalidArgumentException */ public function rule($rule, $fields) { + // Get any other arguments passed to function + $params = array_slice(func_get_args(), 2); + + // Note: we cannot use is_callable here since max, int, and many + // other string can also be callables but aren't really rule callbacks. + // + // If a closure is used, we can be sure that the user actually + // wants $rule to be a custom rule check. + if (is_object($rule) && ($rule instanceof \Closure)) + { + $name = $this->getUniqueRuleName($fields); + $msg = isset($params[0]) ? $params[0] : null; + $this->addInstanceRule($name, $rule, $msg); + $rule = $name; + } + $errors = $this->getRules(); if (!isset($errors[$rule])) { $ruleMethod = 'validate' . ucfirst($rule); @@ -1065,9 +1104,6 @@ class Validator $msgs = $this->getRuleMessages(); $message = isset($msgs[$rule]) ? $msgs[$rule] : self::ERROR_DEFAULT; - // Get any other arguments passed to function - $params = array_slice(func_get_args(), 2); - $this->_validations[] = array( 'rule' => $rule, 'fields' => (array) $fields, diff --git a/tests/Valitron/ValidateAddInstanceRuleTest.php b/tests/Valitron/ValidateAddInstanceRuleTest.php index 796902c..c1f759e 100644 --- a/tests/Valitron/ValidateAddInstanceRuleTest.php +++ b/tests/Valitron/ValidateAddInstanceRuleTest.php @@ -39,7 +39,6 @@ class ValidateAddInstanceRuleTest extends BaseTestCase $v->rule("fuzzerRule", "fuzz"); $v->rule("fooRule", "foo"); - $this->assertValid($v); } @@ -53,4 +52,50 @@ class ValidateAddInstanceRuleTest extends BaseTestCase $v->rule("fooRule", "foo"); $this->assertFalse($v->validate()); } + + public function testAddAddRuleWithCallback() + { + $v = new Validator(array("foo" => "bar")); + $v->rule(function($field, $value) { + return $field === "foo" && $value === "bar"; + }, "foo"); + + $this->assertValid($v); + } + + public function testAddAddRuleWithCallbackFail() + { + $v = new Validator(array("foo" => "baz")); + $v->rule(function($field, $value) { + return $field === "foo" && $value === "bar"; + }, "foo"); + + $this->assertFalse($v->validate()); + } + + public function testAddAddRuleWithCallbackFailMessage() + { + $v = new Validator(array("foo" => "baz")); + $v->rule(function($field, $value) { + return $field === "foo" && $value === "bar"; + }, "foo", "test error message"); + + $this->assertFalse($v->validate()); + $errors = $v->errors(); + $this->assertArrayHasKey("foo", $errors); + $this->assertCount(1, $errors["foo"]); + $this->assertEquals("Foo test error message", $errors["foo"][0]); + } + + public function testUniqueRuleName() + { + $v = new Validator(array()); + $args = array("foo", "bar"); + $this->assertEquals("foo_bar_rule", $v->getUniqueRuleName($args)); + $this->assertEquals("foo_rule", $v->getUniqueRuleName("foo")); + + $v->addInstanceRule("foo_rule", function() {}); + $u = $v->getUniqueRuleName("foo"); + $this->assertRegExp("/^foo_rule_[0-9]{1,5}$/", $u); + } }