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 diff --git a/src/Valitron/Validator.php b/src/Valitron/Validator.php index 7c76738..4a1a49a 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 = null) + { + static::assertRuleCallback($callback); + + $this->_instanceRules[$name] = $callback; + $this->_instanceRuleMessage[$name] = $message; + } + /** * Register new validation rule callback * @@ -973,27 +1035,75 @@ 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 (!is_callable($callback)) { - throw new \InvalidArgumentException('Second argument must be a valid callback. Given argument was not callable.'); + 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; + } + + /** + * 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 * - * @param string $rule + * @param string|callback $rule * @param array $fields * @return $this * @throws \InvalidArgumentException */ public function rule($rule, $fields) { - if (!isset(static::$_rules[$rule])) { + // Get any other arguments passed to function + $params = array_slice(func_get_args(), 2); + + if (is_callable($rule) + && !(is_string($rule) && $this->hasValidator($rule))) + { + $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); if (!method_exists($this, $ruleMethod)) { throw new \InvalidArgumentException("Rule '" . $rule . "' has not been registered with " . __CLASS__ . "::addRule()."); @@ -1001,10 +1111,8 @@ class Validator } // Ensure rule has an accompanying message - $message = isset(static::$_ruleMessages[$rule]) ? static::$_ruleMessages[$rule] : self::ERROR_DEFAULT; - - // Get any other arguments passed to function - $params = array_slice(func_get_args(), 2); + $msgs = $this->getRuleMessages(); + $message = isset($msgs[$rule]) ? $msgs[$rule] : self::ERROR_DEFAULT; $this->_validations[] = array( 'rule' => $rule, diff --git a/tests/Valitron/ValidateAddInstanceRuleTest.php b/tests/Valitron/ValidateAddInstanceRuleTest.php new file mode 100644 index 0000000..ddefac3 --- /dev/null +++ b/tests/Valitron/ValidateAddInstanceRuleTest.php @@ -0,0 +1,120 @@ +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()); + } + + 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 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()); + $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); + } +}