├── .gitignore ├── .travis.yml ├── tests ├── bootstrap.php ├── rulesTest.php └── ValidatorTest.php ├── composer.json ├── phpunit.xml.dist ├── LICENSE ├── CHANGELOG.md ├── README.md └── src ├── rules.php └── Validator.php /.gitignore: -------------------------------------------------------------------------------- 1 | *.lock 2 | *.sublime-* 3 | vendor 4 | test.php -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | - 5.6 6 | install: 7 | - composer install -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | =5.4.0", 10 | "ext-mbstring":"*" 11 | }, 12 | "suggest": { 13 | "ext-intl": "Intl extension is useful for validating formatted numbers" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Form\\": "src/" 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | 22 | ./src/ 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Rémi Lanvin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [Unreleased] 4 | 5 | - `each` validator is now transparent in the error array 6 | 7 | ## [2.0.1] - 2016-04-11 8 | 9 | - Fix a PHP fatal error when `is_array` is used with `each` 10 | 11 | ## [2.0] - 2016-03-28 12 | 13 | - Drop support for PHP 5.3 (minimum version is now PHP 5.4) 14 | - Namespacing: `Form` is now `Form\Validator` 15 | - `Validator` static class is replaced by the namespace `Form\Rule` 16 | - Using PSR-4 autoloader 17 | - Adding syntax to access nested fields directly in `getRules`, `getErrors` and `getValues` (and similar `has*` methods) 18 | - `setRules` now accepts another validator 19 | 20 | ### Rule changes 21 | 22 | - The rule `bool` now takes an optionnal parameter to determine type conversion 23 | - The rule `date` now takes an optionnal parameter to determine the date format 24 | - The rule `trim` now always return true 25 | - New rule: `datetime` 26 | - New rule: `length` 27 | - New rules: `integer`, `decimal`, `intl_integer` and `intl_decimal` 28 | - New rules: `ip`, `ipv4`, `ipv6` 29 | - New rules: `between`, `min`, `max` 30 | - Remove rules: `min_value` (use `min`) and `max_value` (use `max`) 31 | 32 | ## [1.1.0] - 2016-03-25 33 | 34 | - `each` now cast to array in all circumstances 35 | - `each` now accepts a subform 36 | - error array for `each` validator now includes the offset 37 | 38 | ## 1.0.0 - 2015-10-12 39 | 40 | ### Added 41 | 42 | - First release, everything before that was unversioned (`dev-master` was used). 43 | 44 | [Unreleased]: https://github.com/rlanvin/php-form/compare/v2.0.1...HEAD 45 | [2.0.1]: https://github.com/rlanvin/php-form/compare/v2.0.0...v2.0.1 46 | [2.0]: https://github.com/rlanvin/php-form/compare/v1.1.0...v2.0.0 47 | [1.1.0]: https://github.com/rlanvin/php-form/compare/v1.0.0...v1.1.0 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Form 2 | 3 | Lightweight form validation library for PHP, with a concise syntax and powerful use of closures. It can validate traditional form submissions as well as API requests. 4 | 5 | [![Build Status](https://travis-ci.org/rlanvin/php-form.svg?branch=master)](https://travis-ci.org/rlanvin/php-form) 6 | [![Latest Stable Version](https://poser.pugx.org/rlanvin/php-form/v/stable)](https://packagist.org/packages/rlanvin/php-form) 7 | 8 | ## Basic example 9 | 10 | ```php 11 | // create the form with rules 12 | $form = new Form\Validator([ 13 | 'name' => ['required', 'trim', 'max_length' => 255], 14 | 'email' => ['required', 'email'] 15 | ]); 16 | 17 | if ( $form->validate($_POST) ) { 18 | // $_POST data is valid 19 | $form->getValues(); // returns an array of sanitized values 20 | } 21 | else { 22 | // $_POST data is not valid 23 | $form->getErrors(); // contains the errors 24 | $form->getValues(); // can be used to repopulate the form 25 | } 26 | ``` 27 | 28 | Complete doc is available in [the wiki](https://github.com/rlanvin/php-form/wiki). 29 | 30 | ## Requirements 31 | 32 | - PHP >= 5.4 33 | - [`mbstring` extension](http://www.php.net/manual/en/book.mbstring.php) 34 | - [`intl` extension](http://php.net/manual/en/book.intl.php) recommended to validate numbers in local format 35 | 36 | If you are stuck with PHP 5.3, you may still use [version 1.1](https://github.com/rlanvin/php-form/releases/tag/v1.1.0). 37 | 38 | ## Installation 39 | 40 | The recommended way is to install the lib [through Composer](http://getcomposer.org/). 41 | 42 | Just add this to your `composer.json` file: 43 | 44 | ```JSON 45 | { 46 | "require": { 47 | "rlanvin/php-form": "2.*" 48 | } 49 | } 50 | ``` 51 | 52 | Then run `composer install` or `composer update`. 53 | 54 | Now you can use the autoloader, and you will have access to the library: 55 | 56 | ```php 57 | 9 | * @link https://github.com/rlanvin/php-form 10 | */ 11 | 12 | namespace Form\Rule; 13 | 14 | /** 15 | * Each rule is a method that returns a boolean indicating whether the 16 | * value provided is valid (true) or invalid (false). 17 | * 18 | * Rules can optionally alter the value provided, acting as a sanitizer. 19 | * 20 | * Rules can take one additional parameter. To pass the parameter is the 21 | * Form context, use the following syntax: 22 | * ['trim']; // no option 23 | * ['trim' => '/']; // will trim "/" 24 | * 25 | */ 26 | 27 | /////////////////////////////////////////////////////////////////////////////// 28 | // Core type rules 29 | 30 | /** 31 | * Test if the value is empty 32 | */ 33 | function is_empty($value) 34 | { 35 | return $value === null || (\is_array($value) && empty($value)) || (\is_string($value) && \trim($value) === ''); 36 | } 37 | 38 | /** 39 | * Test that the value is an array 40 | */ 41 | function is_array($value) 42 | { 43 | return \is_array($value); 44 | } 45 | 46 | /** 47 | * Test that the value is a string 48 | */ 49 | function is_string($value) 50 | { 51 | return \is_string($value); 52 | } 53 | 54 | /////////////////////////////////////////////////////////////////////////////// 55 | // Type rules 56 | 57 | /** 58 | * Check that the input value is considered a boolean 59 | * and alter the value if necessary. 60 | * 61 | * The type of the value is preserved. 62 | * - strings (such as 'true' or 'y') will become '0' or '1' 63 | * - integers and bools are not modified 64 | * 65 | * This is made to accomodate PDO/MySQL that don't handle boolean directly. 66 | * A SELECT statement will return '0' or '1' as strings too, so this way 67 | * we're consistant accross the board. This is also made to avoid bugs with 68 | * in_array() and the like, due to PHP's type conversion. 69 | * 70 | * If you want to force the type to something else, use the second parameter. 71 | * 72 | * @param $value 73 | * @param $sanitize mixed - true => alter value and keep type (default) 74 | * - 'bool', 'string' or 'int' => alter and cast to this type 75 | * - false => do not alter the value 76 | */ 77 | function bool(&$value, $sanitize = true) 78 | { 79 | $true_values = array('true', 't', 'yes', 'y', 'on', '1', 1, true); 80 | $false_values = array('false', 'f', 'no', 'n', 'off', '0', 0, false); 81 | 82 | $ret = false; 83 | 84 | if ( $sanitize === true ) { 85 | $sanitize = \gettype($value); 86 | } 87 | 88 | // see http://stackoverflow.com/questions/13846769/php-in-array-0-value 89 | if ( \in_array($value, $true_values, true) ) { 90 | $value = $sanitize ? true : $value; 91 | $ret = true; 92 | } 93 | elseif ( \in_array($value, $false_values, true) ) { 94 | $value = $sanitize ? false : $value; 95 | $ret = true; 96 | } 97 | 98 | if ( $ret && $sanitize ) { 99 | switch ( $sanitize ) { 100 | case 'int': 101 | case 'integer': 102 | $value = (int) $value; 103 | break; 104 | 105 | case 'string': 106 | $value = $value ? '1' : '0'; 107 | break; 108 | 109 | case 'bool': 110 | case 'boolean': 111 | $value = (bool) $value; 112 | break; 113 | 114 | default: 115 | throw new \InvalidArgumentException("Cannot cast the value to type $sanitize"); 116 | } 117 | } 118 | 119 | return $ret; 120 | } 121 | 122 | /** 123 | * Check that the input is a valid date, optionally of a given format 124 | * 125 | * @see http://www.php.net/strtotime 126 | */ 127 | function date($value, $format = 'Y-m-d') 128 | { 129 | if ( ! \is_string($value) ) { 130 | return false; 131 | } 132 | 133 | if ( $format ) { 134 | $ret = \DateTime::createFromFormat($format, $value); 135 | if ( $ret ) { 136 | $errors = \DateTime::getLastErrors(); 137 | if (!empty($errors['warning_count'])) { 138 | $ret = false; 139 | } 140 | } 141 | } 142 | else { 143 | // validate anything, not really recommended 144 | try { 145 | $ret = new \DateTime($value); 146 | } catch ( \Exception $e ) { 147 | $ret = false; 148 | } 149 | } 150 | 151 | return $ret !== false; 152 | } 153 | 154 | function datetime($value) 155 | { 156 | return date($value, 'Y-m-d H:i:s'); 157 | } 158 | 159 | function time($value) 160 | { 161 | return date($value, 'H:i'); 162 | } 163 | 164 | function numeric($value) 165 | { 166 | return \is_numeric($value); 167 | } 168 | 169 | function integer($value) 170 | { 171 | return \filter_var($value, FILTER_VALIDATE_INT) !== false; 172 | } 173 | 174 | function decimal($value) 175 | { 176 | return \filter_var($value, FILTER_VALIDATE_FLOAT) !== false; 177 | } 178 | 179 | function intl_decimal(&$value, $locale = null) 180 | { 181 | if ( ! \class_exists('\Locale') ) { 182 | throw new \RuntimeException('intl extension is not installed'); 183 | } 184 | 185 | if ( ! \is_string($value) && ! \is_int($value) && ! \is_float($value) ) { 186 | return false; 187 | } 188 | 189 | if ( $locale === null ) { 190 | $locale = \Locale::getDefault(); 191 | } 192 | 193 | $fmt = new \NumberFormatter($locale, \NumberFormatter::DECIMAL); 194 | $ret = $fmt->parse($value); 195 | 196 | if ( $ret !== false ) { 197 | $value = $ret; 198 | return true; 199 | } 200 | 201 | return false; 202 | } 203 | 204 | function intl_integer(&$value, $locale) 205 | { 206 | $original_value = $value; 207 | $ret = intl_decimal($value, $locale); 208 | if ( $ret == (int) $ret ) { 209 | $value = (int) $ret; 210 | return true; 211 | } 212 | return false; 213 | } 214 | 215 | /** 216 | * Check that the input is a valid email address. 217 | */ 218 | function email($value) 219 | { 220 | return filter_var($value, FILTER_VALIDATE_EMAIL) !== false; 221 | } 222 | 223 | function url(&$value, $protocols = null) 224 | { 225 | $ret = filter_var($value, FILTER_VALIDATE_URL); 226 | if ( $ret === false ) { 227 | return false; 228 | } 229 | 230 | if ( $protocols === null ) { 231 | return true; 232 | } 233 | 234 | if ( ! \is_array($protocols) ) { 235 | $protocols = array($protocols); 236 | } 237 | 238 | foreach ( $protocols as $proto ) { 239 | $proto .= '://'; 240 | if ( substr($value, 0, strlen($proto)) == $proto ) { 241 | return true; 242 | } 243 | } 244 | 245 | return false; 246 | } 247 | 248 | function ip($value, $flags = null) 249 | { 250 | return filter_var($value, FILTER_VALIDATE_IP, $flags) !== false; 251 | } 252 | 253 | function ipv4($value) 254 | { 255 | return ip($value, FILTER_FLAG_IPV4); 256 | } 257 | 258 | function ipv6($value) 259 | { 260 | return ip($value, FILTER_FLAG_IPV6); 261 | } 262 | 263 | 264 | /////////////////////////////////////////////////////////////////////////////// 265 | // Value rules 266 | 267 | /** 268 | * Check that the value is in the param array 269 | * If value is an array, it'll compute array diff. 270 | */ 271 | function in($value, array $param) 272 | { 273 | if ( \is_array($value) ) { 274 | $ret = array_diff($value, $param); 275 | return empty($ret); 276 | } 277 | 278 | return \in_array($value, $param); 279 | } 280 | 281 | /** 282 | * Check that value is a key of the param array. 283 | * If value is an array, it'll compute array diff. 284 | */ 285 | function in_keys($value, array $param) 286 | { 287 | if ( \is_array($value) ) { 288 | $ret = array_diff($value, array_keys($param)); 289 | return empty($ret); 290 | } 291 | 292 | if ( ! is_string($value) && ! is_int($value) ) { 293 | return false; 294 | } 295 | 296 | return array_key_exists($value, $param); 297 | } 298 | 299 | 300 | function between($value, $between) 301 | { 302 | if ( ! is_array($between) || count($between) != 2 ) { 303 | throw new \InvalidArgumentException("'between' rule takes an array of exactly two values"); 304 | } 305 | 306 | list($min,$max) = $between; 307 | if ( $min !== null ) { 308 | if ( ! min($value, $min) ) { 309 | return false; 310 | } 311 | } 312 | if ( $max !== null ) { 313 | if ( ! max($value, $max) ) { 314 | return false; 315 | } 316 | } 317 | 318 | return true; 319 | } 320 | 321 | function max($value, $param) 322 | { 323 | return $value <= $param; 324 | } 325 | 326 | function min($value, $param) 327 | { 328 | return $value >= $param; 329 | } 330 | 331 | function length($value, $between) 332 | { 333 | if ( ! is_array($between) || count($between) != 2 ) { 334 | throw new \InvalidArgumentException("'length' rule takes an array of exactly two values"); 335 | } 336 | 337 | list($min,$max) = $between; 338 | if ( $min !== null ) { 339 | if ( ! min_length($value, $min) ) { 340 | return false; 341 | } 342 | } 343 | if ( $max !== null ) { 344 | if ( ! max_length($value, $max) ) { 345 | return false; 346 | } 347 | } 348 | 349 | return true; 350 | } 351 | 352 | /** 353 | * Check that the value is a maximum length 354 | */ 355 | function max_length($value, $length) 356 | { 357 | if ( ! \is_string($value) && ! \is_int($value) ) { 358 | return false; 359 | } 360 | if ( (! \is_int($length) && ! ctype_digit($length)) || $length < 0 ) { 361 | throw new \InvalidArgumentException('The length must be an positive integer'); 362 | } 363 | return mb_strlen($value) <= $length; 364 | } 365 | 366 | /** 367 | * Check that the value is a minimum length 368 | */ 369 | function min_length($value, $length) 370 | { 371 | if ( ! \is_string($value) && ! \is_int($value) ) { 372 | return false; 373 | } 374 | if ( (! \is_int($length) && ! ctype_digit($length)) || $length < 0 ) { 375 | throw new \InvalidArgumentException('The length must be an positive integer'); 376 | } 377 | return mb_strlen($value) >= $length; 378 | } 379 | 380 | /** 381 | * Check the value against a regexp. 382 | * 383 | * @param $value mixed 384 | * @param $regexp string Regular expression 385 | * @return bool 386 | */ 387 | function regexp($value, $regexp) 388 | { 389 | if ( ! \is_string($regexp) ) { 390 | throw new \InvalidArgumentException('The regular expression must be a string'); 391 | } 392 | if ( ! $regexp ) { 393 | throw new \InvalidArgumentException('The regular expression cannot be empty'); 394 | } 395 | 396 | return !! filter_var($value, FILTER_VALIDATE_REGEXP, array( 397 | 'options' => array('regexp' => $regexp) 398 | )); 399 | } 400 | 401 | 402 | /////////////////////////////////////////////////////////////////////////////// 403 | // Special rules 404 | 405 | /** 406 | * Check that the value is a string and trim it of unwanted character. 407 | * 408 | * @param $value mixed 409 | * @param $character_mask string The list of characters to be trimmed. 410 | * @see http://www.php.net/trim 411 | * @return bool 412 | */ 413 | function trim(&$value, $character_mask = null) 414 | { 415 | // trim will trigger an error if called with something else than a string or an int 416 | if ( ! \is_string($value) && ! \is_int($value) && ! \is_float($value) ) { 417 | return true; 418 | } 419 | 420 | if ( $character_mask === null ) { 421 | $character_mask = " \t\n\r\0\x0B"; 422 | } 423 | elseif ( ! \is_string($character_mask) ) { 424 | throw new \InvalidArgumentException("Character mask for 'trim' must be a string"); 425 | } 426 | 427 | $value = \trim($value, $character_mask); 428 | return true; 429 | } 430 | 431 | 432 | 433 | // function date_max($value, $param) 434 | // { 435 | // return strtotime($value) <= $param; 436 | // } 437 | 438 | // function date_min($value, $param) 439 | // { 440 | // return strtotime($value) >= $param; 441 | // } 442 | -------------------------------------------------------------------------------- /tests/rulesTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($is_array, Form\Rule\is_array($value)); 35 | } 36 | 37 | /** 38 | * @dataProvider dataTypes 39 | */ 40 | public function testIsEmpty($value, $is_empty, $is_string, $is_array) 41 | { 42 | $this->assertEquals($is_empty, Form\Rule\is_empty($value)); 43 | } 44 | 45 | /** 46 | * @dataProvider dataTypes 47 | */ 48 | public function testIsString($value, $is_empty, $is_string, $is_array) 49 | { 50 | $this->assertEquals($is_string, Form\Rule\is_string($value)); 51 | } 52 | 53 | /////////////////////////////////////////////////////////////////////////////// 54 | // Type rules 55 | 56 | public function boolValues() 57 | { 58 | return array( 59 | // value is_bool casted 60 | [0, true, 0], 61 | [false, true, false], 62 | ['0', true, '0'], 63 | ['false', true, '0'], 64 | ['f', true, '0'], 65 | ['no', true, '0'], 66 | ['n', true, '0'], 67 | ['off', true, '0'], 68 | 69 | [1, true, 1], 70 | [true, true, true], 71 | ['1', true, '1'], 72 | ['true', true, '1'], 73 | ['t', true, '1'], 74 | ['yes', true, '1'], 75 | ['y', true, '1'], 76 | ['on', true, '1'], 77 | 78 | 79 | [0.1, false, 0.1], 80 | [2, false, 2], 81 | ['foobar', false, 'foobar'], 82 | [null, false, null], 83 | [array(), false, array()], 84 | [new stdClass(), false, new stdClass()], 85 | ); 86 | } 87 | 88 | /** 89 | * @dataProvider boolValues 90 | */ 91 | public function testBool($value, $is_bool, $casted_value) 92 | { 93 | $this->assertEquals($is_bool, Form\Rule\bool($value)); 94 | $this->assertEquals($casted_value, $value); 95 | } 96 | 97 | /** 98 | * @dataProvider boolValues 99 | */ 100 | public function testBoolNoCast($value, $is_bool, $casted_value) 101 | { 102 | $original_value = $value; 103 | $this->assertEquals($is_bool, Form\Rule\bool($value, false)); 104 | $this->assertEquals($original_value, $value); 105 | } 106 | 107 | /** 108 | * @dataProvider boolValues 109 | */ 110 | public function testBoolCastBool($value, $is_bool, $casted_value) 111 | { 112 | $this->assertEquals($is_bool, Form\Rule\bool($value, 'bool')); 113 | if ( $is_bool ) { 114 | $this->assertInternalType('bool', $value); 115 | $this->assertEquals((bool) $casted_value, $value); 116 | } 117 | else { 118 | $this->assertEquals($casted_value, $value); 119 | } 120 | } 121 | 122 | /** 123 | * @dataProvider boolValues 124 | */ 125 | public function testBoolCastString($value, $is_bool, $casted_value) 126 | { 127 | $this->assertEquals($is_bool, Form\Rule\bool($value, 'string')); 128 | if ( $is_bool ) { 129 | $this->assertInternalType('string', $value); 130 | $this->assertEquals($casted_value ? '1' : '0', $value); 131 | } 132 | else { 133 | $this->assertEquals($casted_value, $value); 134 | } 135 | } 136 | 137 | /** 138 | * @dataProvider boolValues 139 | */ 140 | public function testBoolCastInt($value, $is_bool, $casted_value) 141 | { 142 | $this->assertEquals($is_bool, Form\Rule\bool($value, 'int')); 143 | if ( $is_bool ) { 144 | $this->assertInternalType('int', $value); 145 | $this->assertEquals($casted_value ? 1 : 0, $value); 146 | } 147 | else { 148 | $this->assertEquals($casted_value, $value); 149 | } 150 | } 151 | 152 | public function dateTimeValues() 153 | { 154 | return array( 155 | // value format is_date is_datetime istime 156 | [0, null, false, false, false], 157 | [array(), null, false, false, false], 158 | ['foobar', null, false, false, false], 159 | [true, null, false, false, false], 160 | [false, null, false, false, false], 161 | 162 | ['2015-12-12', null, true, false, false], 163 | ['2015-12-12', 'Y-m-d', true, false, false], 164 | ['2015-13-12', false, false, false, false], 165 | ['2015-13-12', 'Y-m-d', false, false, false], 166 | ['2015-13-12', 'Y-d-m', true, false, false], 167 | ['2015-12-12', 'd-m-Y', false, false, false], 168 | 169 | ['2015-12-12 00:00:00', null, true, true, false], 170 | ['2015-12-12 00:00:00', 'Y-m-d H:i:s', true, true, false], 171 | ['2015-12-12 42:00:00', null, false, false, false], 172 | ['2015-12-12 42:00:00', 'Y-m-d H:i:s', false, false, false], 173 | 174 | ['12:00', null, true, false, true], 175 | ['42:00', null, false, false, false], 176 | ['12:00:00', null, true, false, false], 177 | ['12:00:00', 'H:i:s', true, false, false], 178 | ); 179 | } 180 | 181 | /** 182 | * @dataProvider dateTimeValues 183 | */ 184 | public function testDate($value, $format, $is_date) 185 | { 186 | $this->assertEquals($is_date, Form\Rule\date($value, $format)); 187 | } 188 | 189 | /** 190 | * @dataProvider dateTimeValues 191 | */ 192 | public function testDatetime($value, $format, $is_date, $is_datetime) 193 | { 194 | $this->assertEquals($is_datetime, Form\Rule\datetime($value)); 195 | } 196 | 197 | /** 198 | * @dataProvider dateTimeValues 199 | */ 200 | public function testTime($value, $format, $is_date, $is_datetime, $is_time) 201 | { 202 | $this->assertEquals($is_time, Form\Rule\time($value)); 203 | } 204 | 205 | public function numericValues() 206 | { 207 | return array( 208 | //value numeric integer intl_integer float intl_decimal 209 | [0, true, true, ['fr_FR' => 0], true, ['fr_FR' => 0]], 210 | ['0', true, true, ['fr_FR' => 0], true, ['fr_FR' => 0]], 211 | [42, true, true, ['fr_FR' => 42], true, ['fr_FR' => 42]], 212 | ['42', true, true, ['fr_FR' => 42], true, ['fr_FR' => 42]], 213 | [-42, true, true, ['fr_FR' => -42], true, ['fr_FR' => -42]], 214 | ['-42', true, true, ['fr_FR' => -42], true, ['fr_FR' => -42]], 215 | [42.5, true, false, ['fr_FR' => false, 'en_US' => false], true, ['en_US' => 42.5, 'fr_FR' => false]], 216 | ['42.5', true, false, ['en_US' => false, 'fr_FR' => false], true, ['en_US' => 42.5, 'fr_FR' => false]], 217 | ['42,5', false, false, ['en_US' => false, 'fr_FR' => false], false, ['en_US' => false, 'fr_FR' => 42.5]], 218 | ['1 000', false, false, ['en_GB' => 1000, 'fr_FR' => 1000], false, ['en_GB' => 1000, 'fr_FR' => 1000]], 219 | ['1.000', true, false, ['en_GB' => false, 'fr_FR' => 1000], true, ['en_GB' => 1.0, 'fr_FR' => 1000]], 220 | ['1,000', false, false, ['en_GB' => 1000, 'fr_FR' => false], false, ['en_GB' => 1000, 'fr_FR' => 1.0]], 221 | // sadly, this is valid in en_GB (shouldn't be) 222 | ['1.000,42', false, false, ['en_GB' => 1, 'fr_FR' => false], false, ['en_GB' => 1, 'fr_FR' => 1000.42]], 223 | ['1 000,42', false, false, ['en_GB' => 1000, 'fr_FR' => false], false, ['en_GB' => 1000, 'fr_FR' => 1000.42]], 224 | ['1000,42', false, false, ['en_GB' => false, 'fr_FR' => false], false, ['en_GB' => false, 'fr_FR' => 1000.42]], 225 | 226 | [null, false, false, false, false, false], 227 | [array(), false, false, false, false, false], 228 | ['foobar', false, false, false, false, false] 229 | ); 230 | } 231 | 232 | /** 233 | * @dataProvider numericValues 234 | */ 235 | public function testNumeric($value, $is_numeric) 236 | { 237 | $this->assertEquals($is_numeric, Form\Rule\numeric($value)); 238 | } 239 | 240 | /** 241 | * @dataProvider numericValues 242 | */ 243 | public function testInteger($value, $is_numeric, $is_integer) 244 | { 245 | $this->assertEquals($is_integer, Form\Rule\integer($value)); 246 | } 247 | 248 | /** 249 | * @dataProvider numericValues 250 | */ 251 | public function testDecimal($value, $is_numeric, $is_integer, $locales, $is_decimal) 252 | { 253 | $this->assertEquals($is_decimal, Form\Rule\decimal($value)); 254 | } 255 | 256 | /** 257 | * @dataProvider numericValues 258 | */ 259 | public function testIntlDecimal($value, $is_numeric, $is_integer, $int_locales, $is_float, $locales) 260 | { 261 | $original_value = $value; 262 | if ( $locales ) { 263 | foreach ( $locales as $locale => $float_version ) { 264 | if ( $float_version === false ) { 265 | $this->assertFalse(Form\Rule\intl_decimal($value, $locale), "$original_value is not a decimal in $locale (is it $value)"); 266 | } 267 | else { 268 | $this->assertTrue(Form\Rule\intl_decimal($value, $locale), "$original_value is a valid decimal in $locale"); 269 | $this->assertEquals($float_version, $value); 270 | } 271 | $value = $original_value; 272 | } 273 | } 274 | else { 275 | $this->assertFalse(Form\Rule\intl_decimal($value)); 276 | } 277 | } 278 | 279 | public function emailValues() 280 | { 281 | return array( 282 | ['valid@email.com', true], 283 | ['valid+sep@email.com', true], 284 | ['foobar', false], 285 | [array(), false], 286 | ); 287 | } 288 | 289 | /** 290 | * @dataProvider emailValues 291 | */ 292 | public function testEmail($value, $is_email) 293 | { 294 | $this->assertEquals($is_email, Form\Rule\email($value)); 295 | } 296 | 297 | public function urlValues() 298 | { 299 | return array( 300 | ['www.asdf.com', null, false], 301 | ['example.org', null, false], 302 | ['/peach.kingdom', null, false], 303 | ['x.something.co.uk', null, false], 304 | ['http:myname.com', null, false], 305 | ['https://www.sdf.org', null, true], 306 | ['https://www.sdf.org', 'http', false], 307 | ['http://www.foo.bar/?f=42', null, true], 308 | ['http://user:password@www.foo.bar/?f=42', null, true], 309 | ['ssh://github.com', null, true], 310 | ['ssh://github.com', 'http', false], 311 | ['ssh://github.com', ['http','ssh'], true], 312 | ['mailto://foo@bar.com', null, true] 313 | ); 314 | } 315 | 316 | /** 317 | * @dataProvider urlValues 318 | */ 319 | public function testUrl($value, $protocols, $is_url) 320 | { 321 | $this->assertEquals($is_url, Form\Rule\url($value, $protocols)); 322 | } 323 | 324 | public function ipValues() 325 | { 326 | return array( 327 | // v4 v6 328 | ['foobar', false, false], 329 | [0, false, false], 330 | ['0', false, false], 331 | [array(), false, false], 332 | ['4294967295', false, false], 333 | 334 | [' 127.0.0.1 ', false, false], // not trimmed by default 335 | 336 | ['127.0.0.1', true, false], 337 | ['2a01:8200::', false, true], 338 | ['::ffff:192.0.2.128', false, true] 339 | ); 340 | } 341 | 342 | /** 343 | * @dataProvider ipValues 344 | */ 345 | public function testIp($value, $ipv4, $ipv6) 346 | { 347 | $this->assertEquals($ipv4 || $ipv6, Form\Rule\ip($value)); 348 | } 349 | 350 | /** 351 | * @dataProvider ipValues 352 | */ 353 | public function testIpv4($value, $ipv4, $ipv6) 354 | { 355 | $this->assertEquals($ipv4, Form\Rule\ipv4($value)); 356 | } 357 | 358 | /** 359 | * @dataProvider ipValues 360 | */ 361 | public function testIpv6($value, $ipv4, $ipv6) 362 | { 363 | $this->assertEquals($ipv6, Form\Rule\ipv6($value)); 364 | } 365 | 366 | /////////////////////////////////////////////////////////////////////////////// 367 | // Value rules 368 | 369 | public function inValues() 370 | { 371 | return array( 372 | // value, in, array 373 | ['foo', true, array('foo','bar')], 374 | ['foobar', false, array('foo','bar')], 375 | ['1', true, array(1,2)], 376 | [42, true, array(4,2,42)], 377 | [42, true, array('4','2','42')], 378 | [42, false, array(4,2)], 379 | [0, true, array(0,1)], 380 | [new stdClass(), true, array(new stdClass())], 381 | [new stdClass(), false, array('something')], 382 | [array('foo','bar'), true, array('foo','bar','foobar')], 383 | [array('foo','bar'), false, array('foo')], 384 | ); 385 | } 386 | 387 | /** 388 | * @dataProvider inValues 389 | */ 390 | public function testIn($value, $is_in, $array) 391 | { 392 | $this->assertEquals($is_in, Form\Rule\in($value, $array)); 393 | } 394 | 395 | public function inKeysValues() 396 | { 397 | return array( 398 | // value, in, array 399 | ['foo', true, array('foo' => 'XX', 'bar' => 'XX')], 400 | ['foo', false, array('foo','bar')], 401 | ['foobar', false, array('foo' => 'XX', 'bar' => 'XX')], 402 | ['1', true, array(1 => 'Foo', 2 => 'Bar')], 403 | [1, true, array(1 => 'Foo', 2 => 'Bar')], 404 | [42, false, array(42)], 405 | [new stdClass(), false, array(new stdClass())], 406 | [new stdClass(), false, array('something')], 407 | [array('foo','bar'), false, array('foo','bar','foobar')], 408 | [array('foo','bar'), true, array('foo' => 'XX', 'bar' => 'XX', 'foobar' => 'XX')], 409 | [array('foo','bar'), false, array('foo')], 410 | [array('foo','bar'), false, array('foo' => 'XX')], 411 | ); 412 | } 413 | 414 | /** 415 | * @dataProvider inKeysValues() 416 | */ 417 | public function testInKeys($value, $is_in_keys, $array) 418 | { 419 | $this->assertEquals($is_in_keys, Form\Rule\in_keys($value, $array)); 420 | } 421 | 422 | 423 | public function minMaxValues() 424 | { 425 | return array( 426 | // value between min max between 427 | [42, [0,50], true, true, true], 428 | [42, [50,100], false, true, false], 429 | [42, [0,30], true, false, false], 430 | 431 | [42, [42,42], true, true, true], 432 | [42, [null,50], true, true, true], 433 | [42, [40,null], true, true, true], 434 | 435 | [-42, [-50,0], true, true, true], 436 | [-42, [0,50], false, true, false], 437 | [-42, [-100,-50], true, false, false], 438 | 439 | // with numeric string 440 | ['42', [0,50], true, true, true], 441 | ['42', [50,100], false, true, false], 442 | ['42', [0,30], true, false, false], 443 | 444 | // with dates (this is basic) 445 | ['2015-02-20', ['2015-01-01','2015-12-31'], true, true, true], 446 | ['2015-02-20', ['2015-01-01','2015-02-15'], true, false, false], 447 | ['2015-02-20', ['2015-03-01','2015-12-31'], false, true, false], 448 | 449 | // with times 450 | ['13:00', ['00:00','23:59'], true, true, true], 451 | ['13:00', ['00:00','12:00'], true, false, false], 452 | ['13:00', ['15:00','23:59'], false, true, false], 453 | 454 | // with strings is not recommended 455 | ['b', ['a','z'], true, true, true], 456 | ['a', ['a','z'], true, true, true], 457 | ['z', ['a','e'], true, false, false], 458 | ['é', ['a','z'], true, false, false], // unexpected 459 | ['abc', ['aaa','zzzz'], true, true, true], 460 | ); 461 | } 462 | 463 | /** 464 | * @dataProvider minMaxValues 465 | */ 466 | public function testValueMin($value, $between, $is_valid) 467 | { 468 | list($min,$max) = $between; 469 | if ( $min !== null ) { 470 | $this->assertEquals($is_valid, Form\Rule\min($value, $min)); 471 | } 472 | } 473 | 474 | /** 475 | * @dataProvider minMaxValues 476 | */ 477 | public function testValueMax($value, $between, $min_valid, $is_valid) 478 | { 479 | list($min,$max) = $between; 480 | if ( $max !== null ) { 481 | $this->assertEquals($is_valid, Form\Rule\max($value, $max)); 482 | } 483 | } 484 | 485 | /** 486 | * @dataProvider minMaxValues 487 | */ 488 | public function testValueBetween($value, $between, $min_valid, $max_valid, $is_valid) 489 | { 490 | $this->assertEquals($is_valid, Form\Rule\between($value, $between)); 491 | } 492 | 493 | public function lengthValues() 494 | { 495 | return array( 496 | // value between min max between 497 | ['1234', [4,10], true, true, true], 498 | ['1234', [10,20], false, true, false], 499 | ['1234', [1,3], true, false, false], 500 | 501 | [1234, [4,10], true, true, true], 502 | [1234, [10,20], false, true, false], 503 | [1234, [1,3], true, false, false], 504 | 505 | ['é', [1,1], true, true, true], 506 | 507 | [array(), [1,1], false, false, false], 508 | [null, [1,1], false, false, false], 509 | [false, [1,1], false, false, false], 510 | [new stdClass(), [1,1], false, false, false], 511 | ); 512 | } 513 | 514 | public function invalidLengths() 515 | { 516 | return array( 517 | [null], 518 | [new stdClass()], 519 | [array()], 520 | [-1] 521 | ); 522 | } 523 | 524 | /** 525 | * @dataProvider lengthValues 526 | */ 527 | public function testMinLength($value, $between, $is_valid) 528 | { 529 | list($min,$max) = $between; 530 | if ( $min !== null ) { 531 | $this->assertEquals($is_valid, Form\Rule\min_length($value, $min)); 532 | } 533 | } 534 | 535 | /** 536 | * @dataProvider lengthValues 537 | */ 538 | public function testMaxLength($value, $between, $min_valid, $is_valid) 539 | { 540 | list($min,$max) = $between; 541 | if ( $max !== null ) { 542 | $this->assertEquals($is_valid, Form\Rule\max_length($value, $max)); 543 | } 544 | } 545 | 546 | /** 547 | * @dataProvider lengthValues 548 | */ 549 | public function testLength($value, $between, $min_valid, $max_valid, $is_valid) 550 | { 551 | $this->assertEquals($is_valid, Form\Rule\length($value, $between)); 552 | } 553 | 554 | /** 555 | * @dataProvider invalidLengths 556 | * @expectedException InvalidArgumentException 557 | */ 558 | public function testInvalidMinLength($length) 559 | { 560 | Form\Rule\min_length('foobar', $length); 561 | } 562 | 563 | /** 564 | * @dataProvider invalidLengths 565 | * @expectedException InvalidArgumentException 566 | */ 567 | public function testInvalidMaxLength($length) 568 | { 569 | Form\Rule\max_length('foobar', $length); 570 | } 571 | 572 | /** 573 | * @dataProvider invalidLengths 574 | * @expectedException InvalidArgumentException 575 | */ 576 | public function testInvalidLength($length) 577 | { 578 | Form\Rule\length('foobar', $length); 579 | } 580 | 581 | public function regexpValues() 582 | { 583 | // $regexp = ; 584 | // $this->assertTrue(Form\Rule\regexp('this-is-valid', $regexp)); 585 | // $this->assertTrue(Form\Rule\regexp(42, $regexp)); 586 | // $this->assertFalse(Form\Rule\regexp('This is not!', $regexp)); 587 | // $this->assertFalse(Form\Rule\regexp(array(), $regexp)); 588 | // $this->assertFalse(Form\Rule\regexp(false, $regexp)); 589 | // $this->assertFalse(Form\Rule\regexp(null, $regexp)); 590 | // $this->assertFalse(Form\Rule\regexp(new stdClass(), $regexp)); 591 | // $this->assertFalse(Form\Rule\regexp(42.5, $regexp)); 592 | 593 | return array( 594 | // value regexp is_valid 595 | ['this-is-valid', '/^[0-9a-zA-Z\-]*$/', true], 596 | [42, '/^[0-9a-zA-Z\-]*$/', true], 597 | ['This is not!', '/^[0-9a-zA-Z\-]*$/', false], 598 | [array(), '/^[0-9a-zA-Z\-]*$/', false], 599 | [false, '/^[0-9a-zA-Z\-]*$/', false], 600 | [null, '/^[0-9a-zA-Z\-]*$/', false], 601 | [new stdClass(), '/^[0-9a-zA-Z\-]*$/', false], 602 | [42.5, '/^[0-9a-zA-Z\-]*$/', false], 603 | ); 604 | } 605 | 606 | public function invalidRegexps() 607 | { 608 | return array( 609 | [array()], 610 | [new stdClass()], 611 | [null], 612 | [42], 613 | [42.5], 614 | [''] 615 | ); 616 | } 617 | 618 | /** 619 | * @dataProvider regExpValues 620 | */ 621 | public function testRegexp($value, $regexp, $is_valid) 622 | { 623 | $this->assertEquals($is_valid, Form\Rule\regexp($value, $regexp)); 624 | } 625 | 626 | /** 627 | * @dataProvider invalidRegexps 628 | * @expectedException InvalidArgumentException 629 | */ 630 | public function testInvalidRegexps($regexp) 631 | { 632 | Form\Rule\regexp('something', $regexp); 633 | } 634 | 635 | /////////////////////////////////////////////////////////////////////////////// 636 | // Special rules 637 | 638 | public function trimValues() 639 | { 640 | return array( 641 | [' trim me ', 'trim me'], 642 | ['42', '42'], 643 | [42, '42'], 644 | ["trim\t\n\r", 'trim'], 645 | ["\t", ''], 646 | 647 | [array(), array()], 648 | [new stdClass(), new stdClass()], 649 | [null, null], 650 | [42.5, 42.5] 651 | ); 652 | } 653 | 654 | public function invalidTrim() 655 | { 656 | return array( 657 | [array()], 658 | [new stdClass()], 659 | [42.5], 660 | ); 661 | } 662 | 663 | /** 664 | * @dataProvider trimValues 665 | */ 666 | public function testTrim($value, $trimmed_value, $mask = null) 667 | { 668 | $original_value = $value; 669 | $this->assertTrue(Form\Rule\trim($value, $mask)); 670 | $this->assertEquals($trimmed_value, $value); 671 | } 672 | 673 | /** 674 | * @dataProvider invalidTrim 675 | * @expectedException InvalidArgumentException 676 | */ 677 | public function testInvalidTrim($mask) 678 | { 679 | $value = 'foobar'; 680 | Form\Rule\trim($value, $mask); 681 | } 682 | } 683 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | 9 | * @link https://github.com/rlanvin/php-form 10 | */ 11 | 12 | namespace Form; 13 | 14 | require_once __DIR__.'/rules.php'; 15 | 16 | /** 17 | */ 18 | class Validator implements \ArrayAccess 19 | { 20 | const EACH = 'each'; 21 | 22 | /** 23 | * The values as assoc (possibly recursive) array. 24 | * [field_name => field_value] 25 | * @var array 26 | */ 27 | protected $values = array(); 28 | 29 | /** 30 | * The rules as a big assoc (possibly recursive) array. 31 | * [ 32 | * field_name => [ 33 | * rule_name => rule_value 34 | * .... 35 | * ] 36 | * ] 37 | * @var array 38 | */ 39 | protected $rules = array(); 40 | 41 | /** 42 | * The validation errors as an array 43 | * [ 44 | * field_name => [ 45 | * rule_name => rule_value 46 | * ... 47 | * ] 48 | * ] 49 | * @var array 50 | */ 51 | protected $errors = array(); 52 | 53 | /** 54 | * Store the parent Validator object when it's a subform, so it's accessible 55 | * within a validator callback. 56 | * @var Validator 57 | */ 58 | protected $parent = null; 59 | 60 | /** 61 | * Default options array 62 | * @var array 63 | */ 64 | protected $options = array( 65 | 'use_default' => true, // use the values provided as default values 66 | 'stop_on_error' => true, // stop at the first error 67 | 'allow_empty' => true, // bypass all validators when the value is empty (otherwise: run validators even if the value is empty) 68 | 'ignore_extraneous' => true // ignore values with no rules (otherwise: throws a validation error) 69 | ); 70 | 71 | /** 72 | * Constructor 73 | * 74 | * @param $rules array The form definition, an assoc array of field_name => rules 75 | */ 76 | public function __construct($rules = array(), $options = array()) 77 | { 78 | $this->setOptions($options); 79 | $this->setRules($rules); 80 | } 81 | 82 | /** 83 | * @internal 84 | */ 85 | public static function checkStringNotEmpty($field, $name = 'Field name') 86 | { 87 | if ( ! is_string($field) ) { 88 | throw new \InvalidArgumentException(sprintf("$name must be a string (%s given)", gettype($field))); 89 | } 90 | if ( ! $field ) { 91 | throw new \InvalidArgumentException("$name cannot be empty"); 92 | } 93 | } 94 | 95 | /** 96 | * @internal 97 | * 98 | * Returns the base field name, and a array of subfields (if any) 99 | * Example: 100 | * address[street][number] 101 | * becomes 102 | * ['address','street','number'] 103 | * 104 | * @return array 105 | */ 106 | public static function expandFieldName($field) 107 | { 108 | if ( ! preg_match('/^(\w+)((\[\w+\])*)$/',$field, $matches) ) { 109 | return array($field); 110 | } 111 | 112 | if ( empty($matches[2]) ) { 113 | return array($field); 114 | } 115 | 116 | return array_merge( 117 | array($matches[1]), // base field 118 | preg_split('/\]\[/',trim($matches[2],'[]')) // subfields 119 | ); 120 | } 121 | 122 | public function setOptions(array $options) 123 | { 124 | $this->options = array_merge($this->options, $options); 125 | } 126 | 127 | public function getOptions() 128 | { 129 | return $this->options; 130 | } 131 | 132 | /////////////////////////////////////////////////////////////////////////////// 133 | // RULES 134 | 135 | /** 136 | * Set the rules of the form. 137 | * 138 | * This methods can be called two ways: 139 | * setRules(string $field_name, array|self $rules) to set the rules for a field 140 | * or setRules(array $rules) to set the entire rules array 141 | * 142 | * @param $rules array|self An array or a sub-validator 143 | * @return $this 144 | */ 145 | public function setRules($field_or_rules, $rules = array()) 146 | { 147 | // set the rules for one particular field 148 | if ( is_string($field_or_rules) ) { 149 | if ( ! $field_or_rules ) { 150 | throw new \InvalidArgumentException("Field name cannot be empty"); 151 | } 152 | if ( is_array($rules) ) { 153 | $this->rules[$field_or_rules] = self::expandRulesArray($rules); 154 | } 155 | elseif ( $rules instanceof self ) { 156 | $this->rules[$field_or_rules] = $rules; 157 | } 158 | else { 159 | throw new \InvalidArgumentException("Rules must be an array or an instance of ".__CLASS__); 160 | } 161 | return $this; 162 | } 163 | 164 | // set the rules of the form 165 | if ( is_array($field_or_rules) ) { 166 | $this->rules = self::parseRules($field_or_rules); 167 | return $this; 168 | } 169 | 170 | throw new \BadMethodCallException("Unsupported parameter type"); 171 | } 172 | 173 | /** 174 | * Add rules to existing form. 175 | * Rules will be merged to existing rules. 176 | * 177 | * @param $rules array 178 | * @return $this 179 | */ 180 | public function addRules(array $rules) 181 | { 182 | // XXX apparently this creates a problem when trying to merge rules 183 | // that are subforms 184 | $this->rules = array_merge_recursive($this->rules, self::parseRules($rules)); 185 | return $this; 186 | } 187 | 188 | /** 189 | * Return the rules array of this form or of a single field. 190 | * 191 | * If the field is not set in the rules array, it'll return empty array. 192 | * For nested validators, the field name can be written like this: 193 | * 'a[b][c]' 194 | * 195 | * @return array|Validator 196 | */ 197 | public function getRules($field = '') 198 | { 199 | if ( ! is_string($field) ) { 200 | throw new \InvalidArgumentException(sprintf("Field name must be a string (%s given)", gettype($field))); 201 | } 202 | 203 | if ( ! $field ) { 204 | return $this->rules; 205 | } 206 | 207 | if ( ! isset($this->rules[$field]) ) { 208 | $field = self::expandFieldName($field); 209 | } 210 | else { 211 | $field = array($field); 212 | } 213 | 214 | // now $field is an array, for example 'a[b][c]' is now ['a','b','c'] 215 | 216 | $rules = $this->rules; 217 | foreach ( $field as $f ) { 218 | if ( $rules instanceof self ) { 219 | $rules = $rules->getRules($f); 220 | } 221 | elseif ( isset($rules[$f]) ) { 222 | $rules = $rules[$f]; 223 | } 224 | else { 225 | return array(); // not found, let's stop now 226 | } 227 | 228 | // execute closure 229 | if ( is_callable($rules) ) { 230 | $rules = call_user_func_array($rules, array($this)); 231 | if ( is_array($rules) ) { 232 | $rules = $this->expandRulesArray($rules); 233 | } 234 | } 235 | }; 236 | 237 | return $rules; 238 | } 239 | 240 | /** 241 | * Return true if the given field name as rules associated 242 | * 243 | * @param $field string 244 | * @return true 245 | */ 246 | public function hasRules($field) 247 | { 248 | self::checkStringNotEmpty($field); 249 | 250 | // return isset($this->rules[$field]) && ! empty($this->rules[$field]); 251 | $rules = $this->getRules($field); 252 | return !empty($rules); 253 | } 254 | 255 | /** 256 | * Return the value of a given rule for a given field or null if the rule 257 | * or the field is not set in this form. 258 | * 259 | * @param $field string 260 | * @param $rule_name string 261 | * @return mixed 262 | */ 263 | public function getRuleValue($field, $rule_name) 264 | { 265 | self::checkStringNotEmpty($field); 266 | 267 | $rules = $this->getRules($field); 268 | 269 | self::checkStringNotEmpty($rule_name, 'Rule name'); 270 | 271 | if ( ! array_key_exists($rule_name, $rules) ) { 272 | return null; 273 | } 274 | 275 | return $rules[$rule_name]; 276 | } 277 | 278 | /** 279 | * Returns true if a field is required. 280 | * Shortcut for $this->getRuleValue($field, 'required'); 281 | * 282 | * @return bool 283 | */ 284 | public function isRequired($field) 285 | { 286 | return !! $this->getRuleValue($field, 'required'); 287 | } 288 | 289 | /** 290 | * Check a rules array. 291 | * 292 | * @param $rules array 293 | * @return array 294 | */ 295 | public static function parseRules(array $rules) 296 | { 297 | foreach ( $rules as $field => & $field_rules ) { 298 | self::checkStringNotEmpty($field); 299 | 300 | if ( is_array($field_rules) ) { 301 | $field_rules = self::expandRulesArray($field_rules); 302 | } elseif ( $field_rules instanceof self ) { 303 | // do nothing 304 | } elseif ( is_callable($field_rules) ) { 305 | // do nothing 306 | } else { 307 | throw new \InvalidArgumentException("Invalid rules for field $field, must be array, closure or ".__CLASS__); 308 | } 309 | } 310 | return $rules; 311 | } 312 | 313 | /** 314 | * Makes sure the rules array is properly formatted, i.e. with the rule 315 | * name as key. 316 | * 317 | * This is just a small method so we can use shorter syntax in the code. 318 | * 319 | * For example: 320 | * ['required', 'min_length' => 2] 321 | * becomes 322 | * ['required' => true, 'min_length' => 2] 323 | * 324 | * @param $array array 325 | * @return array 326 | */ 327 | public static function expandRulesArray(array $array) 328 | { 329 | $new_array = array(); 330 | foreach ( $array as $key => $param ) { 331 | // the validator has been written as array value 332 | if ( is_int($key) ) { 333 | self::checkStringNotEmpty($param, 'Rule name'); 334 | $new_array[$param] = true; 335 | } 336 | elseif ( $key == '' ) { 337 | throw new \InvalidArgumentException("Rule name cannot be empty"); 338 | } 339 | elseif ( $key == self::EACH ) { 340 | // these special keys have nested rules 341 | if ( is_array($param) ) { 342 | $new_array[$key] = self::expandRulesArray($param); 343 | } 344 | elseif ( $param instanceof self ) { 345 | // do nothing at this stage 346 | $new_array[$key] = $param; 347 | } 348 | else { 349 | throw new \InvalidArgumentException('The rule "each" needs an array or a '.__CLASS__); 350 | } 351 | } 352 | // nothing to flip 353 | else { 354 | $new_array[$key] = $param; 355 | } 356 | } 357 | return $new_array; 358 | } 359 | 360 | /////////////////////////////////////////////////////////////////////////////// 361 | // VALUES 362 | 363 | /** 364 | * Return all the values as an assoc array. 365 | * [field_name => value] 366 | * 367 | * @return array 368 | */ 369 | public function getValues() 370 | { 371 | return $this->values; 372 | } 373 | 374 | /** 375 | * Return the value of a single field. 376 | * 377 | * @param $field string Field name 378 | * @param $default mixed Value to return if not exist 379 | * @return mixed 380 | */ 381 | public function getValue($field, $default = null) 382 | { 383 | self::checkStringNotEmpty($field); 384 | 385 | if ( array_key_exists($field, $this->values) ) { 386 | return $this->values[$field]; 387 | } 388 | else { 389 | $field = self::expandFieldName($field); 390 | $values = $this->values; 391 | foreach ( $field as $f ) { 392 | if ( ! isset($values[$f]) ) { 393 | return $default; 394 | } 395 | $values = $values[$f]; 396 | } 397 | return $values; 398 | } 399 | } 400 | 401 | /** 402 | * Magic getter to enable $form->field syntax to get a value. 403 | * 404 | * @see getValue 405 | */ 406 | public function __get($field) 407 | { 408 | return $this->getValue($field); 409 | } 410 | 411 | /** 412 | * ArrayAccess interface to enable $form['field'] syntax to get a value. 413 | * 414 | * @see getValue 415 | */ 416 | public function offsetGet($field) 417 | { 418 | return $this->getValue($field); 419 | } 420 | 421 | /** 422 | * Set default values of the form. 423 | * The values won't be validated. 424 | * 425 | * @return $this 426 | */ 427 | public function setValues(array $values) 428 | { 429 | $this->values = $values; 430 | return $this; 431 | } 432 | 433 | /** 434 | * Set a single value. 435 | * 436 | * @param $field string 437 | * @param $value mixed 438 | * @return $this 439 | */ 440 | public function setValue($field, $value) 441 | { 442 | self::checkStringNotEmpty($field); 443 | $this->values[$field] = $value; 444 | return $this; 445 | } 446 | 447 | /** 448 | * Magic setter to enable $form->field = 42 syntax. 449 | * 450 | * @see setValue 451 | */ 452 | public function __set($field, $value) 453 | { 454 | return $this->setValue($field, $value); 455 | } 456 | 457 | /** 458 | * ArrayAccess interface to enable $form['field'] = 42 syntax. 459 | * 460 | * @see setValue 461 | */ 462 | public function offsetSet($field, $value) 463 | { 464 | return $this->setValue($field, $value); 465 | } 466 | 467 | /** 468 | * Merge values with existing array. 469 | * 470 | * @param $values array 471 | * @return $this 472 | */ 473 | public function addValues(array $values) 474 | { 475 | $this->values = array_merge($this->values, $values); 476 | return $this; 477 | } 478 | 479 | /** 480 | * @internal 481 | */ 482 | public function offsetExists($field) 483 | { 484 | return array_key_exists($field, $this->values); 485 | } 486 | 487 | /** 488 | * @internal 489 | */ 490 | public function offsetUnset($field) 491 | { 492 | if ( array_key_exists($field, $this->values) ) { 493 | unset($this->values[$field]); 494 | } 495 | } 496 | 497 | /////////////////////////////////////////////////////////////////////////////// 498 | // ERRORS 499 | 500 | /** 501 | * Return the errors array of the form or of a given field 502 | * 503 | * Error array is like the rules array, expect is only contains the invalid 504 | * fields and rules. Example: 505 | * [ 506 | * field_name => [ 507 | * rule_name => rule_value 508 | * ... 509 | * ] 510 | * ] 511 | * 512 | * @param $field string If empty, return the whole error array 513 | * @return array 514 | */ 515 | public function getErrors($field = '') 516 | { 517 | if ( ! is_string($field) ) { 518 | throw new \InvalidArgumentException(sprintf("Field name must be a string (%s given)", gettype($field))); 519 | } 520 | 521 | if ( ! $field ) { 522 | return $this->errors; 523 | } 524 | 525 | if ( isset($this->errors[$field]) ) { 526 | $errors = $this->errors[$field]; 527 | } 528 | else { 529 | $field = self::expandFieldName($field); 530 | $errors = $this->errors; 531 | foreach ( $field as $f ) { 532 | if ( ! isset($errors[$f]) ) { 533 | return array(); 534 | } 535 | $errors = $errors[$f]; 536 | } 537 | } 538 | 539 | return $errors; 540 | } 541 | 542 | /** 543 | * @internal 544 | * Used for testing and debugging 545 | */ 546 | public function setErrors(array $errors) 547 | { 548 | $this->errors = $errors; 549 | return $this; 550 | } 551 | 552 | /** 553 | * Return true if a field or the entire form has errors 554 | * 555 | * @param $field string optional 556 | * @return bool 557 | */ 558 | public function hasErrors($field = '') 559 | { 560 | if ( ! is_string($field) ) { 561 | throw new \InvalidArgumentException(sprintf("Field name must be a string (%s given)", gettype($field))); 562 | } 563 | 564 | $errors = $this->errors; 565 | 566 | if ( $field ) { 567 | $errors = $this->getErrors($field); 568 | } 569 | 570 | return ! empty($errors); 571 | } 572 | 573 | public function addError($message, $field = '', $param = true) 574 | { 575 | $this->errors[$field][$message] = $param; 576 | } 577 | 578 | /////////////////////////////////////////////////////////////////////////////// 579 | // VALIDATION 580 | 581 | /** 582 | * Validate a array of values, using the rules stored in the class. 583 | * The values that are not in the rules array will be ignored (and not saved in the class). 584 | * The values that are in the rules array will be saved in the class for 585 | * later access. 586 | * The validation errors will also be saved. 587 | * @return bool 588 | */ 589 | public function validate(array $values, array $opt = array()) 590 | { 591 | $opt = array_merge($this->options, $opt); 592 | 593 | // reset errors 594 | $this->errors = array(); 595 | $errors = array(); 596 | 597 | foreach ( $this->rules as $field => $rules ) { 598 | $value = null; 599 | 600 | // closure 601 | if ( is_callable($rules) ) { 602 | $rules = call_user_func_array($rules, array($this)); 603 | if ( is_array($rules) ) { 604 | $rules = $this->expandRulesArray($rules); 605 | } 606 | elseif ( $rules instanceof self ) { 607 | // do nothing 608 | } 609 | else { 610 | throw new \RuntimeException(sprintf( 611 | 'Rules closure for field %s must return an array of rules or a '.__CLASS__.' (%s returned)', 612 | $field, 613 | gettype($rules)) 614 | ); 615 | } 616 | } 617 | 618 | // subform => recursive check 619 | if ( $rules instanceof self ) { 620 | // use provided value if exists 621 | if ( array_key_exists($field, $values) ) { 622 | $value = $values[$field]; 623 | } 624 | 625 | if ( ! is_array($value) ) { 626 | $value = array(); 627 | } 628 | // set the parent so it's accessible from a callback function 629 | $rules->setParent($this); 630 | // pass default values to the subform 631 | $rules->setValues($this->getValue($field) ?: array()); 632 | // pass the value as it (the subform will take care of using default) 633 | $ret = $rules->validate($value, $opt); 634 | $value = $rules->getValues(); 635 | $errors = $rules->getErrors(); 636 | } 637 | // normal 638 | else { 639 | // use provided value if exists 640 | if ( array_key_exists($field, $values) ) { 641 | $value = $values[$field]; 642 | } 643 | // otherwise, use default if exists 644 | elseif ( $opt['use_default'] && array_key_exists($field, $this->values) ) { 645 | $value = $this->values[$field]; 646 | } 647 | 648 | $ret = $this->validateValue($value, $rules, $errors, $opt); 649 | } 650 | 651 | if ( $ret !== true ) { 652 | $this->errors[$field] = $errors; 653 | } 654 | 655 | // merge with the $values array of the class for later use (e.g. repopulate the form) 656 | $this->values[$field] = $value; 657 | } 658 | 659 | if ( ! $opt['ignore_extraneous'] ) { 660 | $extraneous = array_diff(array_keys($values), array_keys($this->rules)); 661 | foreach ( $extraneous as $field ) { 662 | $this->errors[$field] = array('extraneous' => true); 663 | } 664 | } 665 | 666 | return empty($this->errors); 667 | } 668 | 669 | /** 670 | * Validates one single value ($value) against a set of rules ($rules). 671 | * 672 | * This method is designed to be used internaly by validate(), but if needed 673 | * it can work an its own. 674 | * 675 | * @see validate() 676 | * @param $value mixed The value to be validated. This is a reference, as the 677 | * value can be altered (sanitized, casted, etc.) by 678 | * validators 679 | * @param $rules array An array of rules (must have been previously expanded) 680 | * @param $errors array (optional) An array where the errors will be returned 681 | * @param $opt array (optional) An array of options 682 | * @return bool 683 | */ 684 | public function validateValue(& $value, array $rules, array & $errors = array(), array $opt = array()) 685 | { 686 | $opt = array_merge($this->options, $opt); 687 | 688 | $errors = array(); 689 | 690 | // check if value is required. 691 | // if the value is NOT required and NOT present, we do no run any other validator 692 | if ( Rule\is_empty($value) ) { 693 | $required = false; 694 | if ( array_key_exists('required', $rules) ) { 695 | $required = $rules['required']; 696 | if ( is_callable($required) ) { 697 | $required = call_user_func_array($required, array($this)); 698 | } 699 | } 700 | 701 | // cast to an array if necessary 702 | if ( isset($rules[self::EACH]) ) { 703 | $value = array(); 704 | } 705 | 706 | if ( $required ) { 707 | $errors['required'] = true; 708 | if ( $opt['stop_on_error'] ) { 709 | return false; 710 | } 711 | } 712 | elseif ( $opt['allow_empty'] ) { 713 | return true; 714 | } 715 | // else we pass the value through the validators, even if it's empty 716 | } 717 | unset($rules['required']); 718 | 719 | foreach ( $rules as $rule => $param ) { 720 | $local_errors = array(); 721 | $ret = true; 722 | 723 | // special iterative validator for arrays 724 | if ( $rule === self::EACH ) { 725 | $ret = $this->validateMultipleValues($value, $param, $local_errors, $opt); 726 | } 727 | else { 728 | $func = __NAMESPACE__.'\Rule\\'.$rule; 729 | if ( function_exists($func) ) { 730 | if ( is_callable($param) ) { 731 | $param = call_user_func_array($param, array($this)); 732 | } 733 | 734 | if ( $param === true ) { // use default value from the rule 735 | $ret = call_user_func_array($func, array(&$value)); 736 | } else { 737 | $ret = call_user_func_array($func, array(&$value, $param)); 738 | } 739 | } 740 | // callback function (custom validator) 741 | elseif ( is_callable($param) ) { 742 | $ret = call_user_func_array($param, array(&$value, $this)); 743 | $param = true; // I don't want to set a callback into the errors array 744 | } 745 | else { 746 | throw new \InvalidArgumentException("Rule '$rule' not found"); 747 | } 748 | 749 | $local_errors = $param; 750 | } 751 | 752 | // if the validator failed, we store the name of the validator in the $errors array 753 | if ( $ret === false ) { 754 | if ( $rule === self::EACH ) { 755 | // skip each validator 756 | $errors += $local_errors; 757 | } 758 | else { 759 | $errors[$rule] = $local_errors; 760 | } 761 | if ( $opt['stop_on_error'] ) { 762 | return false; 763 | } 764 | } 765 | } 766 | 767 | // return empty($errors) ? true : $errors; 768 | return empty($errors); 769 | } 770 | 771 | /** 772 | * Validate a value that is expected to be an array of values ($values) against 773 | * a set of rules ($rules) or a subform 774 | * 775 | * This method is designed to be used internaly by validate(), but if needed 776 | * it can work an its own. 777 | * 778 | * @param $values mixed The value to validate, should be an array or will be 779 | * casted 780 | * @param $rules mixed An array of rules (expanded), or a subform 781 | * @param $errors array (optional) An array where the errors will be returned 782 | * @param $opt array (optional) An array of options 783 | * @return bool 784 | */ 785 | public function validateMultipleValues(& $values, $rules, array & $errors = array(), array $opt = array()) 786 | { 787 | $opt = array_merge($this->options, $opt); 788 | 789 | $errors = array(); 790 | 791 | // if the value is not an array, cast it 792 | if ( ! is_array($values) ) { 793 | $values = array($values); 794 | } 795 | // validate against a set of rules 796 | if ( is_array($rules) ) { 797 | $local_errors = array(); 798 | foreach ( $values as $key => &$value ) { 799 | $ret = $this->validateValue($value, $rules, $local_errors, $opt); 800 | if ( $ret !== true ) { 801 | $errors[$key] = $local_errors; 802 | } 803 | } 804 | } 805 | // validate against a subform (to validate array of assoc arrays) 806 | elseif ( $rules instanceof self ) { 807 | // set the parent so it's accessible from a callback function 808 | $rules->setParent($this); 809 | 810 | // subform => recursive check 811 | foreach ( $values as $key => &$value ) { 812 | if ( ! is_array($value) ) { 813 | $value = array(); 814 | } 815 | // default values are not passed here, because in the context of an array 816 | // we do not merge (it's how it behaves with a normal "each") 817 | $rules->setValues(array()); 818 | $ret = $rules->validate($value, $opt); 819 | $value = $rules->getValues(); 820 | if ( $ret !== true ) { 821 | $errors[$key] = $rules->getErrors(); 822 | } 823 | } 824 | } 825 | else { 826 | throw new \InvalidArgumentException("$rules must be an array or a instance of ".__CLASS__); 827 | } 828 | 829 | // return true because $errors is already filled 830 | // we dont want validate() to fill it again 831 | return empty($errors); 832 | } 833 | 834 | /////////////////////////////////////////////////////////////////////////////// 835 | // SUB-FORMS HELPERS 836 | 837 | public function setParent(self $parent) 838 | { 839 | $this->parent = $parent; 840 | } 841 | 842 | public function getParent() 843 | { 844 | return $this->parent; 845 | } 846 | } -------------------------------------------------------------------------------- /tests/ValidatorTest.php: -------------------------------------------------------------------------------- 1 | setOptions(array( 15 | 'ignore_extraneous' => false, 16 | 'allow_empty' => false 17 | )); 18 | $option = $form->getOptions(); 19 | $this->assertFalse($option['ignore_extraneous']); 20 | $this->assertFalse($option['allow_empty']); 21 | 22 | $form->setOptions(array( 23 | 'ignore_extraneous' => true, 24 | )); 25 | $option = $form->getOptions(); 26 | $this->assertTrue($option['ignore_extraneous']); 27 | $this->assertFalse($option['allow_empty']); 28 | } 29 | 30 | /////////////////////////////////////////////////////////////////////////////// 31 | // Rules 32 | 33 | public function validRules() 34 | { 35 | // return compressed and uncompressed 36 | return array( 37 | array( 38 | array('required', 'min_length' => 2), 39 | array('required' => true, 'min_length' => 2) 40 | ), 41 | array( 42 | array('each' => array('required')), 43 | array('each' => array('required' => true)) 44 | ) 45 | ); 46 | } 47 | 48 | public function invalidArguments() 49 | { 50 | return array( 51 | array(''), 52 | array(null), 53 | array(array()), 54 | array(new stdClass), 55 | array(42), 56 | array((double) 42) 57 | ); 58 | } 59 | 60 | public function invalidRules() 61 | { 62 | $rules = array(); 63 | foreach ( $this->invalidArguments() as $arg ) 64 | { 65 | $rules[] = array($arg); 66 | } 67 | return $rules; 68 | } 69 | 70 | /** 71 | * @dataProvider validRules 72 | */ 73 | public function testExpandRules($compressed, $uncompressed) 74 | { 75 | $this->assertEquals($uncompressed, Validator::expandRulesArray($compressed)); 76 | } 77 | 78 | /** 79 | * @dataProvider invalidRules 80 | * @expectedException InvalidArgumentException 81 | */ 82 | public function testExpandRulesInvalid($rules) 83 | { 84 | Validator::expandRulesArray($rules); 85 | } 86 | 87 | /** 88 | * @dataProvider validRules 89 | */ 90 | public function testGetRules($compressed, $uncompressed) 91 | { 92 | $form = new Validator(); 93 | $this->assertEquals(array(), $form->getRules()); 94 | $this->assertEquals(array(), $form->getRules('unset_field')); 95 | 96 | $rules = array('my_field' => $uncompressed); 97 | $form = new Validator($rules); 98 | $this->assertEquals($rules, $form->getRules()); 99 | $this->assertEquals($rules['my_field'], $form->getRules('my_field')); 100 | $this->assertEquals(array(), $form->getRules('unset_field')); 101 | } 102 | 103 | /** 104 | * @dataProvider invalidArguments 105 | * @expectedException InvalidArgumentException 106 | */ 107 | public function testGetRulesInvalidArguments($argument) 108 | { 109 | // this is allowed in getRules() 110 | if ( $argument === '' ) { 111 | throw new InvalidArgumentException(); 112 | } 113 | 114 | $form = new Validator(); 115 | $form->getRules($argument); 116 | } 117 | 118 | public function testGetRulesNested() 119 | { 120 | $validator = new Validator([ 121 | 'a' => new Validator([ 122 | 'b' => ['required'] 123 | ]) 124 | ]); 125 | $this->assertEquals(new Validator([ 126 | 'b' => ['required'] 127 | ]), $validator->getRules('a')); 128 | 129 | $this->assertEquals(['required' => true], $validator->getRules('a[b]')); 130 | $this->assertEquals([], $validator->getRules('a[b][c]')); 131 | $this->assertEquals([], $validator->getRules('a[c]')); 132 | $this->assertEquals(true, $validator->getRules('a[b][required]')); // unintended side effect 133 | } 134 | 135 | /** 136 | * @depends testGetRules 137 | * @dataProvider validRules 138 | */ 139 | public function testSetRules($compressed, $uncompressed) 140 | { 141 | $form = new Validator(); 142 | $form->setRules(array('name' => $compressed)); 143 | $this->assertEquals(array('name' => $uncompressed), $form->getRules()); 144 | 145 | $form = new Validator(); 146 | $form->setRules('name', $compressed); 147 | $this->assertEquals(array('name' => $uncompressed), $form->getRules()); 148 | $this->assertEquals($uncompressed, $form->getRules('name')); 149 | } 150 | 151 | public function testSetRulesSubValidator() 152 | { 153 | $form = new Validator(); 154 | $form->setRules('address', new Validator([ 155 | 'street' => ['required'], 156 | 'postcode' => ['required'] 157 | ])); 158 | } 159 | 160 | /** 161 | * @dataProvider invalidRules 162 | * @expectedException InvalidArgumentException 163 | */ 164 | public function testSetRulesInvalid($rules) 165 | { 166 | $form = new Validator(); 167 | $form->setRules($rules); 168 | } 169 | 170 | /** 171 | * @dataProvider invalidRules 172 | * @expectedException InvalidArgumentException 173 | */ 174 | public function testSetRulesFieldInvalid($rules) 175 | { 176 | $form = new Validator(); 177 | $form->setRules('name', $rules); 178 | } 179 | 180 | /** 181 | * @dataProvider invalidArguments 182 | * @expectedException BadMethodCallException 183 | */ 184 | public function testSetRulesInvalidArguments($argument) 185 | { 186 | // this is valid 187 | if ( $argument == array() || $argument === '' ) { 188 | throw new BadMethodCallException(); 189 | } 190 | 191 | $form = new Validator(); 192 | $form->setRules($argument); 193 | } 194 | 195 | /** 196 | * @dataProvider invalidArguments 197 | * @expectedException BadMethodCallException 198 | */ 199 | public function testSetRulesFieldInvalidArguments($argument) 200 | { 201 | // this is valid 202 | if ( $argument == array() || $argument === '') { 203 | throw new BadMethodCallException(); 204 | } 205 | 206 | $form = new Validator(); 207 | $form->setRules($argument, array()); 208 | } 209 | 210 | /** 211 | * @depends testGetRules 212 | */ 213 | public function testAddRules() 214 | { 215 | $form = new Validator([ 216 | 'first_name' => [] 217 | ]); 218 | $form->addRules([ 219 | 'last_name' => [] 220 | ]); 221 | $this->assertEquals([ 222 | 'first_name' => [], 223 | 'last_name' => [] 224 | ], $form->getRules()); 225 | 226 | $form = new Validator([ 227 | 'first_name' => ['required'] 228 | ]); 229 | $form->addRules([ 230 | 'first_name' => ['min_length' => 2] 231 | ]); 232 | $this->assertEquals([ 233 | 'first_name' => ['required' => true, 'min_length' => 2] 234 | ], $form->getRules()); 235 | 236 | $form = new Validator([ 237 | 'first_name' => ['min_length' => 2] 238 | ]); 239 | $form->addRules([ 240 | 'first_name' => ['required'] 241 | ]); 242 | $this->assertEquals([ 243 | 'first_name' => ['required' => true, 'min_length' => 2] 244 | ], $form->getRules()); 245 | } 246 | 247 | public function testAddRulesSubValidator() 248 | { 249 | $form = new Validator([ 250 | 'address' => new Validator([ 251 | 'street' => ['required'] 252 | ]) 253 | ]); 254 | $form->addRules([ 255 | 'first_name' => ['required'] 256 | ]); 257 | $this->assertEquals([ 258 | 'address' => new Validator([ 259 | 'street' => ['required' => true] 260 | ]), 261 | 'first_name' => ['required' => true] 262 | ], $form->getRules()); 263 | 264 | $form->getRules('address')->addRules([ 265 | 'postcode' => [] 266 | ]); 267 | $this->assertEquals([ 268 | 'address' => new Validator([ 269 | 'street' => ['required' => true], 270 | 'postcode' => [] 271 | ]), 272 | 'first_name' => ['required' => true] 273 | ], $form->getRules()); 274 | } 275 | 276 | /** 277 | * @dataProvider validRules 278 | */ 279 | public function testGetRuleValue($compressed, $uncompressed) 280 | { 281 | $rules = array('my_field' => $uncompressed); 282 | $form = new Validator($rules); 283 | 284 | foreach ( $uncompressed as $rule_name => $rule_value ) { 285 | $this->assertEquals($rule_value, $form->getRuleValue('my_field', $rule_name)); 286 | } 287 | } 288 | 289 | /** 290 | * @dataProvider invalidArguments 291 | * @expectedException InvalidArgumentException 292 | */ 293 | public function testGetRuleValueInvalidArguments1($argument) 294 | { 295 | $form = new Validator(); 296 | $form->getRuleValue($argument, 'required'); 297 | } 298 | 299 | /** 300 | * @dataProvider invalidArguments 301 | * @expectedException InvalidArgumentException 302 | */ 303 | public function testGetRuleValueInvalidArguments2($argument) 304 | { 305 | $form = new Validator(); 306 | $form->getRuleValue('my_field', $argument); 307 | } 308 | 309 | public function testGetRuleValueNested() 310 | { 311 | $validator = new Validator([ 312 | 'a' => new Validator([ 313 | 'b' => ['required', 'max_length' => 255] 314 | ]) 315 | ]); 316 | $this->assertEquals(255, $validator->getRuleValue('a[b]', 'max_length')); 317 | } 318 | 319 | public function testIsRequired() 320 | { 321 | $validator = new Validator([ 322 | 'a' => new Validator([ 323 | 'b' => ['required'], 324 | 'c' => [] 325 | ]), 326 | 'e' => ['required'] 327 | ]); 328 | 329 | $this->assertFalse($validator->isRequired('a')); 330 | $this->assertTrue($validator->isRequired('a[b]')); 331 | $this->assertFalse($validator->isRequired('a[c]')); 332 | $this->assertTrue($validator->isRequired('e')); 333 | } 334 | 335 | /** 336 | * @depends testSetRules 337 | */ 338 | public function testHasRules() 339 | { 340 | $form = new Validator(); 341 | $this->assertFalse($form->hasRules('name')); 342 | 343 | $form->setRules(array('name' => array())); 344 | $this->assertFalse($form->hasRules('name')); 345 | 346 | $form->setRules(array('name' => array('required'))); 347 | $this->assertTrue($form->hasRules('name')); 348 | } 349 | 350 | public function testHasRulesNested() 351 | { 352 | $validator = new Validator([ 353 | 'a' => new Validator([ 354 | 'b' => ['required'] 355 | ]) 356 | ]); 357 | $this->assertTrue($validator->hasRules('a[b]')); 358 | $this->assertFalse($validator->hasRules('a[b][c]')); 359 | } 360 | 361 | /////////////////////////////////////////////////////////////////////////////// 362 | // Values 363 | 364 | public function testGetSetValues() 365 | { 366 | $values = array( 367 | 'first_name' => 'John' 368 | ); 369 | 370 | $form = new Validator(); 371 | $form->setValues($values); 372 | $this->assertEquals($values, $form->getValues()); 373 | foreach ( $values as $field => $value ) { 374 | $this->assertEquals($value, $form->getValue($field)); 375 | $this->assertEquals($value, $form->$field); 376 | $this->assertEquals($value, $form[$field]); 377 | } 378 | 379 | $form = new Validator(); 380 | foreach ( $values as $field => $value ) { 381 | $form->$field = $value; 382 | $this->assertEquals($value, $form->getValue($field)); 383 | } 384 | 385 | $form = new Validator(); 386 | foreach ( $values as $field => $value ) { 387 | $form[$field] = $value; 388 | $this->assertEquals($value, $form->getValue($field)); 389 | } 390 | } 391 | 392 | /** 393 | * @dataProvider invalidArguments 394 | * @expectedException InvalidArgumentException 395 | */ 396 | public function testGetValueArguments($argument) 397 | { 398 | $form = new Validator(); 399 | $form->getValue($argument); 400 | } 401 | 402 | public function testGetValueNested() 403 | { 404 | $validator = new Validator([ 405 | 'a' => new Validator([ 406 | 'b' => ['required'] 407 | ]) 408 | ]); 409 | 410 | $validator->setValues([ 411 | 'a' => [ 412 | 'b' => 'Foobar' 413 | ] 414 | ]); 415 | 416 | $this->assertEquals(['b' => 'Foobar'], $validator->getValue('a')); 417 | $this->assertEquals('Foobar', $validator->getValue('a')['b']); 418 | $this->assertEquals('Foobar', $validator->a['b']); 419 | $this->assertEquals('Foobar', $validator['a']['b']); 420 | 421 | $this->assertEquals('Foobar', $validator->getValue('a[b]')); 422 | } 423 | 424 | /////////////////////////////////////////////////////////////////////////////// 425 | // Errors 426 | 427 | public function testGetSetErrors() 428 | { 429 | $form = new Validator(); 430 | $this->assertEquals(array(), $form->getErrors()); 431 | $this->assertEquals(array(), $form->getErrors('Some field')); 432 | 433 | $errors = array('first_name' => array('required' => true)); 434 | $form->setErrors($errors); 435 | $this->assertEquals($errors, $form->getErrors()); 436 | $this->assertEquals($errors['first_name'], $form->getErrors('first_name')); 437 | } 438 | 439 | /** 440 | * @dataProvider invalidArguments 441 | * @expectedException InvalidArgumentException 442 | */ 443 | public function testGetErrorsInvalidArguments($argument) 444 | { 445 | // this is valid 446 | if ( $argument === '' ) { 447 | throw new InvalidArgumentException(); 448 | } 449 | $form = new Validator(); 450 | $form->getErrors($argument); 451 | } 452 | 453 | public function testGetErrorsNested() 454 | { 455 | $validator = new Validator([ 456 | 'a' => new Validator([ 457 | 'b' => ['required'] 458 | ]) 459 | ]); 460 | 461 | $validator->validate([]); 462 | $this->assertEquals(['a' => ['b' => ['required' => true]]], $validator->getErrors()); 463 | $this->assertEquals(['b' => ['required' => true]], $validator->getErrors('a')); 464 | $this->assertEquals(['required' => true], $validator->getErrors('a[b]')); 465 | $this->assertEquals(true, $validator->getErrors('a[b][required]')); // unintended side effect 466 | } 467 | 468 | public function testGetErrorsNestedEach() 469 | { 470 | $validator = new Validator([ 471 | 'list' => ['each' => ['max' => 10]] 472 | ]); 473 | 474 | $this->assertFalse($validator->validate(['list' => [1,2,42]])); 475 | $this->assertEquals([2 => ['max' => 10]], $validator->getErrors('list')); 476 | $this->assertEquals(['max' => 10], $validator->getErrors('list[2]')); 477 | } 478 | 479 | /** 480 | * @depends testGetSetErrors 481 | */ 482 | public function testHasErrors() 483 | { 484 | $form = new Validator(); 485 | $this->assertFalse($form->hasErrors()); 486 | $this->assertFalse($form->hasErrors('first_name')); 487 | 488 | $form->setErrors(array('first_name' => array('required'))); 489 | $this->assertTrue($form->hasErrors()); 490 | $this->assertTrue($form->hasErrors('first_name')); 491 | } 492 | 493 | public function testHasErrorsNested() 494 | { 495 | $validator = new Validator([ 496 | 'a' => new Validator([ 497 | 'b' => ['required'] 498 | ]) 499 | ]); 500 | 501 | $validator->validate([]); 502 | $this->assertTrue($validator->hasErrors('a')); 503 | $this->assertTrue($validator->hasErrors('a[b]')); 504 | $this->assertTrue($validator->hasErrors('a[b][required]')); // unintended side effect 505 | } 506 | 507 | /////////////////////////////////////////////////////////////////////////////// 508 | // Validation options 509 | 510 | public function testUseDefault() 511 | { 512 | // not required 513 | $form = new Validator(array( 514 | 'id' => array() 515 | )); 516 | $this->assertTrue($form->validate(array())); 517 | $this->assertTrue($form->validate(array('id' => null))); 518 | $this->assertTrue($form->validate(array('id' => ''))); 519 | $this->assertTrue($form->validate(array('id' => array()))); 520 | $this->assertTrue($form->validate(array('id' => 0))); 521 | $this->assertTrue($form->validate(array('id' => false))); 522 | $this->assertTrue($form->validate(array('id' => '1'))); 523 | 524 | // required + no default 525 | $form = new Validator(array( 526 | 'id' => array('required') 527 | )); 528 | $this->assertFalse($form->validate(array())); 529 | $this->assertFalse($form->validate(array('id' => null))); 530 | $this->assertFalse($form->validate(array('id' => ''))); 531 | $this->assertFalse($form->validate(array('id' => array()))); 532 | $this->assertTrue($form->validate(array('id' => 0))); 533 | $this->assertTrue($form->validate(array('id' => false))); 534 | $this->assertTrue($form->validate(array('id' => '1'))); 535 | 536 | // required + default 537 | $form = new Validator(array( 538 | 'id' => array('required') 539 | )); 540 | $form->setValues(array('id' => 1)); 541 | $this->assertTrue($form->validate(array()), 'Use default value if missing'); 542 | $this->assertFalse($form->validate(array('id' => null))); 543 | $this->assertFalse($form->validate(array('id' => ''))); 544 | $this->assertFalse($form->validate(array('id' => array()))); 545 | $this->assertTrue($form->validate(array('id' => 0))); 546 | $this->assertTrue($form->validate(array('id' => false))); 547 | $this->assertTrue($form->validate(array('id' => '1'))); 548 | 549 | // strict required + default values 550 | $form = new Validator(array( 551 | 'id' => array('required') 552 | )); 553 | $form->setValues(array('id' => 1)); 554 | $this->assertFalse($form->validate(array(), array('use_default' => false)), 'Use default is missing disabled'); 555 | 556 | // default value not matching rules 557 | $form = new Validator(array('name' => array('required', 'min_length' => 2))); 558 | $form->setValues(array('name' => 'A')); 559 | $this->assertFalse($form->validate(array())); // false, the default value for name doesn't match 'min_length' 560 | } 561 | 562 | public function testAllowEmpty() 563 | { 564 | $test_rules = array( 565 | array('min_length' => 2), 566 | // array('each' => array('min_length' => 2)), 567 | array('date'), 568 | array('time'), 569 | ); 570 | foreach ( $test_rules as $rules ) { 571 | $form = new Validator(array( 572 | 'id' => $rules 573 | )); 574 | $this->assertTrue($form->validate(array('id' => '')), 'Empty value allowed by default'); 575 | $this->assertFalse($form->validate(array('id' => ''), array('allow_empty' => false)), sprintf( 576 | 'Empty value not allowed by default (rule %s)', 577 | json_encode($rules) 578 | )); 579 | } 580 | } 581 | 582 | public function testIgnoreExtraneous() 583 | { 584 | $values = array( 585 | 'a' => 1, 586 | 'b' => 2 587 | ); 588 | $form = new Validator(array( 589 | 'a' => array('required') 590 | )); 591 | $this->assertTrue($form->validate($values)); 592 | $this->assertEquals($values['a'], $form->a); 593 | $this->assertNull($form->b); 594 | 595 | $form->setValues(array()); 596 | $this->assertFalse($form->validate($values, array('ignore_extraneous' => false))); 597 | $this->assertTrue($form->hasErrors('b')); 598 | 599 | $form->setValues(array()); 600 | $form->setOptions(array('ignore_extraneous' => false)); 601 | $this->assertFalse($form->validate($values)); 602 | $this->assertTrue($form->hasErrors('b')); 603 | 604 | $form->setValues(array('c' => 3)); 605 | $form->setOptions(array('ignore_extraneous' => false)); 606 | $this->assertTrue($form->validate(array('a' => 1)), 'Ignore extraneous default'); 607 | } 608 | 609 | /////////////////////////////////////////////////////////////////////////////// 610 | // Validation process 611 | 612 | /** 613 | * @depends testGetSetValues 614 | */ 615 | public function testGetValueAfterValidation() 616 | { 617 | $valid_ids = array(1,2,3,4); 618 | $form = new Validator(array( 619 | 'id' => array('in' => $valid_ids) 620 | )); 621 | $this->assertTrue($form->validate(array('id' => 1))); 622 | $this->assertEquals(1, $form->id); 623 | 624 | $form->id = 2; 625 | $this->assertTrue($form->validate(array('id' => 1))); 626 | $this->assertEquals(1, $form->id, 'Default value has been replaced by validated value'); 627 | 628 | $form->id = 2; 629 | $this->assertFalse($form->validate(array('id' => 42))); 630 | $this->assertEquals(42, $form->id, 'Default value has been replaced by invalid value for repopulating the form'); 631 | 632 | $form->id = 2; 633 | $this->assertTrue($form->validate(array())); 634 | $this->assertEquals(2, $form->id, "Default value hasn't been touched when not present in array"); 635 | 636 | $form->id = 2; 637 | $this->assertTrue($form->validate(array('id' => null))); 638 | $this->assertEquals(null, $form->id); 639 | } 640 | 641 | public function testValidateValue() 642 | { 643 | $form = new Validator(); 644 | 645 | $value = ''; 646 | $errors = array(); 647 | $this->assertFalse($form->validateValue($value, array('required' => true), $errors)); 648 | $this->assertEquals(array('required' => true), $errors); 649 | 650 | $value = 'something'; 651 | $this->assertTrue($form->validateValue($value, array('required' => true), $errors)); 652 | $this->assertEmpty($errors); 653 | } 654 | 655 | public function testValidateMultipleValues() 656 | { 657 | $form = new Validator(); 658 | 659 | $value = array(''); 660 | $errors = array(); 661 | 662 | $this->assertFalse($form->validateMultipleValues($value, array('required' => true), $errors)); 663 | 664 | $value = array(1,2,'garbage'); 665 | $this->assertFalse($form->validateMultipleValues($value, array('numeric' => true), $errors)); 666 | 667 | $value = array(1,2,3); 668 | $this->assertTrue($form->validateMultipleValues($value, array('numeric' => true), $errors)); 669 | } 670 | 671 | /////////////////////////////////////////////////////////////////////////////// 672 | // Special validators 673 | 674 | public function testSubValidator() 675 | { 676 | $form = new Validator([ 677 | 'subform' => new Validator([ 678 | 'first_name' => ['required'], 679 | 'last_name' => ['required'] 680 | ]) 681 | ]); 682 | 683 | // valid data sets 684 | $expected_values = array('subform' => array('first_name' => 'John', 'last_name' => 'Wayne')); 685 | $this->assertTrue($form->validate(array('subform' => array('first_name' => 'John', 'last_name' => 'Wayne')))); 686 | $this->assertEquals($expected_values, $form->getValues()); 687 | 688 | $this->assertTrue($form->validate(array('subform' => array('first_name' => 'John', 'last_name' => 'Wayne', 'garbage' => 'garbage')))); 689 | $this->assertEquals($expected_values, $form->getValues()); 690 | 691 | // invalid data sets 692 | $form->setValues(array()); 693 | $this->assertFalse($form->validate(array())); 694 | $this->assertFalse($form->validate(array('subform' => array()))); 695 | $this->assertFalse($form->validate(array('subform' => array('last_name' => 'Wayne')))); 696 | $this->assertFalse($form->validate(array('subform' => array('first_name' => '', 'last_name' => 'Wayne')))); 697 | 698 | // subform + allow empty (i.e. test that values are passed to subform) 699 | $form->setValues(array('subform' => array('first_name' => 'John'))); 700 | $this->assertTrue($form->validate(array('subform' => array('last_name' => 'Doe'))), 'Values are passed to subform'); 701 | 702 | // subform + extraneous (i.e. test that we are careful in passing the values to the subform) 703 | $form->setValues(array( 704 | 'subform' => array('first_name' => 'John', 'email' => 'john@doe.com') 705 | )); 706 | $this->assertTrue($form->validate(array('subform' => array('last_name' => 'Doe')), array('ignore_extraneous' => false)), 'Subform also ignore extraneous default'); 707 | 708 | $form->setValues(array( 709 | 'subform' => array('first_name' => 'John', 'last_name' => 'Foobar', 'email' => 'john@doe.com') 710 | )); 711 | $this->assertTrue($form->validate(array(), array('ignore_extraneous' => false)), 'Subform also ignore extraneous default'); 712 | } 713 | 714 | public function testEach() 715 | { 716 | $form = new Validator(array( 717 | 'list' => array('each' => array('min_length' => 1, 'max_length' => 4)) 718 | )); 719 | 720 | $this->assertTrue($form->validate(array('list' => array('a','b','c')))); 721 | $this->assertEquals(array('list' => array('a','b','c')), $form->getValues()); 722 | 723 | $form->setValues(array()); 724 | $this->assertTrue($form->validate(array('garbage' => 'garbage'))); 725 | $this->assertEquals(array('list' => array()), $form->getValues(), 'List is set and casted to array when missing'); 726 | 727 | $this->assertFalse($form->validate(array('list' => 'garbage')), 'When not an array, autocasted, but does not validate because max_length failed'); 728 | $this->assertEquals([0 => ['max_length' => 4]], $form->getErrors('list')); 729 | 730 | $this->assertFalse($form->validate(array('list' => array('garbage'))), 'When an array with garbage, does not validate'); 731 | $this->assertEquals([0 => ['max_length' => 4]], $form->getErrors('list')); 732 | 733 | // with required = false and allow_empty = false 734 | $form->setValues(array()); 735 | $this->assertTrue($form->validate( 736 | array('list' => array()), 737 | array('allow_empty' => false) 738 | ), "Since it is not required, allow_empty won't be triggered"); 739 | 740 | // with required = true 741 | $form = new Validator(array( 742 | 'list' => array('required' => true, 'each' => array()) 743 | )); 744 | $this->assertTrue($form->validate(array('list' => 'garbage'))); 745 | $this->assertEquals(array('list' => array('garbage')), $form->getValues(), 'Original value is casted to an array'); 746 | 747 | $this->assertFalse($form->validate(array('list' => array())), 'Empty array does not pass required = true'); 748 | $this->assertEquals(array( 749 | 'required' => true 750 | ), $form->getErrors('list')); 751 | 752 | $form->setValues(array()); 753 | $this->assertFalse($form->validate(array('garbage' => 'garbage'))); 754 | $this->assertEquals(array('list' => array()), $form->getValues(), 'List is set and casted to array, when required and empty'); 755 | 756 | // used in conjuction with is_array type check 757 | $form = new Validator(array( 758 | 'list' => array('is_array', 'each' => array('min_length' => 1, 'max_length' => 4)) 759 | )); 760 | 761 | $this->assertFalse($form->validate(array('list' => 'garbage')), 'When not an array, does not validate'); 762 | $this->assertEquals(array('is_array' => true), $form->getErrors('list')); 763 | $this->assertEquals(array('list' => 'garbage'), $form->getValues(), 'Original value is not casted to an array when is_array is used'); 764 | 765 | // with required = true inside the each 766 | $form = new Validator(array( 767 | 'list' => array('each' => array('required' => true)) 768 | )); 769 | $this->assertTrue($form->validate(array('list' => array())), 'Ok for an empty array'); 770 | $form->setValues(array()); 771 | $this->assertFalse($form->validate(array('list' => array(''))), 'Not ok for an empty array with a empty string inside it'); 772 | $this->assertEquals([0 => array('required' => true)], $form->getErrors('list')); 773 | } 774 | 775 | public function testEachWithSubValidator() 776 | { 777 | $form = new Validator(array( 778 | 'list' => array('each' => new Validator(array( 779 | 'name' => array('required') 780 | ))) 781 | )); 782 | 783 | $this->assertTrue($form->validate(array( 784 | 'list' => array( 785 | array('name' => 'John'), 786 | array('name' => 'Jane') 787 | ) 788 | )), "Each can be used with a subform to validate a array of arrays"); 789 | 790 | $form->setValues(array()); 791 | $this->assertFalse($form->validate(array( 792 | 'list' => array( 793 | array('name' => 'John', 'age' => '12'), 794 | array('age' => '12') 795 | ) 796 | )), "Each can be used with a subform to validate a array of arrays (validation should fail)"); 797 | 798 | $this->assertEquals(array( 799 | 1 => array( 800 | 'name' => array( 801 | 'required' => true 802 | ) 803 | ) 804 | ), $form->getErrors('list'), 'Error array contains the offset'); 805 | 806 | // with default values 807 | $form->setValues(array( 808 | 'list' => array( 809 | 0 => array(), 810 | 1 => array('name' => 'Jane') 811 | ) 812 | )); 813 | $this->assertFalse($form->validate(array( 814 | 'list' => array( 815 | array('name' => 'John', 'age' => '12'), 816 | array('age' => '12') 817 | ) 818 | )), "Default values are not deep-merged (like for a normal each)"); 819 | 820 | $form = new Validator(array( 821 | 'list' => array('is_array', 'each' => new Validator(array( 822 | 'name' => array('required') 823 | ))) 824 | )); 825 | $this->assertFalse($form->validate(array('list' => 'foobar')), 'Not an array'); 826 | $this->assertTrue($form->validate(array('list' => array(array('name'=>'foobar')))), 'List is array and has a name, all good'); 827 | } 828 | 829 | public function testConditionalValue() 830 | { 831 | $form = new Validator(array( 832 | 'field' => array('required' => function($form) { return true; }) 833 | )); 834 | 835 | $this->assertTrue($form->validate(array('field' => 42)), 'Required evaluates to true'); 836 | $form->setValues(array()); 837 | $this->assertFalse($form->validate(array()), 'Required evaluates to true'); 838 | $this->assertFalse($form->validate(array('field' => '')), 'Required evaluates to true'); 839 | 840 | $form = new Validator(array( 841 | 'field' => array('required' => function($form) { return false; }) 842 | )); 843 | 844 | $this->assertTrue($form->validate(array('field' => 42)), 'Required evaluates to false'); 845 | $form->setValues(array()); 846 | $this->assertTrue($form->validate(array()), 'Required evaluates to false'); 847 | $form->setValues(array()); 848 | $this->assertTrue($form->validate(array('field' => '')), 'Required evaluates to false'); 849 | } 850 | 851 | public function testConditionalValueWithSubform() 852 | { 853 | $form = new Validator(array( 854 | 'main_field' => array('required'), 855 | 'options' => new Validator(array( 856 | 'sub_field' => array('required' => function($form) { 857 | return $form->getParent()->main_field == 42; 858 | }) 859 | )) 860 | )); 861 | 862 | $this->assertTrue($form->validate(array('main_field' => 42, 'options' => array('sub_field' => 1))), 'Required evaluates to true'); 863 | $form->setValues(array()); 864 | $this->assertFalse($form->validate(array('main_field' => 42, 'options' => array())), 'Required evaluates to true'); 865 | 866 | $form->setValues(array()); 867 | $this->assertTrue($form->validate(array('main_field' => 0, 'options' => array('sub_field' => 1))), 'Required evaluates to false'); 868 | $form->setValues(array()); 869 | $this->assertTrue($form->validate(array('main_field' => 0, 'options' => array())), 'Required evaluates to false'); 870 | } 871 | 872 | public function testConditionalRules() 873 | { 874 | $form = new Validator(array( 875 | 'field' => array(), 876 | 'options' => function($form) { 877 | return $form->field == "x" ? array("required" => true) : array(); 878 | } 879 | )); 880 | 881 | $this->assertTrue($form->validate(array('field' => 42))); 882 | $this->assertFalse($form->validate(array('field' => 'x')), 'Options is required when field = x'); 883 | } 884 | 885 | public function testCallback() 886 | { 887 | $form = new Validator(array( 888 | 'field' => array('callback' => function(&$value, $form) { 889 | $value = 42; 890 | $form->proof = "it worked!"; 891 | return true; 892 | }) 893 | )); 894 | 895 | $this->assertTrue($form->validate(array('field' => 1))); 896 | $this->assertEquals(42, $form->getValue('field'), 'Callback can modify value'); 897 | $this->assertEquals("it worked!", $form->getValue('proof'), 'Callback has access to form object'); 898 | 899 | $identical_password_validator = function($confirmation, $form) { 900 | return $form->password == $confirmation; 901 | }; 902 | 903 | $form = new Validator(array( 904 | 'password' => array('required', 'min_length' => 6), 905 | 'password_confirm' => array('required', 'identical' => $identical_password_validator) 906 | )); 907 | 908 | $this->assertTrue($form->validate(array('password' => 'abcdef', 'password_confirm' => 'abcdef'))); 909 | $this->assertFalse($form->validate(array('password' => 'abcdef', 'password_confirm' => ''))); 910 | $this->assertFalse($form->validate(array('password' => 'abcdef', 'password_confirm' => 'x'))); 911 | 912 | // order is important! 913 | $form = new Validator(array( 914 | 'password_confirm' => array('required', 'identical' => $identical_password_validator), 915 | 'password' => array('required', 'min_length' => 6) 916 | )); 917 | 918 | $this->assertFalse($form->validate(array('password' => 'abcdef', 'password_confirm' => 'abcdef'))); 919 | $this->assertFalse($form->validate(array('password' => 'abcdef', 'password_confirm' => ''))); 920 | $this->assertFalse($form->validate(array('password' => 'abcdef', 'password_confirm' => 'x'))); 921 | } 922 | 923 | /////////////////////////////////////////////////////////////////////////////// 924 | // Tests with various validators 925 | 926 | public function testDate() 927 | { 928 | $form = new Validator([ 929 | 'birthday' => ['date'] 930 | ]); 931 | $this->assertTrue($form->validate([ 932 | 'birthday' => '2000-01-01' 933 | ])); 934 | $this->assertFalse($form->validate([ 935 | 'birthday' => '01/01/2001' 936 | ])); 937 | 938 | $form = new Validator([ 939 | 'birthday' => ['date' => null] 940 | ]); 941 | $this->assertTrue($form->validate([ 942 | 'birthday' => '2000-01-01' 943 | ])); 944 | $this->assertTrue($form->validate([ 945 | 'birthday' => '01/01/2001' 946 | ])); 947 | } 948 | } 949 | --------------------------------------------------------------------------------