From df78fe188c53cf29ffae7773e899fd9318113c9a Mon Sep 17 00:00:00 2001 From: Matteo Kloiber Date: Sun, 6 Nov 2016 01:46:17 +0100 Subject: [PATCH 1/5] Added support for instance rules Instance rules are not shared among other validator objects (thus they are not static). This is useful if you have a rule you do not want your other validators to use (because it is very special to this validator). The parameter given to addInstanceRule is identically to addRule: public function addInstanceRule(string $name, callable $callback [, string message]) Instead of appending to the static variables $_rules, and $_ruleMessages, it now appends to $_instanceRules and $_instanceRuleMessages. A new getter for the rules and ruleMessage has also been added: public array getRules() public array getRuleMessages() All existing code has been updated to use getRules(), and getRuleMessages() instead of $_rules, and $_ruleMessages. --- src/Valitron/Validator.php | 78 +++++++++++++++++-- .../Valitron/ValidateAddInstanceRuleTest.php | 56 +++++++++++++ 2 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 tests/Valitron/ValidateAddInstanceRuleTest.php diff --git a/src/Valitron/Validator.php b/src/Valitron/Validator.php index 7c76738..f57cc08 100644 --- a/src/Valitron/Validator.php +++ b/src/Valitron/Validator.php @@ -37,6 +37,21 @@ class Validator */ protected $_labels = array(); + /** + * Contains all rules that are available to the current valitron instance. + * + * @var array + */ + protected $_instanceRules = array(); + + /** + * Contains all rule messages that are available to the current valitron + * instance + * + * @var array + */ + protected $_instanceRuleMessage = array(); + /** * @var string */ @@ -503,7 +518,7 @@ class Validator foreach ($this->validUrlPrefixes as $prefix) { if (strpos($value, $prefix) !== false) { $host = parse_url(strtolower($value), PHP_URL_HOST); - + return checkdnsrr($host, 'A') || checkdnsrr($host, 'AAAA') || checkdnsrr($host, 'CNAME'); } } @@ -921,8 +936,9 @@ class Validator } // Callback is user-specified or assumed method on class - if (isset(static::$_rules[$v['rule']])) { - $callback = static::$_rules[$v['rule']]; + $errors = $this->getRules(); + if (isset($errors[$v['rule']])) { + $callback = $errors[$v['rule']]; } else { $callback = array($this, 'validate' . ucfirst($v['rule'])); } @@ -945,6 +961,26 @@ class Validator return count($this->errors()) === 0; } + /** + * Returns all rule callbacks, the static and instance ones. + * + * @return array + */ + protected function getRules() + { + return array_merge($this->_instanceRules, static::$_rules); + } + + /** + * Returns all rule message, the static and instance ones. + * + * @return array + */ + protected function getRuleMessages() + { + return array_merge($this->_instanceRuleMessage, static::$_ruleMessages); + } + /** * Determine whether a field is being validated by the given rule. * @@ -952,6 +988,7 @@ class Validator * @param string $field The name of the field * @return boolean */ + protected function hasRule($name, $field) { foreach ($this->_validations as $validation) { @@ -965,6 +1002,31 @@ class Validator return false; } + protected static function assertRuleCallback($callback) + { + if (!is_callable($callback)) { + throw new \InvalidArgumentException('Second argument must be a valid callback. Given argument was not callable.'); + } + } + + + /** + * Adds a new validation rule callback that is tied to the current + * instance only. + * + * @param string $name + * @param mixed $callback + * @param string $message + * @throws \InvalidArgumentException + */ + public function addInstanceRule($name, $callback, $message = self::ERROR_DEFAULT) + { + static::assertRuleCallback($callback); + + $this->_instanceRules[$name] = $callback; + $this->_instanceRuleMessage[$name] = $message; + } + /** * Register new validation rule callback * @@ -975,9 +1037,7 @@ class Validator */ public static function addRule($name, $callback, $message = self::ERROR_DEFAULT) { - if (!is_callable($callback)) { - throw new \InvalidArgumentException('Second argument must be a valid callback. Given argument was not callable.'); - } + static::assertRuleCallback($callback); static::$_rules[$name] = $callback; static::$_ruleMessages[$name] = $message; @@ -993,7 +1053,8 @@ class Validator */ public function rule($rule, $fields) { - if (!isset(static::$_rules[$rule])) { + $errors = $this->getRules(); + if (!isset($errors[$rule])) { $ruleMethod = 'validate' . ucfirst($rule); if (!method_exists($this, $ruleMethod)) { throw new \InvalidArgumentException("Rule '" . $rule . "' has not been registered with " . __CLASS__ . "::addRule()."); @@ -1001,7 +1062,8 @@ class Validator } // Ensure rule has an accompanying message - $message = isset(static::$_ruleMessages[$rule]) ? static::$_ruleMessages[$rule] : self::ERROR_DEFAULT; + $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); diff --git a/tests/Valitron/ValidateAddInstanceRuleTest.php b/tests/Valitron/ValidateAddInstanceRuleTest.php new file mode 100644 index 0000000..796902c --- /dev/null +++ b/tests/Valitron/ValidateAddInstanceRuleTest.php @@ -0,0 +1,56 @@ +validate(); + foreach ($v->errors() as $label => $messages) + { + foreach ($messages as $theMessage) + { + $msg .= "\n\t{$label}: {$theMessage}"; + } + } + + $this->assertTrue($v->validate(), $msg); + } + + public function testAddInstanceRule() + { + $v = new Validator(array( + "foo" => "bar", + "fuzz" => "bazz", + )); + + $v->addInstanceRule("fooRule", function($field, $value) + { + return $field !== "foo" || $value !== "barz"; + }); + + Validator::addRule("fuzzerRule", function($field, $value) + { + return $field !== "fuzz" || $value === "bazz"; + }); + + $v->rule("required", array("foo", "fuzz")); + $v->rule("fuzzerRule", "fuzz"); + $v->rule("fooRule", "foo"); + + + $this->assertValid($v); + } + + public function testAddInstanceRuleFail() + { + $v = new Validator(array("foo" => "bar")); + $v->addInstanceRule("fooRule", function($field) + { + return $field === "for"; + }); + $v->rule("fooRule", "foo"); + $this->assertFalse($v->validate()); + } +} From 7a5fd669906258d66a4657d543dbc84a7f120c25 Mon Sep 17 00:00:00 2001 From: Matteo Kloiber Date: Sun, 6 Nov 2016 01:46:25 +0100 Subject: [PATCH 2/5] 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); + } } From 74a882969ecaa4e8d4deb1c2259274efa68ac0a8 Mon Sep 17 00:00:00 2001 From: Matteo Kloiber Date: Sun, 6 Nov 2016 01:46:28 +0100 Subject: [PATCH 3/5] Indent fixed --- src/Valitron/Validator.php | 64 +++---- .../Valitron/ValidateAddInstanceRuleTest.php | 158 +++++++++--------- 2 files changed, 111 insertions(+), 111 deletions(-) diff --git a/src/Valitron/Validator.php b/src/Valitron/Validator.php index 54f6bf5..20dc88e 100644 --- a/src/Valitron/Validator.php +++ b/src/Valitron/Validator.php @@ -1037,10 +1037,10 @@ class Validator */ public static function addRule($name, $callback, $message = null) { - if ($message === null) - { - $message = static::ERROR_DEFAULT; - } + if ($message === null) + { + $message = static::ERROR_DEFAULT; + } static::assertRuleCallback($callback); @@ -1048,23 +1048,23 @@ class Validator static::$_ruleMessages[$name] = $message; } - public function getUniqueRuleName($fields) - { - if (is_array($fields)) - { - $fields = implode("_", $fields); - } + 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); - } + $orgName = "{$fields}_rule"; + $name = $orgName; + $rules = $this->getRules(); + while (isset($rules[$name])) + { + $name = $orgName . "_" . rand(0, 10000); + } - return $name; - } + return $name; + } /** * Convenience method to add a single validation rule @@ -1079,19 +1079,19 @@ class Validator // 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; - } - + // 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); diff --git a/tests/Valitron/ValidateAddInstanceRuleTest.php b/tests/Valitron/ValidateAddInstanceRuleTest.php index c1f759e..cc10d0b 100644 --- a/tests/Valitron/ValidateAddInstanceRuleTest.php +++ b/tests/Valitron/ValidateAddInstanceRuleTest.php @@ -3,99 +3,99 @@ use Valitron\Validator; class ValidateAddInstanceRuleTest extends BaseTestCase { - protected function assertValid($v) - { - $msg = "\tErrors:\n"; - $status = $v->validate(); - foreach ($v->errors() as $label => $messages) - { - foreach ($messages as $theMessage) - { - $msg .= "\n\t{$label}: {$theMessage}"; - } - } + protected function assertValid($v) + { + $msg = "\tErrors:\n"; + $status = $v->validate(); + foreach ($v->errors() as $label => $messages) + { + foreach ($messages as $theMessage) + { + $msg .= "\n\t{$label}: {$theMessage}"; + } + } - $this->assertTrue($v->validate(), $msg); - } + $this->assertTrue($v->validate(), $msg); + } - public function testAddInstanceRule() - { - $v = new Validator(array( - "foo" => "bar", - "fuzz" => "bazz", - )); + public function testAddInstanceRule() + { + $v = new Validator(array( + "foo" => "bar", + "fuzz" => "bazz", + )); - $v->addInstanceRule("fooRule", function($field, $value) - { - return $field !== "foo" || $value !== "barz"; - }); + $v->addInstanceRule("fooRule", function($field, $value) + { + return $field !== "foo" || $value !== "barz"; + }); - Validator::addRule("fuzzerRule", function($field, $value) - { - return $field !== "fuzz" || $value === "bazz"; - }); + Validator::addRule("fuzzerRule", function($field, $value) + { + return $field !== "fuzz" || $value === "bazz"; + }); - $v->rule("required", array("foo", "fuzz")); - $v->rule("fuzzerRule", "fuzz"); - $v->rule("fooRule", "foo"); + $v->rule("required", array("foo", "fuzz")); + $v->rule("fuzzerRule", "fuzz"); + $v->rule("fooRule", "foo"); - $this->assertValid($v); - } + $this->assertValid($v); + } - public function testAddInstanceRuleFail() - { - $v = new Validator(array("foo" => "bar")); - $v->addInstanceRule("fooRule", function($field) - { - return $field === "for"; - }); - $v->rule("fooRule", "foo"); - $this->assertFalse($v->validate()); - } + public function testAddInstanceRuleFail() + { + $v = new Validator(array("foo" => "bar")); + $v->addInstanceRule("fooRule", function($field) + { + return $field === "for"; + }); + $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"); + public function testAddAddRuleWithCallback() + { + $v = new Validator(array("foo" => "bar")); + $v->rule(function($field, $value) { + return $field === "foo" && $value === "bar"; + }, "foo"); - $this->assertValid($v); - } + $this->assertValid($v); + } - public function testAddAddRuleWithCallbackFail() - { - $v = new Validator(array("foo" => "baz")); - $v->rule(function($field, $value) { - return $field === "foo" && $value === "bar"; - }, "foo"); + public function testAddAddRuleWithCallbackFail() + { + $v = new Validator(array("foo" => "baz")); + $v->rule(function($field, $value) { + return $field === "foo" && $value === "bar"; + }, "foo"); - $this->assertFalse($v->validate()); - } + $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"); + 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()); + $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]); - } + $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")); + 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); - } + $v->addInstanceRule("foo_rule", function() {}); + $u = $v->getUniqueRuleName("foo"); + $this->assertRegExp("/^foo_rule_[0-9]{1,5}$/", $u); + } } From ebbeca46af510934dcb9c68840865b1d558b8437 Mon Sep 17 00:00:00 2001 From: Matteo Kloiber Date: Sun, 6 Nov 2016 01:46:34 +0100 Subject: [PATCH 4/5] Updated README.md --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index c2f7195..18ee53c 100644 --- a/README.md +++ b/README.md @@ -188,6 +188,29 @@ Valitron\Validator::addRule('alwaysFail', function($field, $value, array $params }, 'Everything you do is wrong. You fail.'); ``` +You can also use one-off rules that are only valid for the specified +fields. + +```php +$v = new Valitron\Validator(array("foo" => "bar")); +$v->rule(function($field, $value, $params, $fields) { + return true; +}, "foo")->message("{field} failed..."); +``` + +This is useful because such rules can have access to variables +defined in the scope where the `Validator` lives. The Closure's +signature is identical to `Validator::addRule` callback's +signature. + +If you wish to add your own rules that are not static (i.e., +your rule is not static and available to call `Validator` +instances), you need to use `Validator::addInstanceRule`. +This rule will take the same parameters as +`Validator::addRule` but it has to be called on a `Validator` +instance. + + ## Alternate syntax for adding rules As the number of rules grows, you may prefer the alternate syntax From a6be7e2dd00ac752611d99ab74b48d1c1cc24058 Mon Sep 17 00:00:00 2001 From: Matteo Kloiber Date: Sun, 6 Nov 2016 01:40:05 +0100 Subject: [PATCH 5/5] Validator::rule now accepts any kind of callback This has been achieved by making sure that the rule is neither a "native" Validator rule (e.g. there is a validate{$CamleCaseName} method) nor it has been registered. If both conditions are met, the rule (even if it is a string) will be accepted and used. Tests for this case have also been added. --- src/Valitron/Validator.php | 22 ++++++++++++++----- .../Valitron/ValidateAddInstanceRuleTest.php | 19 ++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/Valitron/Validator.php b/src/Valitron/Validator.php index 20dc88e..4a1a49a 100644 --- a/src/Valitron/Validator.php +++ b/src/Valitron/Validator.php @@ -1066,6 +1066,20 @@ class Validator return $name; } + /** + * Returns true if either a valdiator with the given name has been + * registered or there is a default validator by that name. + * + * @param string $name + * @return bool + */ + public function hasValidator($name) + { + $rules = $this->getRules(); + return method_exists($this, "validate" . ucfirst($name)) + || isset($rules[$name]); + } + /** * Convenience method to add a single validation rule * @@ -1079,12 +1093,8 @@ class Validator // 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)) + if (is_callable($rule) + && !(is_string($rule) && $this->hasValidator($rule))) { $name = $this->getUniqueRuleName($fields); $msg = isset($params[0]) ? $params[0] : null; diff --git a/tests/Valitron/ValidateAddInstanceRuleTest.php b/tests/Valitron/ValidateAddInstanceRuleTest.php index cc10d0b..ddefac3 100644 --- a/tests/Valitron/ValidateAddInstanceRuleTest.php +++ b/tests/Valitron/ValidateAddInstanceRuleTest.php @@ -1,6 +1,11 @@ assertEquals("Foo test error message", $errors["foo"][0]); } + public function testAddRuleWithNamedCallbackOk() + { + $v = new Validator(array("bar" => "foo")); + $v->rule("callbackTestFunction", "bar"); + $this->assertFalse($v->validate()); + } + + public function testAddRuleWithNamedCallbackErr() + { + $v = new Validator(array("foo" => "bar")); + $v->rule("callbackTestFunction", "foo"); + $this->assertTrue($v->validate()); + } + public function testUniqueRuleName() { $v = new Validator(array());