From 5d98b04c0c6a52bcda392c8c4e849c4db5992e4f Mon Sep 17 00:00:00 2001 From: inhere Date: Fri, 1 Dec 2017 18:01:37 +0800 Subject: [PATCH] merge master update to current branch --- README.md | 52 +++-- src/Filter/FilterList.php | 208 +++++++++++++------- src/Filter/Filtration.php | 3 +- src/Utils/DataFiltersTrait.php | 76 +++++-- src/Utils/UserAndContextValidatorsTrait.php | 20 +- src/ValidationTrait.php | 52 +++-- tests/FiltrationTest.php | 16 ++ tests/RuleValidationTest.php | 2 +- tests/test.sh | 4 - 9 files changed, 300 insertions(+), 133 deletions(-) delete mode 100644 tests/test.sh diff --git a/README.md b/README.md index 15e967a..6822be4 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,8 @@ ## 项目地址 -- **github** https://github.com/inhere/php-console.git -- **git@osc** https://gitee.com/inhere/php-console.git +- **github** https://github.com/inhere/php-validate.git +- **git@osc** https://gitee.com/inhere/php-validate.git **注意:** @@ -39,7 +39,7 @@ ```bash composer require inhere/php-validate -// composer require inhere/php-validate ^1.2 +// composer require inhere/php-validate ^version // 指定版本 ``` - 使用 composer.json @@ -62,7 +62,7 @@ git clone https://gitee.com/inhere/php-validate.git // git@osc ## 使用 - + ### 方式 1: 创建一个新的class,并继承Validation 创建一个新的class,并继承 `Inhere\Validate\Validation`。用于一个(或一系列相关)请求的验证, 相当于 laravel 的 表单请求验证 @@ -145,6 +145,7 @@ $safeData = $v->getSafeData(); // 验证通过的安全数据 $db->save($safeData); ``` + ### 方式 2: 直接使用类 Validation 需要快速简便的使用验证时,可直接使用 `Inhere\Validate\Validation` @@ -175,6 +176,7 @@ class SomeController } ``` + ### 方式 3: 创建一个新的class,使用 ValidationTrait 创建一个新的class,并使用 Trait `Inhere\Validate\ValidationTrait`。 此方式是高级自定义的使用方式, 可以方便的嵌入到其他类中 @@ -250,10 +252,12 @@ class UserController } ``` - ## 添加自定义验证器 -- 在继承了 `Inhere\Validate\Validation` 的子类添加验证方法. 请看上面的 **使用方式1** +- 在继承了 `Inhere\Validate\Validation` 的子类添加验证方法. 请看上面的 [使用方式1](#how-to-use1) + +> 注意: 写在当前类里的验证器方法必须带有后缀 `Validator`, 以防止对内部的其他的方法造成干扰 + - 通过 `Validation::addValidator()` 添加自定义验证器. e.g: ```php @@ -410,8 +414,21 @@ $v = Validation::make($_POST,[ ```php ['tagId,userId,freeTime', 'number', 'filter' => 'int'], ['field', 'validator', 'filter' => 'filter0|filter1...'], + +// 需要自定义性更高时,可以使用数组。 +['field1', 'validator', 'filter' => [ + 'string', + 'trim', + ['Class', 'method'], + ['Object', 'method'], + // 追加额外参数。 传入时,第一个参数总是要过滤的字段值,其余的依次追加 + 'myFilter' => ['arg1', 'arg2'], +]], ``` +> 注意: 写在当前类里的过滤器方法必须带有后缀 `Filter`, 以防止对内部的其他的方法造成干扰 + + > 过滤器请参看 http://php.net/manual/zh/filter.filters.sanitize.php ### 一个完整的规则示例 @@ -565,18 +582,21 @@ public function get(string $key, $default = null) `float` | 过滤非法字符,保留`float`格式的数据 | `['price', 'float', 'filter' => 'float'],` `string` | 过滤非法字符并转换为`string`类型 | `['userId', 'number', 'filter' => 'string'],` `trim` | 去除首尾空白字符,支持数组。 | `['username', 'min', 4, 'filter' => 'trim'],` -`lowercase` | 字符串转换为小写 | `['description', 'string', 'filter' => 'lowercase'],` -`uppercase` | 字符串转换为大写 | `['title', 'string', 'filter' => 'uppercase'],` -`snakeCase` | 字符串转换为蛇形风格 | `['title', 'string', 'filter' => 'snakeCase'],` -`camelCase` | 字符串转换为驼峰风格 | `['title', 'string', 'filter' => 'camelCase'],` +`lower/lowercase` | 字符串转换为小写 | `['description', 'string', 'filter' => 'lowercase'],` +`upper/uppercase` | 字符串转换为大写 | `['title', 'string', 'filter' => 'uppercase'],` +`snake/snakeCase` | 字符串转换为蛇形风格 | `['title', 'string', 'filter' => 'snakeCase'],` +`camel/camelCase` | 字符串转换为驼峰风格 | `['title', 'string', 'filter' => 'camelCase'],` `timestamp/strToTime` | 字符串日期转换时间戳 | `['pulishedAt', 'number', 'filter' => 'strToTime'],` `abs` | 返回绝对值 | `['field', 'int', 'filter' => 'abs'],` `url` | URL 过滤,移除所有不符合 URL 的字符 | `['field', 'url', 'filter' => 'url'],` `email` | email 过滤,移除所有不符合 email 的字符 | `['field', 'email', 'filter' => 'email'],` `encoded` | 去除 URL 编码不需要的字符,与 `urlencode()` 函数很类似 | `['imgUrl', 'url', 'filter' => 'encoded'],` +`clearTags/stripTags` | 相当于使用 `strip_tags()` | `['content', 'string', 'filter' => 'clearTags'],` `escape/specialChars` | 相当于使用 `htmlspecialchars()` 转义数据 | `['content', 'string', 'filter' => 'specialChars'],` `quotes` | 应用 `addslashes()` 转义数据 | `['content', 'string', 'filter' => 'quotes'],` +> php 内置的函数可直接使用。 e.g `string|ucfirst` + ## 内置的验证器 @@ -606,9 +626,9 @@ public function get(string $key, $default = null) `length` | 长度验证( 跟 `size`差不多, 但只能验证 `string`, `array` 的长度 | `['username', 'length', 'min' => 5, 'max' => 20]` `in/enum` | 枚举验证 | `['status', 'in', [1,2,3]` `notIn` | 枚举验证 | `['status', 'notIn', [4,5,6]]` -`mustBe` | 必须是等于给定值 | `['status', 'mustBe', 0]` +`mustBe` | 必须是等于给定值 | `['status', 'mustBe', 1]` `notBe` | 不能等于给定值 | `['status', 'notBe', 0]` -`compare/same/equal` | 字段值比较 | `['passwd', 'compare', 'repasswd']` +`compare/same/equal` | 字段值相同比较 | `['passwd', 'compare', 'repasswd']` `notEqual` | 字段值不能相同比较 | `['passwd', 'notEqual', 'repasswd']` `required` | 要求此字段/属性是必须的 | `['tagId, userId', 'required' ]` `requiredIf` | 指定的其它字段( anotherField )值等于任何一个 value 时,此字段为 **必填** | `['city', 'requiredIf', 'myCity', ['chengdu'] ]` @@ -633,7 +653,7 @@ public function get(string $key, $default = null) `md5` | 验证是否是 md5 格式的字符串 | `['passwd', 'md5']` `sha1` | 验证是否是 sha1 格式的字符串 | `['passwd', 'sha1']` `color` | 验证是否是html color | `['backgroundColor', 'color']` -`regex/regexp` | 使用正则进行验证 | `['name', 'regexp', '/^\w+$/']` +`regex/regexp` | 使用正则进行验证 | `['name', 'regexp', '/^\w+$/']` `safe` | 用于标记字段是安全的,无需验证 | `['createdAt, updatedAt', 'safe']` ### `safe` 验证器,标记属性/字段是安全的 @@ -661,13 +681,13 @@ $v = Validation::make($_POST, [ // ... ``` -### 一些补充说明 +### (注意)一些补充说明 - **请将 `required*` 系列规则写在规则列表的最前面** - 关于布尔值验证 * 如果是 "1"、"true"、"on" 和 "yes",则返回 TRUE * 如果是 "0"、"false"、"off"、"no" 和 "",则返回 FALSE -- `size/range` `length` 可以只定义 min 最小值。 但是 **当定义了max 值时,必须同时定义最小值** +- `size/range` `length` 可以只定义 `min` 最小值。 但是 **当定义了 `max` 值时,必须同时定义最小值** - 支持对数组的子级值验证 ```php @@ -685,8 +705,8 @@ $v = Validation::make($_POST, [ ['goods.pear', 'max', 30], //goods 下的 pear 值最大不能超过 30 ``` -- 验证大小范围 `int` 是比较大小。 `string` 和 `array` 是检查长度。大小范围 是包含边界值的 - `required*` 系列规则参考自 laravel +- 验证大小范围 `int` 是比较大小。 `string` 和 `array` 是检查长度。大小范围 是包含边界值的 ## 代码示例 diff --git a/src/Filter/FilterList.php b/src/Filter/FilterList.php index 2a2fda1..5bdf94b 100644 --- a/src/Filter/FilterList.php +++ b/src/Filter/FilterList.php @@ -1,5 +1,4 @@ ' 允许

+ * @return string + */ + public static function stripTags($val, $allowedTags = null) + { + if (!$val || !\is_string($val)) { + return ''; + } + + return $allowedTags ? strip_tags($val, $allowedTags) : strip_tags($val); } /** * 去除 URL 编码不需要的字符。 * @note 与 urlencode() 函数很类似。 - * @param string $var 要过滤的数据 + * @param string $val 要过滤的数据 * @param int $flags 标志 * FILTER_FLAG_STRIP_LOW - 去除 ASCII 值在 32 以下的字符 * FILTER_FLAG_STRIP_HIGH - 去除 ASCII 值在 32 以上的字符 @@ -201,69 +269,72 @@ public static function strToTime($var) * FILTER_FLAG_ENCODE_HIGH - 编码 ASCII 值在 32 以上的字符 * @return mixed */ - public static function encoded($var, $flags = 0) + public static function encoded($val, $flags = 0) { $settings = []; + if ((int)$flags !== 0) { $settings['flags'] = (int)$flags; } - return filter_var($var, FILTER_SANITIZE_ENCODED, $settings); + return filter_var($val, FILTER_SANITIZE_ENCODED, $settings); } /** * 应用 addslashes() 转义数据 - * @param string $var + * @param string $val * @return string */ - public static function quotes($var) + public static function quotes($val) { - return filter_var($var, FILTER_SANITIZE_MAGIC_QUOTES); + return filter_var($val, FILTER_SANITIZE_MAGIC_QUOTES); } /** * like htmlspecialchars(), HTML 转义字符 '"<>& 以及 ASCII 值小于 32 的字符。 - * @param string $var + * @param string $val * @param int $flags 标志 * FILTER_FLAG_STRIP_LOW - 去除 ASCII 值在 32 以下的字符 * FILTER_FLAG_STRIP_HIGH - 去除 ASCII 值在 32 以上的字符 * FILTER_FLAG_ENCODE_HIGH - 编码 ASCII 值在 32 以上的字符 * @return string */ - public static function specialChars($var, $flags = 0) + public static function specialChars($val, $flags = 0) { $settings = []; + if ((int)$flags !== 0) { $settings['flags'] = (int)$flags; } - return filter_var($var, FILTER_SANITIZE_SPECIAL_CHARS, $settings); + return filter_var($val, FILTER_SANITIZE_SPECIAL_CHARS, $settings); } /** - * @param $var + * @param $val * @param int $flags * @return string */ - public static function escape($var, $flags = 0) + public static function escape($val, $flags = 0) { - return self::specialChars($var, $flags); + return self::specialChars($val, $flags); } /** * HTML 转义字符 '"<>& 以及 ASCII 值小于 32 的字符。 - * @param string $var + * @param string $val * @param int $flags 标志 FILTER_FLAG_NO_ENCODE_QUOTES * @return string */ - public static function fullSpecialChars($var, $flags = 0) + public static function fullSpecialChars($val, $flags = 0) { $settings = []; + if ((int)$flags !== 0) { $settings['flags'] = (int)$flags; } - return filter_var($var, FILTER_SANITIZE_FULL_SPECIAL_CHARS, $settings); + return filter_var($val, FILTER_SANITIZE_FULL_SPECIAL_CHARS, $settings); } /** @@ -286,22 +357,22 @@ public static function stringCute($string, $start = 0, $end = null) /** * url地址过滤 移除所有不符合 url 的字符 * @note 该过滤器允许所有的字母、数字以及 $-_.+!*'(),{}|\^~[]`"><#%;/?:@&= - * @param string $var 要过滤的数据 + * @param string $val 要过滤的数据 * @return mixed */ - public static function url($var) + public static function url($val) { - return filter_var($var, FILTER_SANITIZE_URL); + return filter_var($val, FILTER_SANITIZE_URL); } /** * email 地址过滤 移除所有不符合 email 的字符 - * @param string $var 要过滤的数据 + * @param string $val 要过滤的数据 * @return mixed */ - public static function email($var) + public static function email($val) { - return filter_var($var, FILTER_SANITIZE_EMAIL); + return filter_var($val, FILTER_SANITIZE_EMAIL); } /** @@ -320,6 +391,7 @@ public static function email($var) public static function unsafeRaw($string, $flags = 0) { $settings = []; + if ((int)$flags !== 0) { $settings['flags'] = (int)$flags; } diff --git a/src/Filter/Filtration.php b/src/Filter/Filtration.php index 9cfc513..6beab9a 100644 --- a/src/Filter/Filtration.php +++ b/src/Filter/Filtration.php @@ -10,6 +10,7 @@ namespace Inhere\Validate\Filter; use Inhere\Validate\Utils\DataFiltersTrait; +use Inhere\Validate\Utils\Helper; /** * Class Filtration @@ -95,7 +96,7 @@ public function applyRules(array $rules = [], array $data = []) if (!($fields = $rule[0])) { continue; } - $fields = \is_string($fields) ? array_map('trim', explode(',', $fields)) : (array)$fields; + $fields = \is_string($fields) ? Helper::explode($fields) : (array)$fields; foreach ($fields as $field) { if (!isset($data[$field])) { $filtered[$field] = isset($rule['default']) ? $rule['default'] : null; diff --git a/src/Utils/DataFiltersTrait.php b/src/Utils/DataFiltersTrait.php index 52db852..baf6188 100644 --- a/src/Utils/DataFiltersTrait.php +++ b/src/Utils/DataFiltersTrait.php @@ -23,36 +23,42 @@ trait DataFiltersTrait */ private static $_filters = []; + /** * value sanitize 直接对给的值进行过滤 * @param mixed $value * @param string|array $filters + * string: + * 'string|trim|upper' + * array: + * [ + * 'string', + * 'trim', + * ['Class', 'method'], + * // 追加额外参数. 传入时,第一个参数总是要过滤的字段值,其余的依次追加 + * 'myFilter' => ['arg1', 'arg2'], + * ] * @return mixed * @throws \InvalidArgumentException */ protected function valueFiltering($value, $filters) { - $filters = \is_string($filters) ? array_map('trim', explode('|', $filters)) : $filters; - foreach ($filters as $filter) { - if (\is_object($filter) && method_exists($filter, '__invoke')) { + $filters = \is_string($filters) ? Helper::explode($filters, '|') : $filters; + + foreach ($filters as $key => $filter) { + // key is a filter. ['myFilter' => ['arg1', 'arg2']] + if (\is_string($key)) { + $args = (array)$filter; + $value = $this->callStringCallback($key, $value, ...$args); + + // closure + } elseif (\is_object($filter) && method_exists($filter, '__invoke')) { $value = $filter($value); + // string, trim, .... } elseif (\is_string($filter)) { - // if $filter is a custom add callback in the property {@see $_filters}. - if (isset(self::$_filters[$filter])) { - $callback = self::$_filters[$filter]; - $value = $callback($value); - // if $filter is a custom method of the subclass. - } elseif (method_exists($this, $filter)) { - $value = $this->{$filter}($value); - // $filter is a method of the class 'FilterList' - } elseif (method_exists(FilterList::class, $filter)) { - $value = FilterList::$filter($value); - // it is function name - } elseif (\function_exists($filter)) { - $value = $filter($value); - } else { - throw new \InvalidArgumentException("The filter [{$filter}] don't exists!"); - } + $value = $this->callStringCallback($filter, $value); + + // e.g ['Class', 'method'], } else { $value = Helper::call($filter, $value); } @@ -60,6 +66,38 @@ protected function valueFiltering($value, $filters) return $value; } + + /** + * @param $filter + * @param array ...$args + * @return mixed + */ + protected function callStringCallback($filter, ...$args) + { + // if $filter is a custom add callback in the property {@see $_filters}. + if (isset(self::$_filters[$filter])) { + $callback = self::$_filters[$filter]; + $value = $callback(...$args); + + // if $filter is a custom method of the subclass. + } elseif (method_exists($this, $filter . 'Filter')) { + $filter .= 'Filter'; + $value = $this->$filter(...$args); + + // $filter is a method of the class 'FilterList' + } elseif (method_exists(FilterList::class, $filter)) { + $value = FilterList::$filter(...$args); + + // it is function name + } elseif (\function_exists($filter)) { + $value = $filter(...$args); + } else { + throw new \InvalidArgumentException("The filter [$filter] don't exists!"); + } + + return $value; + } + /******************************************************************************* * custom filters ******************************************************************************/ diff --git a/src/Utils/UserAndContextValidatorsTrait.php b/src/Utils/UserAndContextValidatorsTrait.php index 798296a..00927cf 100644 --- a/src/Utils/UserAndContextValidatorsTrait.php +++ b/src/Utils/UserAndContextValidatorsTrait.php @@ -262,7 +262,7 @@ public function requiredWithoutAll($field, $fields) * @param string|array $suffixes e.g ['jpg', 'jpeg', 'png', 'gif', 'bmp'] * @return bool */ - public function file($field, $suffixes = null) + public function fileValidator($field, $suffixes = null) { if (!($file = isset($this->uploadedFiles[$field]) ? $this->uploadedFiles[$field] : null)) { return false; @@ -289,7 +289,7 @@ public function file($field, $suffixes = null) * @param string|array $suffixes e.g ['jpg', 'jpeg', 'png', 'gif', 'bmp'] * @return bool */ - public function image($field, $suffixes = null) + public function imageValidator($field, $suffixes = null) { if (!($file = isset($this->uploadedFiles[$field]) ? $this->uploadedFiles[$field] : null)) { return false; @@ -308,7 +308,7 @@ public function image($field, $suffixes = null) $mime = strtolower($imgInfo['mime']); // 支持不标准扩展名 // 是否是图片 - if (!\in_array($mime, Helper::IMG_MIME_TYPES, true)) { + if (!\in_array($mime, Helper::$imgMimeTypes, true)) { return false; } if (!$suffixes) { @@ -330,7 +330,7 @@ public function image($field, $suffixes = null) * @param string|array $types * @return bool */ - public function mimeTypes($field, $types) + public function mimeTypesValidator($field, $types) { if (!($file = isset($this->uploadedFiles[$field]) ? $this->uploadedFiles[$field] : null)) { return false; @@ -354,7 +354,7 @@ public function mimeTypes($field, $types) * @param string|array $types * return bool */ - public function mimes($field, $types = null) + public function mimesValidator($field, $types = null) { } /******************************************************************************* @@ -366,19 +366,19 @@ public function mimes($field, $types = null) * @param string $compareField * @return bool */ - public function compare($val, $compareField) + public function compareValidator($val, $compareField) { return $compareField && $val === $this->get($compareField); } - public function same($val, $compareField) + public function sameValidator($val, $compareField) { - return $this->compare($val, $compareField); + return $this->compareValidator($val, $compareField); } public function equal($val, $compareField) { - return $this->compare($val, $compareField); + return $this->compareValidator($val, $compareField); } /** @@ -387,7 +387,7 @@ public function equal($val, $compareField) * @param string $compareField * @return bool */ - public function notEqual($val, $compareField) + public function notEqualValidator($val, $compareField) { return $compareField && ($val !== $this->get($compareField)); } diff --git a/src/ValidationTrait.php b/src/ValidationTrait.php index 31abe3f..02f8df4 100644 --- a/src/ValidationTrait.php +++ b/src/ValidationTrait.php @@ -287,8 +287,8 @@ protected function valueValidate($data, $field, $value, $validator, $args) $callback = self::$_validators[$validator]; $passed = $callback($value, ...$args); // if $validator is a custom method of the subclass. - } elseif (method_exists($this, $validator)) { - $passed = $this->{$validator}($value, ...$args); + } elseif (method_exists($this, $method = $validator . 'Validator')) { + $passed = $this->{$method}($value, ...$args); // $validator is a method of the class 'ValidatorList' } elseif (method_exists(ValidatorList::class, $validator)) { $passed = ValidatorList::$validator($value, ...$args); @@ -312,6 +312,22 @@ protected function valueValidate($data, $field, $value, $validator, $args) return false; } + /** + * collect Safe Value + * @param string $field + * @param mixed $value + */ + protected function collectSafeValue($field, $value) + { + // 进行的是子级属性检查 eg: 'goods.apple' + if ($pos = strpos($field, '.')) { + $firstLevelKey = substr($field, 0, $pos); + $this->_safeData[$firstLevelKey] = $this->data[$firstLevelKey]; + } else { + $this->_safeData[$field] = $value; + } + } + /** * @param bool|false $clearErrors * @return $this @@ -349,7 +365,7 @@ protected function collectRules() $this->_usedRules[] = $rule; // only use to special scene. } else { - $sceneList = \is_string($rule['on']) ? array_map('trim', explode(',', $rule['on'])) : (array)$rule['on']; + $sceneList = \is_string($rule['on']) ? Helper::explode($rule['on']) : (array)$rule['on']; if ($scene && !\in_array($scene, $sceneList, true)) { continue; } @@ -358,27 +374,35 @@ protected function collectRules() } $fields = array_shift($rule); - (yield $fields => $rule); + (yield $fields => $this->prepareRule($rule)); } yield []; } /** - * collect Safe Value - * @param string $field - * @param mixed $value + * @param array $rule + * @return array */ - protected function collectSafeValue($field, $value) + protected function prepareRule(array $rule) { - // 进行的是子级属性检查 eg: 'goods.apple' - if ($pos = strpos($field, '.')) { - $firstLevelKey = substr($field, 0, $pos); - $this->_safeData[$firstLevelKey] = $this->data[$firstLevelKey]; - } else { - $this->_safeData[$field] = $value; + $validator = $rule[0]; + + switch ($validator) { + case 'size': + case 'range': + case 'string': + case 'between': + // fixed: 当只有 max 时,自动补充一个 min + if (isset($rule['max']) && !isset($rule['min'])) { + $rule['min'] = PHP_INT_MIN; + } + break; } + + return $rule; } + /******************************************************************************* * getter/setter ******************************************************************************/ diff --git a/tests/FiltrationTest.php b/tests/FiltrationTest.php index e7937f1..6bcdb84 100644 --- a/tests/FiltrationTest.php +++ b/tests/FiltrationTest.php @@ -19,13 +19,29 @@ public function testFiltration() $data = [ 'name' => ' tom ', 'status' => ' 23 ', + 'word' => 'word', + 'toLower' => 'WORD', + 'title' => 'helloWorld', ]; + $rules = [ ['name', 'string|trim'], + ['status', 'trim|int'], + ['word', 'string|trim|upper'], + ['toLower', 'lower'], + ['title', [ + 'string', + 'snake' => ['-'], + 'ucfirst', + ]], ]; $cleaned = Filtration::make($data, $rules)->filtering(); $this->assertSame($cleaned['name'], 'tom'); + $this->assertSame($cleaned['status'], 23); + $this->assertSame($cleaned['word'], 'WORD'); + $this->assertSame($cleaned['toLower'], 'word'); + $this->assertSame($cleaned['title'], 'Hello-world'); } } diff --git a/tests/RuleValidationTest.php b/tests/RuleValidationTest.php index 971b1ee..df5d79d 100644 --- a/tests/RuleValidationTest.php +++ b/tests/RuleValidationTest.php @@ -83,7 +83,7 @@ public function testValidateString() 'user_name' => $val ], [ ['user_name', 'string', 'min' => 6], - // ['user_name', 'string', 'max' => 16], + ['user_name', 'string', 'max' => 17], ])->validate(); $this->assertTrue($v->passed()); diff --git a/tests/test.sh b/tests/test.sh deleted file mode 100644 index 5a97045..0000000 --- a/tests/test.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -# phpunit6.phar --colors --coverage-html ./coverage/ -phpunit6.phar --colors --bootstrap tests/boot.php tests