diff --git a/.gitignore b/.gitignore index a6696cb8b762a42181e960cba8841bd2b12e6520..76a40980b440a32e3c74f66460702a20a0b54c96 100644 --- a/.gitignore +++ b/.gitignore @@ -7,12 +7,10 @@ temp/ runtime/ *.lock .DS_Store -Dockerfile -docker-compose.yml -phpunit.xml .phpstorm.meta* /.env /vendor -/.php_cs /.idea /composer.lock +/coverage/ +/cache/ diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000000000000000000000000000000000000..4ac4d5a53b96deb908b4566971cc02c4fa5d8fb8 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,56 @@ +files() + ->name('*.php') + ->exclude('vendor') + ->in(__DIR__) + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +$fixers = [ + '@PSR2' => true, + 'single_quote' => true, //简单字符串应该使用单引号代替双引号; + 'no_unused_imports' => true, //删除没用到的use + 'no_singleline_whitespace_before_semicolons' => true, //禁止只有单行空格和分号的写法; + 'no_empty_statement' => true, //多余的分号 + 'no_extra_blank_lines' => true, //多余空白行 + 'no_blank_lines_after_phpdoc' => true, //注释和代码中间不能有空行 + 'no_empty_phpdoc' => true, //禁止空注释 + 'phpdoc_indent' => true, //注释和代码的缩进相同 + 'no_blank_lines_after_class_opening' => true, //类开始标签后不应该有空白行; + 'include' => true, //include 和文件路径之间需要有一个空格,文件路径不需要用括号括起来; + 'no_trailing_comma_in_list_call' => true, //删除 list 语句中多余的逗号; + 'no_leading_namespace_whitespace' => true, //命名空间前面不应该有空格; + 'standardize_not_equals' => true, //使用 <> 代替 !=; + 'blank_line_after_opening_tag' => true, //PHP开始标记后换行 + 'indentation_type' => true, + 'concat_space' => [ + 'spacing' => 'one', + ], + 'space_after_semicolon' => [ + 'remove_in_empty_for_expressions' => true, + ], + 'header_comment' => [ + 'comment_type' => 'PHPDoc', + 'header' => "WeEngine System \r\n\r\n(c) We7Team 2021 \r\n\r\nThis is not a free software \r\nUsing it under the license terms\r\nvisited https://www.w7.cc for more details", + ], + 'binary_operator_spaces' => ['default' => 'align_single_space_minimal'], //等号对齐、数字箭头符号对齐 + 'whitespace_after_comma_in_array' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ternary_operator_spaces' => true, + 'yoda_style' => true, + 'normalize_index_brace' => true, + 'short_scalar_cast' => true, + 'function_typehint_space' => true, + 'function_declaration' => true, + 'return_type_declaration' => true + +]; +$config = new \PhpCsFixer\Config(); +return $config + ->setRules($fixers) + ->setFinder($finder) + ->setUsingCache(false); diff --git a/.php_cs b/.php_cs deleted file mode 100644 index 230a5887087446dc540b21b9600f4afbd555de6a..0000000000000000000000000000000000000000 --- a/.php_cs +++ /dev/null @@ -1,58 +0,0 @@ - - * - * This is not a free software - * Using it under the license terms - * visited https://www.w7.cc for more details - */ - -define('SOFT_NAME', 'WeEngine System'); - -$finder = PhpCsFixer\Finder::create() - ->files() - ->name('*.php') - ->exclude('vendor') - ->in(__DIR__) - ->ignoreDotFiles(true) - ->ignoreVCS(true); - -$fixers = [ - '@PSR2' => true, - 'single_quote' => true, //简单字符串应该使用单引号代替双引号; - 'no_unused_imports' => true, //删除没用到的use - 'no_singleline_whitespace_before_semicolons' => true, //禁止只有单行空格和分号的写法; - 'no_empty_statement' => true, //多余的分号 - 'no_extra_consecutive_blank_lines' => true, //多余空白行 - 'no_blank_lines_after_class_opening' => true, //类开始标签后不应该有空白行; - 'include' => true, //include 和文件路径之间需要有一个空格,文件路径不需要用括号括起来; - 'no_trailing_comma_in_list_call' => true, //删除 list 语句中多余的逗号; - 'no_leading_namespace_whitespace' => true, //命名空间前面不应该有空格; - 'standardize_not_equals' => true, //使用 <> 代替 !=; - 'blank_line_after_opening_tag' => true, //PHP开始标记后换行 - 'indentation_type' => true, - 'concat_space' => [ - 'spacing' => 'one', - ], - 'space_after_semicolon' => [ - 'remove_in_empty_for_expressions' => true, - ], - 'header_comment' => [ - 'comment_type' => 'PHPDoc', - 'header' => SOFT_NAME . " \r\n\r\n(c) We7Team 2019 \r\n\r\nThis is not a free software \r\nUsing it under the license terms\r\nvisited https://www.w7.cc for more details", - ], - // 'braces' => ['position_after_functions_and_oop_constructs' => 'same'], //设置大括号换行,暂时根本Psr - 'binary_operator_spaces' => ['default' => 'align_single_space_minimal'], //等号对齐、数字箭头符号对齐 - 'array_syntax' => ['syntax' => 'short'], - 'ternary_operator_spaces' => true, - 'yoda_style' => true, - -]; -return PhpCsFixer\Config::create() - ->setRules($fixers) - ->setFinder($finder) - ->setIndent("\t") - ->setUsingCache(false); diff --git a/README-EN.md b/README-EN.md new file mode 100644 index 0000000000000000000000000000000000000000..29b435cf4a5aaf2e5f3a63d2fcdf3d0f63f887f7 --- /dev/null +++ b/README-EN.md @@ -0,0 +1,240 @@ +
+

Micro Engine Form Validate

+ Stars + Forks +
+ License + PHP Version Support + FOSSA Status + Tests + Download +
+
+ +[中文](https://gitee.com/we7coreteam/w7-engine-validate/blob/4.x/README.md) | +English + +## Introduction +An extension to make your form validation easier, faster and more secure for all your validation needs. + +## Catalog +- [Validator](https://v.neww7.com/en/4/Validate.html) +- [Validate Scene](https://v.neww7.com/en/4/Scene.html) +- [Validate Events](https://v.neww7.com/en/4/Event.html) +- [Rule Manager](https://v.neww7.com/en/4/RuleManager.html) +- [Built-in Rules](https://v.neww7.com/en/4/BuiltRule.html) +- [Custom Rules](https://v.neww7.com/en/4/Rule.html) +- [Custom Error Messages](https://v.neww7.com/en/4/Message.html) +- [Data Default](https://v.neww7.com/en/4/Default.html) +- [Data Filter](https://v.neww7.com/en/4/Filter.html) +- [Validate Collection](https://v.neww7.com/en/4/Collection.html) + +## Install +using Composer +``` shell +composer require w7/engine-validate +``` + +## Simple Validate +Support for simply defining a validator and performing validation: +```php +try { + $data = Validate::make([ + 'user' => 'required|email', + 'pass' => 'required|lengthBetween:6,16', + ], [ + 'user.required' => 'Please enter your username', + 'user.email' => 'User name format error', + 'pass.required' => 'Please enter your password', + 'pass.lengthBetween' => 'Password length is 6~16 bits', + ])->check($data); +} catch (ValidateException $e) { + echo $e->getMessage(); +} +``` +If the validation passes, all values that passed the validation are returned, +if not, a `W7\Validate\Exception\ValidateException` exception is thrown + +## Validator Definition +To define the validator class for a specific validation scenario or data form, we need to inherit the `W7\Validate\Validate` class, +Then instantiate and call the `check` method of the validation class directly to complete the validation, an example of which is as follows. + +Define a `LoginValidate` validator class for login validation. +```php +class LoginValidate extends Validate +{ + protected $rule = [ + 'user' => 'required|email', + 'pass' => 'required|digits_between:6,16', + ]; + + protected $message = [ + 'user.required' => 'Please enter your username', + 'user.email' => 'Incorrect username format', + 'pass.required' => 'Please enter your password', + 'pass.digits_between' => 'Password length of 6 to 16 digits', + ]; +} + +``` +:::tip
Error messages defined by class attributes take precedence over the default responses in custom rules and over the errors returned by custom rule methods.
+::: + +## Data Validate +``` php +$data = [ + 'user' => '123@qq.com', + 'pass' => '' +]; +$validate = new LoginValidate(); +$validate->check($data); +``` +This throws a `W7\Validate\Exception\ValidateException` exception with the message `Please enter your password` +``` php +$data = [ + 'user' => '123@qq.com', + 'pass' => '123456' +]; +$validate = new LoginValidate(); +$data = $validate->check($data); +``` +Validates successfully and returns the validated value, which is an array type + +## Validate Arrays +It is not difficult to validate the form input as an array of fields. +You can use the "dot" method to validate properties in an array. +For example, if the incoming HTTP request contains the `search[keyword]` field, +it can be validated as follows. + +``` php +protected $rule = [ + 'search.order' => 'numeric|between:1,2', + 'search.keyword' => 'chsAlphaNum', + 'search.recycle' => 'boolean', +]; +``` +You can also validate each element in an array. For example, to validate that each id in a given array input field is unique, you can do this. + +``` php +protected $rule = [ + 'search.*.id' => 'numeric|unique:account' +]; +``` +The definition of an error message for an array rule is the same + +``` php +protected $message = [ + 'search.order.numeric' => 'order parameter error', + 'search.order.between' => 'order parameter error', + 'search.keyword.chsAlphaNum' => 'Keywords can only contain Chinese, letters, numbers', + 'search.recycle.boolean' => 'Parameter error:recycle', +]; +``` +## Validator Class Attributes +### $rule +The validation rules of the user-defined validator can also be set via the `setRules` method, +This method will superimpose the data.If the parameter is `null` then it is clear all rules. +```php +// Definition in class +protected $rule = [ + 'user' => 'required' +]; + +// Definition of usage methods +$v->setRules([ + 'user' => 'required' +]); +``` +### $message +User-defined validator error messages can also be set via the `setMessages` method, +This method will superimpose the data,If the parameter is `null` then it is clear all error messages. +```php +// Definition in class +protected $message = [ + 'user.required' => 'user is required' +]; + +// Definition of usage methods +$v->setMessages([ + 'user.required' => 'pass is required' +]); +``` +### $scene +Define the data of the validation scenario, which is used to specify the validation fields corresponding to the validation scenario, etc. +See the section on [validate scene](https://v.neww7.com/en/4/Scene.html) for detailed usage. + +The same can be done with the `setScene` method. +This method will superimpose the data, +If the parameter is `null`, then all validate scene are cleared. +```php +// Definition in class +protected $scene = [ + 'login' => ['user', 'pass'] +]; + +// Definition of usage methods +$v->setScene([ + 'login' => ['user', 'pass'] +]); +``` +### $event +Define global events under this validator, see the section [Events](https://v.neww7.com/en/4/Event.html) for detailed usage. +```php +protected $handler = [ + CheckSiteStatus::class +]; +``` +### $default +Defining the default value of a field +```php +protected $default = [ + 'name' => 'Emma' +]; +``` +For more information about default values, please see the section [Default Values](https://v.neww7.com/en/4/Default.html). +### $filter +For processing data after data validation +```php +protected $filter = [ + 'name' => 'trim' +]; +``` +For more information about filters, please see the [Filter](https://v.neww7.com/en/4/Filter.html) section. +### $customAttributes +Define the name of the validation field, also can be set by `setCustomAttributes` method, this method will superimpose the data, +if the parameter is `null` then it is clear all field names. +the [:attribute](https://v.neww7.com/en/4/Message.htm#attribute) in the error message will be replaced with the value corresponding to the following + +```php +protected $customAttributes = [ + 'user' => 'Account', + 'pass' => 'Password' +]; +``` +### $ruleMessage +Error messages for class method rules +```php + protected $ruleMessage = [ + 'The value of :attribute can only have Chinese' +]; +``` +Click to view [example](https://v.neww7.com/en/4/Rule.htm#using-rule-objects) +### $filled +All validated fields cannot be empty when they exist, if the value of this property is `true`, +all rules will automatically add `filled` rules, the default is `true` + +The `filled` rule is not automatically added when: +- Validation rules exist in `filled`, `nullable`, `accepted`, `present`,`required`, `required_if`, `required_unless`, `required_with`,`required_with_all`, `required_without`, `required_without_all` Rule +- Validation rules exist [extendImplicit](https://v.neww7.com/en/4/Rule.htm#extendimplicit) Defined rules +- Validation rules exist [extendImplicitRule](https://v.neww7.com/en/4/Rule.htm#defining-the-implicit-validator) Defined rules +- The validation rules implement the `Itwmw\Validation\Support\Interfaces\ImplicitRule` Marker Interface +```php +protected bool $filled = true; +``` +### $regex +Predefined regular expression validation rules, see [Regular Expression Rule](https://v.neww7.com/en/4/Rule.htm#regular-expression-rule) for details +```php +protected $regex = [ + 'number' => '/^\d+$/' +]; +``` \ No newline at end of file diff --git a/README.md b/README.md index d5ac1111c06852fef7029f8eb20d84483885d43f..459c3937c3ead652efc2e1791dbaa26b7dcd51e5 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,224 @@ -# 软擎验证扩展 +
+

微擎表单验证

+ Stars + Forks +
+ License + PHP Version Support + FOSSA Status + Tests + Download +
+
-## 说明 -此验证基于Laravel的Validator验证器,做了如下改变 - - 可通过类的方式定义一个[验证器](https://wiki.w7.cc/chapter/868?id=3234) - - 增加[验证场景](https://wiki.w7.cc/chapter/868?id=3233) - - 增加[场景事件处理](https://wiki.w7.cc/chapter/868?id=3232) - - 修改了[自定义验证规则](https://wiki.w7.cc/chapter/868?id=3231) +中文 | +[English](https://gitee.com/we7coreteam/w7-engine-validate/blob/4.x/README-EN.md) -此文档只说明与Laravel的Validator验证器不同的地方,完整Laravel Validator文档可查看:[完整文档](https://learnku.com/docs/laravel/6.x/validation/5144) +## 介绍 +一个让你的表单验证更为方便,快捷,安全的扩展,满足你的一切验证需求。 + +## 目录 +- [验证器](https://v.neww7.com/4/Validate.html) +- [验证场景](https://v.neww7.com/4/Scene.html) +- [场景事件](https://v.neww7.com/4/Event.html) +- [规则管理器](https://v.neww7.com/4/RuleManager.html) +- [内置规则](https://v.neww7.com/4/BuiltRule.html) +- [自定义验证规则](https://v.neww7.com/4/Rule.html) +- [自定义消息](https://v.neww7.com/4/Message.html) +- [默认值](https://v.neww7.com/4/Default.html) +- [过滤器](https://v.neww7.com/4/Filter.html) +- [验证集合](https://v.neww7.com/4/Collection.html) ## 安装 使用composer命令 ``` shell -composer require w7/rangine-validate +composer require w7/engine-validate +``` + +完整文档查看[完整文档](https://v.neww7.com) + +## 简单验证 +支持简单定义一个验证器并进行验证: +```php +try { + $data = Validate::make([ + 'user' => 'required|email', + 'pass' => 'required|lengthBetween:6,16', + ], [ + 'user.required' => '请输入用户名', + 'user.email' => '用户名格式错误', + 'pass.required' => '请输入密码', + 'pass.lengthBetween' => '密码长度为6~16位', + ])->check($data); +} catch (ValidateException $e) { + echo $e->getMessage(); +} +``` +如果验证通过,则返回所有通过验证的值,如未通过,则抛出一个`W7\Validate\Exception\ValidateException`异常 + +## 验证器定义 +为具体的验证场景或者数据表单定义验证器类,我们需要继承`W7\Validate\Validate`类,然后实例化后直接调用验证类的`check`方法即可完成验证,下面是一个例子: + +我们定义一个`LoginValidate`验证器类用于登录的验证。 +```php +class LoginValidate extends Validate +{ + protected $rule = [ + 'user' => 'required|email', + 'pass' => 'required|digits_between:6,16', + ]; + + protected $message = [ + 'user.required' => '请输入用户名', + 'user.email' => '用户名格式错误', + 'pass.required' => '请输入密码', + 'pass.digits_between' => '密码长度为6~16位', + ]; +} + +``` + +>
类属性定义的错误消息,优先级要高于自定义规则中的默认回复,高于自定义规则方法返回的错误
+ +## 数据验证 +``` php +$data = [ + 'user' => '123@qq.com', + 'pass' => '' +]; +$validate = new LoginValidate(); +$validate->check($data); +``` +此时会抛出一个`W7\Validate\Exception\ValidateException`异常,message为`请输入密码` +``` php +$data = [ + 'user' => '123@qq.com', + 'pass' => '123456' +]; +$validate = new LoginValidate(); +$data = $validate->check($data); +``` +验证成功,并返回通过验证的值,返回的值为数组类型 + +## 验证数组 +验证表单的输入为数组的字段也不难。你可以使用 「点」方法来验证数组中的属性。例如,如果传入的 HTTP 请求中包含`search[keyword]`字段, 可以如下验证: +``` php +protected $rule = [ + 'search.order' => 'numeric|between:1,2', + 'search.keyword' => 'chsAlphaNum', + 'search.recycle' => 'boolean', +]; ``` +你也可以验证数组中的每个元素。例如,要验证指定数组输入字段中的每一个 id 是唯一的,可以这么做: +``` php +protected $rule = [ + 'search.*.id' => 'numeric|unique:account' +]; +``` +数组规则的错误消息的定义也一样 +``` php +protected $message = [ + 'search.order.numeric' => '排序参数错误', + 'search.order.between' => '排序参数错误', + 'search.keyword.chsAlphaNum' => '关键词只能包含中文,字母,数字', + 'search.recycle.boolean' => '参数错误:recycle', +]; +``` +## 验证器类属性 +### $rule +用户定义验证器的验证规则,也可以通过`setRules`方法来进行设置,此方法为叠加,如果参数为`null`则为清空全部规则 +```php +// 类中定义 +protected $rule = [ + 'user' => 'required' +]; + +// 使用方法定义 +$v->setRules([ + 'user' => 'required' +]); +``` +### $message +用户定义验证器的错误信息,也可以通过`setMessages`方法来进行设置,此方法为叠加,如果参数为`null`则为清空全部错误消息 +```php +// 类中定义 +protected $message = [ + 'user.required' => '账号必须填写' +]; + +// 使用方法定义 +$v->setMessages([ + 'user.required' => '账号必须填写' +]); +``` +### $scene +定义验证场景的数据,用于指定验证场景对应的验证字段等,详细用法查看[验证场景](https://v.neww7.com/4/Scene.html)一节,同样也可以通过`setScene`方法来进行设置,此方法为叠加,如果参数为`null`则为清空全部验证场景 +```php +// 类中定义 +protected $scene = [ + 'login' => ['user', 'pass'] +]; + +// 使用方法定义 +$v->setScene([ + 'login' => ['user', 'pass'] +]); +``` +### $event +定义此验证器下的全局事件,详细用法查看[事件](https://v.neww7.com/4/Event.html)一节 +```php +protected $event = [ + CheckSiteStatus::class +]; +``` +### $customAttributes +定义验证字段的名称,也可以通过`setCustomAttributes`方法来进行设置,此方法为叠加,如果参数为`null`则为清空全部字段名称, +错误消息中的[:attribute](https://v.neww7.com/4/Message.html#attribute)会使用下面的值对应的替换 +```php +protected $customAttributes = [ + 'user' => '账号', + 'pass' => '密码' +]; +``` +### $default +定义字段的默认值 +```php +protected $default = [ + 'name' => '张三' +]; +``` +关于默认值的详情请查看[默认值](https://v.neww7.com/4/Default.html)一节 +### $filter +用于数据验证后处理数据 +```php +protected $filter = [ + 'name' => 'trim' +]; +``` +关于过滤器的详情请查看[过滤器](https://v.neww7.com/4/Filter.html)一节 +### $ruleMessage +类方法规则的错误信息 +```php + protected $ruleMessage = [ + ':attribute的值只能具有中文' +]; +``` +点击查看[示例](https://v.neww7.com/4/Rule.html#%E4%BD%BF%E7%94%A8%E8%A7%84%E5%88%99%E5%AF%B9%E8%B1%A1) +### $filled +所有验证的字段在存在时不能为空,如果此属性值为`true`,所有规则会自动增加`filled`规则,默认为`true` -完整文档查看[完整文档](https://wiki.w7.cc/chapter/868?id=3235) \ No newline at end of file +当出现以下情况时,不会自动添加`filled`规则 +- 验证规则中存在`filled`, `nullable`, `accepted`, `present`,`required`, `required_if`, `required_unless`, `required_with`,`required_with_all`, `required_without`, `required_without_all`规则 +- 验证规则存在[extendImplicit](https://v.neww7.com/4/Rule.html#extendimplicit-%E9%9A%90%E5%BC%8F%E6%89%A9%E5%B1%95)定义的规则 +- 验证规则存在[extendImplicitRule](https://v.neww7.com/4/Rule.html#%E5%AE%9A%E4%B9%89Implicit%E9%AA%8C%E8%AF%81%E5%99%A8)定义的规则 +- 验证规则实现了`Itwmw\Validation\Support\Interfaces\ImplicitRule`标记接口 +```php +protected bool $filled = true; +``` +### $regex +预定义正则表达式验证规则,详情查看[正则表达式规则](https://v.neww7.com/4/Rule.html#%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E8%A7%84%E5%88%99) +```php +protected $regex = [ + 'number' => '/^\d+$/' +]; +``` \ No newline at end of file diff --git a/composer.json b/composer.json index 4b207ac2fdb4ddf30d8131f9a731ebdca22948da..3995fd9e65937b0e2e80854977aa91809601fba5 100644 --- a/composer.json +++ b/composer.json @@ -1,14 +1,29 @@ { "name": "w7/engine-validate", - "require": { - "w7/rangine": "^2.3" - }, + "description": "增强表单验证", + "license": "Apache-2.0", "authors": [ { - "name": "邪少", - "email": "453618847@qq.com" + "name": "虞灪", + "email": "995645888@qq.com" } ], + "scripts": { + "test" : "phpunit --configuration phpunit.xml" + }, + "keywords": [ + "w7", + "validate", + "validator", + "form validate" + ], + "require": { + "php": "^7.3|^8.0", + "itwmw/validation": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^8|^9" + }, "autoload": { "psr-4": { "W7\\Validate\\" : "src/" @@ -16,5 +31,29 @@ "files": [ "src/Support/helpers.php" ] - } -} \ No newline at end of file + }, + "autoload-dev": { + "psr-4": { + "W7\\Tests\\" : "tests/" + } + }, + "extra": { + "rangine": { + "providers": [ + "W7\\Validate\\Providers\\Rangine\\ValidateProvider" + ] + }, + "laravel": { + "providers": [ + "W7\\Validate\\Providers\\Laravel\\ValidateProvider" + ] + }, + "think": { + "services": [ + "W7\\Validate\\Providers\\Think\\ValidateService" + ] + } + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000000000000000000000000000000000000..dc2d580360e65c29e87e4dd5015597958610c64b --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,31 @@ + + + + tests/Test + + + + + src + + + src/Providers + src/Exception + src/Support/Storage/ValidateConfig.php + + + + + + \ No newline at end of file diff --git a/src/Exception/CollectionException.php b/src/Exception/CollectionException.php new file mode 100644 index 0000000000000000000000000000000000000000..ce609a192662f83ff5a42d92b7f89f94780310e6 --- /dev/null +++ b/src/Exception/CollectionException.php @@ -0,0 +1,19 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Exception; + +use Exception; + +class CollectionException extends Exception +{ +} diff --git a/src/Exception/ValidateException.php b/src/Exception/ValidateException.php index 4c5569934b7de07f6c806a54c40e4cbc7ffc9fec..8ab9e1039e371393c14d9fc0f34f4bf7b8ff8f8c 100644 --- a/src/Exception/ValidateException.php +++ b/src/Exception/ValidateException.php @@ -1,5 +1,15 @@ + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + namespace W7\Validate\Exception; use Exception; @@ -7,17 +17,16 @@ use Throwable; class ValidateException extends Exception { - /** @var array */ - protected $data = []; + protected $attribute; - public function __construct($message = '', $code = 0, array $data = [], Throwable $previous = null) - { - $this->data = $data; - parent::__construct($message, $code, $previous); - } + public function __construct($message = '', $code = 0, string $attribute = '', Throwable $previous = null) + { + $this->attribute = $attribute; + parent::__construct($message, $code, $previous); + } - public function getData() - { - return $this->data; - } + public function getAttribute(): string + { + return $this->attribute; + } } diff --git a/src/Exception/ValidateRuntimeException.php b/src/Exception/ValidateRuntimeException.php new file mode 100644 index 0000000000000000000000000000000000000000..ee09e6c1ff9dff9ce02fa47be1efd7e03324e16e --- /dev/null +++ b/src/Exception/ValidateRuntimeException.php @@ -0,0 +1,19 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Exception; + +use RuntimeException; + +class ValidateRuntimeException extends RuntimeException +{ +} diff --git a/src/Providers/Laravel/PresenceVerifier.php b/src/Providers/Laravel/PresenceVerifier.php new file mode 100644 index 0000000000000000000000000000000000000000..8dee0dcf89a6fac30ea5f009b5495d10efe32a6c --- /dev/null +++ b/src/Providers/Laravel/PresenceVerifier.php @@ -0,0 +1,57 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Providers\Laravel; + +use Illuminate\Database\ConnectionResolverInterface; +use Itwmw\Validation\Support\Interfaces\PresenceVerifierInterface; + +class PresenceVerifier implements PresenceVerifierInterface +{ + protected $db; + + protected $connectionResolver; + + public function __construct(ConnectionResolverInterface $db) + { + $this->connectionResolver = $db; + } + + public function table(string $table): PresenceVerifierInterface + { + $this->db = $this->connectionResolver->table($table)->useWritePdo(); + return $this; + } + + public function setConnection($connection) + { + $this->connectionResolver->connection($connection); + return $this; + } + + public function where(string $column, $operator = null, $value = null, string $boolean = 'and'): PresenceVerifierInterface + { + $this->db->where($column, $operator, $value, $boolean); + return $this; + } + + public function whereIn(string $column, $values, string $boolean = 'and'): PresenceVerifierInterface + { + $this->db->whereIn($column, $values, $boolean); + return $this; + } + + public function count(string $columns = '*'): int + { + return $this->db->count($columns); + } +} diff --git a/src/Providers/Laravel/ValidateProvider.php b/src/Providers/Laravel/ValidateProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..43249cfed854c8ce4935087d6c9947a7b8d93dc6 --- /dev/null +++ b/src/Providers/Laravel/ValidateProvider.php @@ -0,0 +1,27 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Providers\Laravel; + +use Illuminate\Support\ServiceProvider; +use Itwmw\Validation\Factory; +use W7\Validate\Support\Storage\ValidateConfig; + +class ValidateProvider extends ServiceProvider +{ + public function boot() + { + $factory = new Factory(null, str_replace('-', '_', config('app.locale'))); + $factory->setPresenceVerifier(new PresenceVerifier($this->app->get('db'))); + ValidateConfig::instance()->setFactory($factory); + } +} diff --git a/src/Providers/Rangine/ValidateProvider.php b/src/Providers/Rangine/ValidateProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..951663cf34468f1d67c2b2485ad635c4748513e6 --- /dev/null +++ b/src/Providers/Rangine/ValidateProvider.php @@ -0,0 +1,35 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Providers\Rangine; + +use Itwmw\Validation\Factory; +use W7\Core\Provider\ProviderAbstract; +use W7\Facade\Config; +use W7\Facade\Container; +use W7\Validate\Providers\Laravel\PresenceVerifier; +use W7\Validate\Support\Storage\ValidateConfig; + +class ValidateProvider extends ProviderAbstract +{ + /** + * Register any application services. + * + * @return void + */ + public function boot() + { + $factory = new Factory(null, str_replace('-', '_', Config::get('app.setting.lang', 'zh_cn'))); + $factory->setPresenceVerifier(new PresenceVerifier(Container::get('db-factory'))); + ValidateConfig::instance()->setFactory($factory); + } +} diff --git a/src/Providers/Think/PresenceVerifier.php b/src/Providers/Think/PresenceVerifier.php new file mode 100644 index 0000000000000000000000000000000000000000..cbfa36f5a94c11fd6bfa672282c30bc79e998371 --- /dev/null +++ b/src/Providers/Think/PresenceVerifier.php @@ -0,0 +1,69 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Providers\Think; + +use think\facade\Db; +use Itwmw\Validation\Support\Interfaces\PresenceVerifierInterface; + +class PresenceVerifier implements PresenceVerifierInterface +{ + protected $db; + + public function __construct() + { + $this->db = Db::newQuery(); + } + + public function setConnection(?string $connection) + { + if (!empty($connection)) { + $this->db->connect($connection); + } + } + + public function table(string $table): PresenceVerifierInterface + { + $this->db->table($table); + return $this; + } + + public function where(string $column, $operator = null, $value = null, string $boolean = 'and'): PresenceVerifierInterface + { + switch (strtolower($boolean)) { + case 'and': + $this->db->where($column, $operator, $value); + break; + case 'or': + $this->db->whereOr($column, $operator, $value); + break; + case 'xor': + $this->db->whereXor($column, $operator, $value); + break; + default: + throw new \RuntimeException('操作错误'); + } + + return $this; + } + + public function whereIn(string $column, $values, string $boolean = 'and'): PresenceVerifierInterface + { + $this->db->whereIn($column, $values, $boolean); + return $this; + } + + public function count(string $columns = '*'): int + { + return $this->db->count($columns); + } +} diff --git a/src/Providers/Think/ValidateService.php b/src/Providers/Think/ValidateService.php new file mode 100644 index 0000000000000000000000000000000000000000..57cd70e2f5c8e4679b6c11580c9660d0a3088933 --- /dev/null +++ b/src/Providers/Think/ValidateService.php @@ -0,0 +1,28 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Providers\Think; + +use Itwmw\Validation\Factory; +use think\facade\Config; +use think\Service; +use W7\Validate\Support\Storage\ValidateConfig; + +class ValidateService extends Service +{ + public function register() + { + $factory = new Factory(null, str_replace('-', '_', Config::get('lang.default_lang'))); + $factory->setPresenceVerifier(new PresenceVerifier()); + ValidateConfig::instance()->setFactory($factory); + } +} diff --git a/src/RuleManager.php b/src/RuleManager.php new file mode 100644 index 0000000000000000000000000000000000000000..26000042d08cf05163a96894f7ab24624f4d28c7 --- /dev/null +++ b/src/RuleManager.php @@ -0,0 +1,641 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate; + +use Closure; +use W7\Validate\Support\Common; +use W7\Validate\Support\Rule\BaseRule; +use W7\Validate\Support\RuleManagerScene; +use W7\Validate\Support\Storage\ValidateConfig; +use Itwmw\Validation\Support\Interfaces\ImplicitRule; + +/** + * @link https://v.neww7.com/en/3/RuleManager.html#introduction + */ +class RuleManager +{ + /** + * All original validation rules + * + * @link https://v.neww7.com/en/3/Validate.html#rule + * @var array + */ + protected $rule = []; + + /** + * Define a scenario for the validation rule + * + * @link https://v.neww7.com/en/3/Validate.html#scene + * @var array + */ + protected $scene = []; + + /** + * The array of custom attribute names. + * + * @var array + */ + protected $customAttributes = []; + + /** + * The array of custom error messages. + * + * @link https://v.neww7.com/en/3/Validate.html#message + * @var array + */ + protected $message = []; + + /** + * Messages for class method rules + * + * @var array + */ + protected $ruleMessage = []; + + /** + * Regular validation rules + * + * @link https://v.neww7.com/en/3/Validate.html#regex + * @var array + */ + protected $regex = []; + + /** + * Current validate scene + * + * @var string|null + */ + private $currentScene = null; + + /** + * Rules for using regular expressions for validation + * + * @var string[] + */ + private $regexRule = ['regex', 'not_regex']; + + /** + * Extension method name + * + * @var array + */ + private static $extendName = []; + + /** + * Implicit extension method name + * + * @var array + */ + private static $implicitRules = []; + + /** + * Scene Management Class + * + * @var string + */ + protected $sceneProvider = RuleManagerScene::class; + + /** + * Set current validate scene + * + * @param string|null $name + * @return $this + */ + final public function scene(?string $name): RuleManager + { + $this->currentScene = $name; + return $this; + } + + /** + * Get the name of the current validate scene + * + * @return string|null + */ + public function getCurrentSceneName(): ?string + { + return $this->currentScene; + } + + /** + * Get initial rules provided + * + * @param string|null $sceneName The scene name, or the current scene name if not provided. + * @return array + */ + public function getInitialRules(?string $sceneName = ''): array + { + if ('' === $sceneName) { + $sceneName = $this->currentScene; + } + + if (empty($sceneName)) { + return $this->rule; + } + + if (method_exists($this, 'scene' . ucfirst($sceneName))) { + $scene = new $this->sceneProvider($this->rule); + call_user_func([$this, 'scene' . ucfirst($sceneName)], $scene); + return $scene->getRules(); + } + + if (isset($this->scene[$sceneName])) { + return array_intersect_key($this->rule, array_flip((array) $this->scene[$sceneName])); + } + + return $this->rule; + } + + /** + * Converting raw rules into rules for checking data + * + * @param array|null $rules If this parameter is not provided, it will be retrieved by default from the `getRules` method + * @return array|array[] + */ + public function getCheckRules(?array $rules = null): array + { + if (is_null($rules)) { + $rules = $this->getInitialRules(); + } + $rulesFields = array_keys($rules); + $rule = array_map(function ($rules, $field) { + if (!is_array($rules)) { + $rules = explode('|', $rules); + } + + return array_map(function ($ruleName) use ($field) { + if (is_string($ruleName)) { + foreach ($this->regexRule as $regexRuleName) { + $regexRuleName = $regexRuleName . ':'; + if (0 === strpos($ruleName, $regexRuleName)) { + $regexName = substr($ruleName, strlen($regexRuleName)); + if (isset($this->regex[$regexName])) { + return $regexRuleName . $this->regex[$regexName]; + } + } + } + + $ruleClass = $this->getRuleClass($ruleName); + if (false !== $ruleClass) { + if (!empty($message = $this->getMessage($field, $ruleName))) { + $ruleClass->setMessage($message); + } + return $ruleClass; + } + + return $this->getExtendsRule($ruleName, $field); + } + + return $ruleName; + }, $rules); + }, $rules, $rulesFields); + + return array_combine($rulesFields, $rule); + } + + /** + * Get the instance class of a custom rule + * + * @param string $ruleName Custom Rule Name + * @return false|BaseRule + */ + private function getRuleClass(string $ruleName) + { + static $rulesClass = []; + + list($ruleName, $param) = Common::getKeyAndParam($ruleName, true); + + if (isset($rulesClass[$ruleName]) && 0 === $rulesClass[$ruleName]) { + return false; + } + + foreach (ValidateConfig::instance()->getRulePath() as $rulesPath) { + $ruleNameSpace = $rulesPath . ucfirst($ruleName); + if (isset($rulesClass[$ruleNameSpace])) { + return new $ruleNameSpace(...$param); + } elseif (class_exists($ruleNameSpace) && is_subclass_of($ruleNameSpace, BaseRule::class)) { + $rulesClass[$ruleNameSpace] = 1; + return new $ruleNameSpace(...$param); + } + } + + $rulesClass[$ruleName] = 0; + return false; + } + + /** + * Register a custom validator extension. + * + * @param string $rule Rule Name + * @param Closure|string|array $extension Closure rules, providing four parameters:$attribute, $value, $parameters, $validator + * @param string|null $message Error Message + */ + public static function extend(string $rule, $extension, ?string $message = null) + { + self::validatorExtend('', $rule, $extension, $message); + } + + /** + * Register a custom implicit validator extension. + * + * @param string $rule Rule Name + * @param Closure|string|array $extension Closure rules, providing four parameters:$attribute, $value, $parameters, $validator + * @param string|null $message Error Message + */ + public static function extendImplicit(string $rule, $extension, ?string $message = null) + { + self::validatorExtend('Implicit', $rule, $extension, $message); + } + + /** + * Register a custom dependent validator extension. + * + * @param string $rule Rule Name + * @param Closure|string|array $extension Closure rules, providing four parameters:$attribute, $value, $parameters, $validator + * @param string|null $message Error Message + */ + public static function extendDependent(string $rule, $extension, ?string $message = null) + { + self::validatorExtend('Dependent', $rule, $extension, $message); + } + + /** + * Register a custom validator message replacer. + * + * @param string $rule Rule Name + * @param string|Closure $replacer Closure rules, providing four parameters:$message,$attribute,$rule,$parameters + */ + public static function replacer(string $rule, $replacer) + { + ValidateConfig::instance()->getFactory()->replacer($rule, $replacer); + } + + /** + * Register for custom validator extensions + * + * @param string $type Type + * @param string $rule Rule Name + * @param Closure|string|array $extension Closure rules, providing four parameters:$attribute, $value, $parameters, $validator + * @param string|null $message Error Messages + * @param boolean $avoidDuplication Avoid duplication caused by multiple validators with the same rule name + */ + protected static function validatorExtend(string $type, string $rule, $extension, ?string $message = null, bool $avoidDuplication = false) + { + if ($avoidDuplication) { + // Multiple rule managers using the same rule will result in the later methods not taking effect. + // So here a unique rule name is generated based on the namespace. + $ruleName = md5(get_called_class() . $rule); + + if (array_key_exists($rule, self::$extendName)) { + array_push(self::$extendName[$rule], $ruleName); + self::$extendName[$rule] = array_unique(self::$extendName[$rule]); + } else { + self::$extendName[$rule] = [$ruleName]; + } + } else { + $ruleName = $rule; + } + + if (!empty($type)) { + $method = 'extend' . $type; + } else { + $method = 'extend'; + } + + if ('Implicit' === $type) { + self::$implicitRules[] = $ruleName; + } + + ValidateConfig::instance()->getFactory()->$method($ruleName, $extension, $message); + } + + /** + * Get extension rules + * + * The method names are processed due to the need to distinguish the same custom method names for multiple validators. + * This method is used in order to make the rules correspond to the processed method names. + * @param string $ruleName + * @param string|null $field + * @return string + */ + private function getExtendsRule(string $ruleName, string $field = null): string + { + list($rule, $param) = Common::getKeyAndParam($ruleName, false); + + // Retrieve the real custom rule method name, and modify the corresponding error message + $ruleName = md5(get_called_class() . $rule); + if (array_key_exists($rule, self::$extendName) && in_array($ruleName, self::$extendName[$rule])) { + // Determine if an error message is defined for a custom rule method + if (null !== $field && isset($this->message[$field . '.' . $rule])) { + $this->message[$field . '.' . $ruleName] = $this->message[$field . '.' . $rule]; + } + + $rule = $ruleName; + } else { + // If it does not exist in the current custom rule, determine if it is a class method + // If it is a class method, register the rule to the rule manager first, + // and then process the corresponding error message + if (method_exists($this, 'rule' . ucfirst($rule))) { + if (array_key_exists(lcfirst($rule), $this->ruleMessage)) { + $message = $this->ruleMessage[lcfirst($rule)]; + } elseif (array_key_exists(ucfirst($rule), $this->ruleMessage)) { + $message = $this->ruleMessage[ucfirst($rule)]; + } else { + $message = ''; + } + + self::validatorExtend('', $rule, Closure::fromCallable([$this, 'rule' . ucfirst($rule)]), $message, true); + + if ('' !== $param) { + $rule = $rule . ':' . $param; + } + + return $this->getExtendsRule($rule, $field); + } + } + + if ('' !== $param) { + $rule = $rule . ':' . $param; + } + return $rule; + } + + /** + * Add the `filled` rule + * + * @param array $rules Original Rules + * @return array + */ + protected function addFilledRule(array $rules): array + { + $conflictRules = [ + 'filled', 'nullable', 'accepted', 'present', 'required', 'required_if', 'required_unless', 'required_with', + 'required_with_all', 'required_without', 'required_without_all', + ]; + + foreach ($rules as &$rule) { + $rulesName = array_map(function ($value) { + if (is_object($value)) { + // By default, when an attribute being validated is not present or contains an empty string, + // normal validation rules, including custom extensions, are not run. + // If the ImplicitRule interface is implemented, + // it means that the rule object needs to be run even if the property is empty. + // So there is no need for the `filled` rule either, + // so let there be a `filled` in the array object to skip this process. + if ($value instanceof ImplicitRule) { + return 'filled'; + } else { + return ''; + } + } + + if (is_string($value)) { + $ruleName = Common::getKeyAndParam($value)[0]; + + if (in_array($ruleName, self::$implicitRules)) { + return 'filled'; + } + } + + return $value; + }, $rule); + + if (empty(array_intersect($conflictRules, $rulesName))) { + array_unshift($rule, 'filled'); + } + } + + return $rules; + } + + /** + * Set validator scene data (overlay) + * + * @param array|null $scene [Scene => [Field]] If $Scene is null, clear all validation scenes + * @return static + */ + public function setScene(?array $scene = null): RuleManager + { + if (is_null($scene)) { + $this->scene = []; + } else { + $this->scene = array_merge($this->scene, $scene); + } + + return $this; + } + + /** + * Set validator rules (overlay) + * + * @param array|null $rules [field => rules] If $rules is null, clear all validation rules + * @return $this + */ + public function setRules(?array $rules = null): RuleManager + { + if (is_null($rules)) { + $this->rule = []; + } else { + $this->rule = array_merge($this->rule, $rules); + } + + return $this; + } + + /** + * Get the specified rule + * + * @param string|string[]|null $field Field name or array of field names.If $field is null, then return all rules + * If a scene value is set, the specified rule is retrieved from the current scene. + * + * @param bool $initial Whether to get the original rule, default is false + * @return array + */ + public function getRules($field = null, bool $initial = false): array + { + $rules = $this->getInitialRules(); + if (null !== $field) { + $field = is_array($field) ? $field : [$field]; + $rules = Common::getRulesAndFill($rules, $field); + } + + if (false === $initial) { + $rules = $this->getCheckRules($rules); + } + + return $rules; + } + + /** + * Set the Message(overlay) + * + * @param array|null $message [Field. Rule => validation message] If $message is null,clear all validation messages + * @return $this + */ + public function setMessages(?array $message = null): RuleManager + { + if (is_null($message)) { + $this->message = []; + } else { + $this->message = array_merge($this->message, $message); + } + + return $this; + } + + /** + * Get the defined error message + * + * + * If you want to get the error messages after validate, use the `getMessages` method of the message processor + * + *

If you have defined an extension rule using the {@see RuleManager}, + * you need to call the `getCheckRules` method first before calling this method. + * Otherwise, the error message may not match the extension rule name.

+ * + * @param string $key Full message key + * @param string|null $rule If the first value is a field name, the second value is a rule, otherwise leave it blank + * @return array|string|null + */ + protected function getMessage(string $key, ?string $rule = null) + { + if (null !== $rule) { + $key = Common::makeMessageName($key, $rule); + } + + return $this->message[$key] ?? ''; + } + + /** + * Get the defined error message + * + * @param string|string[]|null $key Full message key or field name or array of fields + * + * @param string|null $rule If $key is a field name string and this parameter is filled in with the + * corresponding rule under that field, then this method returns the message corresponding to $key$.rule. + * + * When this parameter is provided and $key is a string, the $subMessage parameter fails + * + * @param bool $subMessage Whether to retrieve all error messages under the specified field, default is false + * + * When this parameter is true, the $rule parameter is disabled + * @return array|mixed|string + */ + public function getMessages($key = null, ?string $rule = null, bool $subMessage = false) + { + if (null === $key) { + return $this->message; + } + if (null !== $rule && is_string($key)) { + $key = Common::makeMessageName($key, $rule); + return $this->message[$key] ?? ''; + } + + if ($subMessage) { + $fields = is_array($key) ? $key : [$key]; + return array_filter($this->message, function ($value, $key) use ($fields) { + foreach ($fields as $field) { + if (0 === strrpos($key, $field)) { + return true; + } + } + return false; + }, ARRAY_FILTER_USE_BOTH); + } + + if (is_array($key)) { + return array_intersect_key($this->message, array_flip($key)); + } + + return $this->message[$key] ?? ''; + } + + /** + * Set the custom attributes(overlay) + * + * @param array|null $customAttributes [fields => names] If $customAttributes is null, clear all field names + * @return $this + */ + public function setCustomAttributes(?array $customAttributes = null): RuleManager + { + if (is_null($customAttributes)) { + $this->customAttributes = []; + } else { + $this->customAttributes = array_merge($this->customAttributes, $customAttributes); + } + + return $this; + } + + /** + * Get array of custom attribute. + * + * @param null|string|array $fields Field name or array of fields, or if null, return all custom attribute + * @return array + */ + public function getCustomAttributes($fields = null): array + { + if (null === $fields) { + return $this->customAttributes; + } + + $fields = is_array($fields) ? $fields : [$fields]; + + return array_intersect_key($this->customAttributes, array_flip($fields)); + } + + /** + * Get rules, error messages and custom attribute + * + * Note: This method is not affected by the validate {@see scene} + * + * @param null|string|array $fields Field name or array of fields, or if null, return all + * @param bool $initial Whether to get the original rule, default is false + * @return array Returns three arrays of rules, error messages, and custom attribute + */ + public static function get($fields = null, bool $initial = false): array + { + $validate = new static(); + $rules = $validate->getRules($fields, $initial); + $message = $validate->getMessages($fields, null, true); + $customAttributes = $validate->getCustomAttributes($fields); + + return [$rules, $message, $customAttributes]; + } + + /** + * Get rules, error messages and custom attribute by scene name + * + * @param string $sceneName + * @param bool $initial Whether to get the original rule, default is false + * @return array + */ + public static function getBySceneName(string $sceneName, bool $initial = false): array + { + $validate = new static(); + $rules = $validate->getInitialRules($sceneName); + $fields = array_keys($rules); + $message = $validate->getMessages($fields, null, true); + $customAttributes = $validate->getCustomAttributes($fields); + if (false === $initial) { + $rules = $validate->getCheckRules($rules); + } + return [$rules, $message, $customAttributes]; + } + + public static function __callStatic($name, $arguments) + { + $initial = count($arguments) > 0 ? $arguments[0] : false; + return self::getBySceneName($name, $initial); + } +} diff --git a/src/Support/Common.php b/src/Support/Common.php new file mode 100644 index 0000000000000000000000000000000000000000..3996a86a3bc67fc9cb486815b002e665c508b0ba --- /dev/null +++ b/src/Support/Common.php @@ -0,0 +1,71 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support; + +class Common +{ + /** + * Get the name and parameters of the rule + * + * @param string $value Complete Rules + * @param bool $parsing Whether to parse parameters, default is false + * @return array + */ + public static function getKeyAndParam(string $value, bool $parsing = false): array + { + $param = ''; + if (false !== strpos($value, ':')) { + $arg = explode(':', $value, 2); + $key = $arg[0]; + if (count($arg) >= 2) { + $param = $arg[1]; + } + } else { + $key = $value; + } + + if ($parsing) { + $param = explode(',', $param); + } + return [$key, $param]; + } + + /** + * Name of the generated error message + * + * @param string $fieldName + * @param string $rule + * @return string + */ + public static function makeMessageName(string $fieldName, string $rule): string + { + if (false !== strpos($rule, ':')) { + $rule = substr($rule, 0, strpos($rule, ':')); + } + return $fieldName . '.' . $rule; + } + + /** + * Get rules for a given field and populate non-existent fields + * + * @param array $rules + * @param array $fields + * @return array + */ + public static function getRulesAndFill(array $rules, array $fields): array + { + $rules = array_intersect_key($rules, array_flip($fields)); + $notExist = array_diff($fields, array_keys($rules)); + return array_merge($rules, array_fill_keys($notExist, [])); + } +} diff --git a/src/Support/Concerns/DefaultInterface.php b/src/Support/Concerns/DefaultInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..edfa416d07b1488eacf6f1ceca480b0c5497c944 --- /dev/null +++ b/src/Support/Concerns/DefaultInterface.php @@ -0,0 +1,18 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support\Concerns; + +interface DefaultInterface +{ + public function handle($value, string $attribute, array $originalData); +} diff --git a/src/Support/Concerns/FilterInterface.php b/src/Support/Concerns/FilterInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..fb23c880caba49a428812675f56c36178fbe0856 --- /dev/null +++ b/src/Support/Concerns/FilterInterface.php @@ -0,0 +1,18 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support\Concerns; + +interface FilterInterface +{ + public function handle($value); +} diff --git a/src/Support/Concerns/MessageProviderInterface.php b/src/Support/Concerns/MessageProviderInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..f98f250cf15df701c930389740e8b66a3ac5c856 --- /dev/null +++ b/src/Support/Concerns/MessageProviderInterface.php @@ -0,0 +1,76 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support\Concerns; + +use W7\Validate\RuleManager; + +interface MessageProviderInterface +{ + /** + * Handling a message + * + * @param string $messages + * @return string + */ + public function handleMessage(string $messages): string; + + /** + * Get a validation message for the processed attributes and rules. + * + * @param string $key + * @param string|null $rule + * @return string + */ + public function getMessage(string $key, ?string $rule = null): ?string; + + /** + * Get the Initial validation message for an attribute and rule. + * + * @param string $key + * @param string|null $rule + * @return string + */ + public function getInitialMessage(string $key, ?string $rule = null): ?string; + + /** + * Provide array of error messages. + * + * @param array $messages + * @return mixed + */ + public function setMessage(array $messages): MessageProviderInterface; + + /** + * Provide array of custom attribute names. + * + * @param array $customAttributes + * @return MessageProviderInterface + */ + public function setCustomAttributes(array $customAttributes): MessageProviderInterface; + + /** + * Provide data for validation + * + * @param array $data + * @return MessageProviderInterface + */ + public function setData(array $data): MessageProviderInterface; + + /** + * Provide RuleManager for validation + * + * @param RuleManager $ruleManager + * @return MessageProviderInterface + */ + public function setRuleManager(RuleManager $ruleManager): MessageProviderInterface; +} diff --git a/src/Support/Concerns/SceneInterface.php b/src/Support/Concerns/SceneInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..13a219a9276e24c9437686e83b83af0b0f906970 --- /dev/null +++ b/src/Support/Concerns/SceneInterface.php @@ -0,0 +1,82 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support\Concerns; + +use Closure; + +interface SceneInterface +{ + /** + * Specify the list of fields to be validated + * + * @link https://v.neww7.com/en/3/Scene.html#only + * @param array $fields + * @return $this + */ + public function only(array $fields): SceneInterface; + + /** + * Adding a validation rule for a field + * + * @link https://v.neww7.com/en/3/Scene.html#append + * @param string $field + * @param string|array|Closure $rules If you only need the functionality of a custom rule once throughout your application, + * you may use a closure instead of a rule object. + * The closure receives the attribute's name, + * the attribute's value, and a `$fail` callback that should be called if validation fails: + * + * function ($attribute, $value, $fail) { + * if ($value === 'foo') { + * $fail('The '.$attribute.' is invalid.'); + * } + * }, + * + * @return $this + */ + public function append(string $field, $rules): SceneInterface; + + /** + * Remove the validation rule for a field + * + * @link https://v.neww7.com/en/3/Scene.html#remove + * @param string $field + * @param array|string|null $rule Validate rules.if $rule is null,remove all rules from the current field + * @return $this + */ + public function remove(string $field, $rule = null): SceneInterface; + + /** + * Add fields to the validation list + * + * @link https://v.neww7.com/en/3/Scene.html#appendcheckfield + * @param string $field + * @return $this + */ + public function appendCheckField(string $field): SceneInterface; + + /** + * Delete fields from the validation list + * + * @link https://v.neww7.com/en/3/Scene.html#removecheckfield + * @param string $field + * @return $this + */ + public function removeCheckField(string $field): SceneInterface; + + /** + * Get validate rules + * + * @return array + */ + public function getRules(): array; +} diff --git a/src/Support/DataAttribute.php b/src/Support/DataAttribute.php new file mode 100644 index 0000000000000000000000000000000000000000..dcff6cc42a6c14fe67725802d2fc41e876be4879 --- /dev/null +++ b/src/Support/DataAttribute.php @@ -0,0 +1,22 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support; + +class DataAttribute +{ + /** + * Whether to delete the field in the check data + * @var bool + */ + public $deleteField = false; +} diff --git a/src/Support/Event/ValidateEventAbstract.php b/src/Support/Event/ValidateEventAbstract.php index fa15f9cc597029f70eb67c8dcdfdd02f528ad1da..2dd99bb7939a594402d885ba69b6591676f7f20b 100644 --- a/src/Support/Event/ValidateEventAbstract.php +++ b/src/Support/Event/ValidateEventAbstract.php @@ -1,33 +1,46 @@ + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ -use Closure; -use Psr\Http\Message\ServerRequestInterface; +namespace W7\Validate\Support\Event; abstract class ValidateEventAbstract implements ValidateEventInterface { - /** - * 场景验证前 - * @param array $data 用户输入的数据 - * @param ServerRequestInterface $request - * @param Closure $next - * @return mixed - */ - public function beforeValidate(array $data, ServerRequestInterface $request, Closure $next) - { - return $next($data, $request); - } - - /** - * 场景验证后 - * @param array $data 验证后的数据 - * @param ServerRequestInterface $request - * @param Closure $next - * @return mixed - */ - public function afterValidate(array $data, ServerRequestInterface $request, Closure $next) - { - return $next($data, $request); - } + /** + * Current validation scene name + * @var ?string + */ + public $sceneName; + + /** + * Current validated data + * @var array + */ + public $data; + + /** + * Error message + * @var string + */ + public $message; + + /** @inheritDoc */ + public function beforeValidate(): bool + { + return true; + } + + /** @inheritDoc */ + public function afterValidate(): bool + { + return true; + } } diff --git a/src/Support/Event/ValidateEventInterface.php b/src/Support/Event/ValidateEventInterface.php index 865a8cf21b6d34b8ed25ac5f73a1ece9cd0c12c3..f4bcdd7c5ba7c58b595abe45b15cda777db270ba 100644 --- a/src/Support/Event/ValidateEventInterface.php +++ b/src/Support/Event/ValidateEventInterface.php @@ -1,12 +1,30 @@ + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ -use Closure; -use Psr\Http\Message\ServerRequestInterface; +namespace W7\Validate\Support\Event; interface ValidateEventInterface { - public function beforeValidate(array $data, ServerRequestInterface $request, Closure $next); - public function afterValidate(array $data, ServerRequestInterface $request, Closure $next); + /** + * Methods implemented prior to validation + * + * @return bool + */ + public function beforeValidate(): bool; + + /** + * Methods implemented after validation + * + * @return bool + */ + public function afterValidate(): bool; } diff --git a/src/Support/Event/ValidateResult.php b/src/Support/Event/ValidateResult.php deleted file mode 100644 index a13499b321f64eb66e19955c3d0e117dc2a2851f..0000000000000000000000000000000000000000 --- a/src/Support/Event/ValidateResult.php +++ /dev/null @@ -1,30 +0,0 @@ -data = $data; - $this->request = $request; - } - - public function getData() - { - return $this->data; - } - - public function getRequest() - { - return $this->request; - } -} diff --git a/src/Support/MessageProvider.php b/src/Support/MessageProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..df0199afe590aec527638eaff2d1b99f529e94a9 --- /dev/null +++ b/src/Support/MessageProvider.php @@ -0,0 +1,116 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support; + +use W7\Validate\RuleManager; +use W7\Validate\Support\Concerns\MessageProviderInterface; +use Itwmw\Validation\Support\Arr; + +class MessageProvider implements MessageProviderInterface +{ + /** + * The array of custom attribute names. + * + * @var array + */ + protected $customAttributes = []; + + /** + * The array of custom error messages. + * + * @var array + */ + protected $message = []; + + /** + * Data for validate + * + * @var array + */ + protected $data = []; + + /** @inheritDoc */ + public function setRuleManager(RuleManager $ruleManager): MessageProviderInterface + { + $this->message = $ruleManager->getMessages(); + $this->customAttributes = $ruleManager->getCustomAttributes(); + return $this; + } + + /** @inheritDoc */ + public function setMessage(array $messages): MessageProviderInterface + { + $this->message = $messages; + return $this; + } + + /** @inheritDoc */ + public function setCustomAttributes(array $customAttributes): MessageProviderInterface + { + $this->customAttributes = $customAttributes; + return $this; + } + + /** @inheritDoc */ + public function setData(array $data): MessageProviderInterface + { + $this->data = $data; + return $this; + } + + /** @inheritDoc */ + public function getInitialMessage(string $key, ?string $rule = null): ?string + { + if (null !== $rule) { + $key = Common::makeMessageName($key, $rule); + } + + return $this->message[$key] ?? ''; + } + + /** @inheritDoc */ + public function handleMessage(string $messages): string + { + return $this->replacingFieldsInMessage($messages); + } + + /** @inheritDoc */ + public function getMessage(string $key, ?string $rule = null): ?string + { + $error = $this->getInitialMessage($key, $rule); + return $this->replacingFieldsInMessage($error); + } + + /** + * Replacing fields in error messages + * + * @param string $message + * @return string|string[] + */ + private function replacingFieldsInMessage(string $message) + { + if (preg_match_all('/{:(.*?)}/', $message, $matches) > 0) { + foreach ($matches[0] as $index => $pregString) { + $message = str_replace($pregString, Arr::get($this->data, $matches[1][$index], ''), $message); + } + } + + if (preg_match_all('/{@(.*?)}/', $message, $matches) > 0) { + foreach ($matches[0] as $index => $pregString) { + $message = str_replace($pregString, $this->customAttributes[$matches[1][$index]] ?? '', $message); + } + } + + return $message; + } +} diff --git a/src/Support/Middleware/ValidateMiddleware.php b/src/Support/Middleware/ValidateMiddleware.php deleted file mode 100644 index 60fd67e31ca3a697181787e4b051c4d9f943a62e..0000000000000000000000000000000000000000 --- a/src/Support/Middleware/ValidateMiddleware.php +++ /dev/null @@ -1,58 +0,0 @@ -getValidate($request); - if (false === $validator) { - $data = []; - } else { - $data = array_merge([], $request->getQueryParams(), $request->getParsedBody(), $request->getUploadedFiles()); - $data = $validator->check($data); - $request = $validator->getRequest(); - } - - /** @var Request $request */ - $request = $request->withAttribute('validate', $data); - Context::setRequest($request); - return $handler->handle($request); - } - - final public function getValidate(ServerRequestInterface $request) - { - $route = $request->getAttribute('route'); - $controller = $route['controller'] ?? ''; - $scene = $route['method'] ?? ''; - - $validate = str_replace(ValidateConfig::instance()->controllerPath, '', $controller); - $_namespace = explode('\\', $validate); - $fileName = str_replace('Controller', 'Validate', array_pop($_namespace)); - $validate = ValidateConfig::instance()->validatePath . implode('\\', $_namespace) . '\\' . $fileName; - - if (class_exists($validate)) { - if (is_subclass_of($validate, Validate::class)) { - /** @var Validate $validator */ - $validator = new $validate($request); - $validator->scene($scene); - return $validator; - } - - throw new Exception("The given 'Validate' " . $validate . ' has to be a subtype of W7\Validate\Validate'); - } - return false; - } -} diff --git a/src/Support/Rule/BaseRule.php b/src/Support/Rule/BaseRule.php index cd9a9fa98efec5676ed276bf404165cb5fe6e92c..499e1ff2479a5af435fda51bcce1b72b12f41edd 100644 --- a/src/Support/Rule/BaseRule.php +++ b/src/Support/Rule/BaseRule.php @@ -1,28 +1,59 @@ + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ namespace W7\Validate\Support\Rule; +/** + * Custom Rules + * + * @link https://v.neww7.com/en/3/Rule.html#using-rule-objects + */ +abstract class BaseRule implements RuleInterface +{ + /** + * Error messages, support for format strings + * @var string + */ + protected $message = ''; -use Illuminate\Contracts\Validation\Rule; + /** + * Parameters for format error messages + * @var array + */ + protected $messageParam = []; -abstract class BaseRule implements Rule -{ - protected $message = ''; - - public function setMessage(string $message) - { - $this->message = $message; - return $this; - } - - public function getMessage() - { - return $this->message; - } - - public function message() - { - return $this->getMessage(); - } + public function setMessage(string $message): BaseRule + { + $this->message = $message; + return $this; + } + + public function getMessage(): string + { + return vsprintf($this->message, $this->messageParam); + } + + public function message(): string + { + return $this->getMessage(); + } + + public static function make(...$params): BaseRule + { + return new static(...$params); + } + + public function check($data): bool + { + return $this->passes('', $data); + } } diff --git a/src/Support/Rule/RuleInterface.php b/src/Support/Rule/RuleInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..71d7d99452fc0ea480994a9db9ca5e3aebd09c29 --- /dev/null +++ b/src/Support/Rule/RuleInterface.php @@ -0,0 +1,34 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support\Rule; + +use Itwmw\Validation\Support\Interfaces\Rule; + +interface RuleInterface extends Rule +{ + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value): bool; + + /** + * Get the validation error message. + * + * @return string + */ + public function message(): string; +} diff --git a/src/Support/RuleManagerScene.php b/src/Support/RuleManagerScene.php new file mode 100644 index 0000000000000000000000000000000000000000..67c6813b8228ca12846e2a6d273589b0de845153 --- /dev/null +++ b/src/Support/RuleManagerScene.php @@ -0,0 +1,138 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support; + +use W7\Validate\Support\Concerns\SceneInterface; + +/** + * Scene classes for rule managers + * + * @link https://v.neww7.com/en/3/Scene.html#methods-of-the-scene-class + */ +class RuleManagerScene implements SceneInterface +{ + /** + * The rules to be applied to the data. + * @var array + */ + protected $checkRules = []; + + /** + * All original validation rules + * @var array + */ + protected $rules = []; + + /** + * RuleManagerScene constructor. + * @param array $rules All original validation rules + */ + public function __construct(array $rules = []) + { + $this->rules = $rules; + } + + /** @inheritDoc */ + public function only(array $fields): SceneInterface + { + $this->checkRules = Common::getRulesAndFill($this->rules, $fields); + return $this; + } + + /** @inheritDoc */ + public function append(string $field, $rules): SceneInterface + { + if (isset($this->checkRules[$field])) { + if (empty($this->checkRules[$field])) { + $this->checkRules[$field] = []; + } elseif (!is_array($this->checkRules[$field])) { + $this->checkRules[$field] = explode('|', $this->checkRules[$field]); + } + + if (is_string($rules)) { + $rules = explode('|', $rules); + array_push($this->checkRules[$field], ...$rules); + } else { + array_push($this->checkRules[$field], $rules); + } + } + + return $this; + } + + /** @inheritDoc */ + public function remove(string $field, $rule = null): SceneInterface + { + $removeRule = $rule; + if (is_string($rule) && false !== strpos($rule, '|')) { + $removeRule = explode('|', $rule); + } + + if (is_array($removeRule)) { + foreach ($removeRule as $rule) { + $this->remove($field, $rule); + } + + return $this; + } + + if (isset($this->checkRules[$field])) { + if (null === $rule) { + $this->checkRules[$field] = []; + return $this; + } + + $rules = $this->checkRules[$field]; + + if (is_string($rules)) { + $rules = explode('|', $rules); + } + + if (false !== ($index = strpos($rule, ':'))) { + $rule = substr($rule, 0, $index); + } + + $rules = array_filter($rules, function ($value) use ($rule) { + if (false !== strpos($value, ':')) { + $value = substr($value, 0, strpos($value, ':')); + } + return $value !== $rule; + }); + + $this->checkRules[$field] = $rules; + } + + return $this; + } + + /** @inheritDoc */ + public function appendCheckField(string $field): SceneInterface + { + $rule = $this->rules[$field] ?? ''; + $this->checkRules = array_merge($this->checkRules, [$field => $rule]); + return $this; + } + + /** @inheritDoc */ + public function removeCheckField(string $field): SceneInterface + { + unset($this->checkRules[$field]); + return $this; + } + + /** @inheritDoc */ + public function getRules(): array + { + return $this->checkRules; + } +} diff --git a/src/Support/Storage/ValidateCollection.php b/src/Support/Storage/ValidateCollection.php deleted file mode 100644 index eecbac04664a719dbe5168ae5987f87a16aaf7f6..0000000000000000000000000000000000000000 --- a/src/Support/Storage/ValidateCollection.php +++ /dev/null @@ -1,36 +0,0 @@ -items, $value)) { - return false; - } - } - - return true; - } - - public function get($key, $default = null) - { - if (false !== strpos($key, '.')) { - return Arr::get($this->items, $key, $default instanceof \Closure ? $default() : $default); - } - return parent::get($key, $default); - } - - public function set($key, $value) - { - Arr::set($this->items, $key, $value); - return $this; - } -} diff --git a/src/Support/Storage/ValidateConfig.php b/src/Support/Storage/ValidateConfig.php index 6bbc5de337037ba8ecf60562da20d5af1b1b547a..59aaa5d37370a36ed21842f06538fb953c7544ff 100644 --- a/src/Support/Storage/ValidateConfig.php +++ b/src/Support/Storage/ValidateConfig.php @@ -1,56 +1,159 @@ + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + namespace W7\Validate\Support\Storage; -use Exception; -use W7\Core\Helper\Traiter\InstanceTraiter; +use Itwmw\Validation\Factory; +use Itwmw\Validation\Support\Interfaces\PresenceVerifierInterface; +use Itwmw\Validation\Support\Translation\Interfaces\Translator; -/** - * Class ValidateConfig - * @package W7\Validate\Support\Storage - * @method ValidateConfig rulesPath(string $path) - * @method ValidateConfig controllerPath(string $path) - * @method ValidateConfig validatePath(string $path) - * @property-read string $rulesPath - * @property-read string $controllerPath - * @property-read string $validatePath - */ -class ValidateConfig +final class ValidateConfig { - use InstanceTraiter; - - /** - * 自定义规则命名空间前缀 - * @var string - */ - protected $rulesPath = ''; - - /** @var string */ - protected $controllerPath = null; - - /** @var string */ - protected $validatePath = null; - - public function __get($name) - { - if (false === property_exists($this, $name)) { - throw new Exception('Unknown property:' . $name); - } - - return $this->$name; - } - - public function __call($name, $args) - { - if (false === property_exists($this, $name)) { - throw new Exception('Unknown property: ' . $name); - } - - $this->$name = $args[0]; - return $this; - } - - public function __clone() - { - } + /** + * Custom rules namespace prefixes + * @var array + */ + protected $rulesPath = []; + + /** + * Translator + * @var Translator + */ + protected $translator; + + /** + * Validator Factory + * @var Factory + */ + protected $factory; + + /** + * Presence Validator + * @var PresenceVerifierInterface + */ + protected $verifier; + + protected static $instance; + + public static function instance(): ValidateConfig + { + if (empty(self::$instance)) { + self::$instance = new ValidateConfig(); + } + + return self::$instance; + } + + /** + * Provide validator factory + * + * @link https://v.neww7.com/en/3/Start.html#configuration-validator-factory + * @param Factory $factory + * @return ValidateConfig + */ + public function setFactory(Factory $factory): ValidateConfig + { + $this->factory = $factory; + return $this; + } + + /** + * Get Validator Factory + * + * @return Factory + */ + public function getFactory(): Factory + { + if (empty($this->factory)) { + $translator = $this->getTranslator(); + if (is_null($translator)) { + $this->factory = new Factory(); + } else { + $this->factory = new Factory($translator); + } + + if ($this->getPresenceVerifier()) { + $this->factory->setPresenceVerifier($this->getPresenceVerifier()); + } + } + + return $this->factory; + } + + /** + * Set the presence verifier implementation. + * + * @param PresenceVerifierInterface $presenceVerifier + * @return $this + */ + public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier): ValidateConfig + { + $this->verifier = $presenceVerifier; + return $this; + } + + /** + * Get presence verifier + * + * @return PresenceVerifierInterface|null + */ + private function getPresenceVerifier(): ?PresenceVerifierInterface + { + return $this->verifier ?? null; + } + + /** + * Provide translator + * + * @param Translator $translator + * @return $this + */ + public function setTranslator(Translator $translator): ValidateConfig + { + $this->translator = $translator; + return $this; + } + + /** + * Get Translator + * + * @return Translator + */ + private function getTranslator(): ?Translator + { + return $this->translator; + } + + /** + * Set the custom rules namespace prefix, If more than one exists, they all take effect + * + * @link https://v.neww7.com/en/3/Rule.html#pre-processing + * @param string $rulesPath Custom rules namespace prefixes + * @return $this + */ + public function setRulesPath(string $rulesPath): ValidateConfig + { + $this->rulesPath[] = $rulesPath; + $this->rulesPath = array_unique($this->rulesPath); + return $this; + } + + /** + * Get custom rules namespace prefixes + * + * @return array + */ + public function getRulePath(): array + { + return $this->rulesPath; + } } diff --git a/src/Support/Storage/ValidateHandler.php b/src/Support/Storage/ValidateHandler.php deleted file mode 100644 index f51b006581c1433c8e2cd666f5ac36b25b843df2..0000000000000000000000000000000000000000 --- a/src/Support/Storage/ValidateHandler.php +++ /dev/null @@ -1,71 +0,0 @@ -data = $data; - $this->request = $request; - $this->handlers = $handlers; - } - - protected function carry() - { - return function ($stack, $pipe) { - return function ($data, $request) use ($stack, $pipe) { - return $pipe($data, $request, $stack); - }; - }; - } - - protected function pipes(string $method) - { - return array_map(function ($middleware) use ($method) { - return function ($data, $request, $next) use ($middleware,$method) { - list($callback, $param) = $middleware; - if (class_exists($callback) && is_subclass_of($callback, ValidateEventAbstract::class)) { - return call_user_func([new $callback(...$param), $method], $data, $request, $next); - } else { - throw new ValidateException('Event error or nonexistence'); - } - }; - }, $this->handlers); - } - - protected function destination() - { - return function ($data, $request) { - return new ValidateResult($data, $request); - }; - } - - public function handle(string $method) - { - $destination = $this->destination(); - $pipeline = array_reduce( - array_reverse($this->pipes($method)), - $this->carry(), - function ($data, $request) use ($destination) { - return $destination($data, $request); - } - ); - - return $pipeline($this->data, $this->request); - } -} diff --git a/src/Support/ValidateScene.php b/src/Support/ValidateScene.php new file mode 100644 index 0000000000000000000000000000000000000000..91a51ab4afb3aa660c15cf85b1210e0bf4d14a81 --- /dev/null +++ b/src/Support/ValidateScene.php @@ -0,0 +1,291 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Validate\Support; + +use Closure; +use Itwmw\Validation\Support\Arr; +use W7\Validate\Exception\ValidateRuntimeException; +use W7\Validate\Support\Concerns\DefaultInterface; +use W7\Validate\Support\Concerns\FilterInterface; +use W7\Validate\Support\Rule\BaseRule; +use Itwmw\Validation\Support\Collection\Collection; + +/** + * Scene classes for validators + * + * @link https://v.neww7.com/en/3/Scene.html#methods-of-the-scene-class + * @package W7\Validate\Support + * @property-read array $events Events to be processed for this validate + * @property-read array $befores Methods to be executed before this validate + * @property-read array $afters Methods to be executed after this validate + * @property-read array $defaults This validation requires a default value for the value + * @property-read array $filters The filter. This can be a global function name, anonymous function, etc. + * @property-read bool $eventPriority Event Priority + * @property-read string $next Next scene or next scene selector + */ +class ValidateScene extends RuleManagerScene +{ + /** + * Data to be validated + * @var array + */ + protected $checkData = []; + + /** + * Events to be processed for this validate + * @var array + */ + private $events = []; + + /** + * Methods to be executed before this validate + * @var array + */ + private $befores = []; + + /** + * Methods to be executed after this validate + * @var array + */ + private $afters = []; + + /** + * This validation requires a default value for the value + * @var array + */ + private $defaults = []; + + /** + * The filter. This can be a global function name, anonymous function, etc. + * @var array + */ + private $filters = []; + + /** + * Event Priority + * @var bool + */ + private $eventPriority = true; + + /** + * Next scene or next scene selector + * @var string + */ + private $next; + + /** + * ValidateScene constructor. + * @param array $rules + * @param array $checkData + */ + public function __construct(array $rules = [], array $checkData = []) + { + parent::__construct($rules); + $this->checkData = $checkData; + } + + /** + * Add conditions to a given field based on a Closure. + * + * @link https://v.neww7.com/en/3/Scene.html#sometimes + * @param string|string[] $attribute field name + * @param string|array|BaseRule $rules rules + * @param callable $callback Closure,method provides a {@see Collection} $data parameter, + * which is the current validation data, + * if the Closure passed as the third argument returns true, the rules will be added. + * @return $this + */ + public function sometimes($attribute, $rules, callable $callback): ValidateScene + { + $data = $this->getValidateData(); + $result = call_user_func($callback, $data); + + if (!$result) { + return $this; + } + + if (is_array($attribute)) { + foreach ($attribute as $filed) { + $this->append($filed, $rules); + } + } else { + $this->append($attribute, $rules); + } + + return $this; + } + + /** + * Join the event + * + * @link https://v.neww7.com/en/3/Scene.html#event + * @param string $handler Full class name of the event, full namespace string or add ::class + * @param mixed ...$params Parameters to be passed to the event + * @return $this + */ + public function event(string $handler, ...$params): ValidateScene + { + $this->events[] = [$handler, $params]; + return $this; + } + + /** + * Add a method that needs to be executed before validation + * + * @link https://v.neww7.com/en/3/Scene.html#before + * @param string|Closure|callable $callbackName {@see Closure},{@see callable} or Validate the method name in the class + * @param mixed ...$params Parameters to be passed to the method + * @return $this + */ + public function before($callbackName, ...$params): ValidateScene + { + $this->befores[] = [$callbackName, $params]; + return $this; + } + + /** + * Add a method that needs to be executed after validation + * + * @link https://v.neww7.com/en/3/Scene.html#after + * @param string|Closure|callable $callbackName {@see Closure},{@see callable} or Validate the method name in the class + * @param mixed ...$params Parameters to be passed to the method + * @return $this + */ + public function after($callbackName, ...$params): ValidateScene + { + $this->afters[] = [$callbackName, $params]; + return $this; + } + + /** + * Set a default value for the specified field + * + * @link https://v.neww7.com/en/3/Scene.html#default + * @param string $field Name of the data field to be processed + * @param callable|Closure|mixed|DefaultInterface|null $callback The default value or an anonymous function that returns the default value which will + * be assigned to the attributes being validated if they are empty. The signature of the anonymous function + * should be as follows,The anonymous function has two parameters: + * + * + * e.g: + * + * function($value,string $attribute,array $originalData){ + * return $value; + * } + * + * If this parameter is null, the default value of the field will be removed + * @param bool $any Whether to handle arbitrary values, default only handle values that are not null + * @return $this + */ + public function default(string $field, $callback, bool $any = false): ValidateScene + { + if (null === $callback) { + $this->defaults[$field] = null; + } else { + $this->defaults[$field] = ['value' => $callback, 'any' => $any]; + } + return $this; + } + + /** + * Set a filter for the specified field + * + * @link https://v.neww7.com/en/3/Scene.html#filter + * Filter is a data processor. + * It invokes the specified filter callback to process the attribute value + * and save the processed value back to the attribute. + * @param string $field Name of the data field to be processed + * @param string|callable|Closure|FilterInterface|null $callback The filter. This can be a global function name, anonymous function, etc. + * The filter must be a valid PHP callback with the following signature: + * + * function foo($value) { + * // compute $newValue here + * return $newValue; + * } + * + * Many PHP functions qualify this signature (e.g. `trim()`). + * + * If this parameter is null, the filter for this field will be cancelled. + * @return $this + */ + public function filter(string $field, $callback): ValidateScene + { + $this->filters[$field] = $callback; + return $this; + } + + /** + * Set event priority + * + * @param bool $priority + * @return $this + */ + public function setEventPriority(bool $priority): ValidateScene + { + $this->eventPriority = $priority; + return $this; + } + + /** + * Specify the next scene or next scene selector + * + * @link https://v.neww7.com/en/3/Scene.html#next + * @param string $name + * @return $this + */ + public function next(string $name): ValidateScene + { + $this->next = $name; + return $this; + } + + /** + * Get the current validation data + * + * @link https://v.neww7.com/en/3/Scene.html#getdata + * @param string $key + * @param mixed $default + * @return array|mixed + */ + public function getData(string $key = '', $default = null) + { + if (!empty($key)) { + return Arr::get($this->checkData, $key, $default); + } + return $this->checkData; + } + + /** + * Get the current validation data,Return the {@see Collection} type + * + * @link https://v.neww7.com/en/3/Scene.html#getvalidatedata + * @return Collection + */ + public function getValidateData(): Collection + { + return validate_collect($this->getData()); + } + + public function __get($name) + { + if (property_exists($this, $name)) { + return $this->$name; + } + + throw new ValidateRuntimeException('Unknown property:' . $name); + } +} diff --git a/src/Support/helpers.php b/src/Support/helpers.php index ce23dce257f6dce8a04e5cdeb0391f8393b3d53f..4a371b9113f60703714c592d2561c6690b639e7e 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -1,32 +1,12 @@ + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ -if (!function_exists('validate_collect')) { - /** - * 数值转为验证器集合ValidateCollection类型 - * @param null $value - * @return ValidateCollection - */ - function validate_collect($value = null) - { - return new ValidateCollection($value); - } -} - -if (!function_exists('get_validate_data')) { - /** - * 获取验证后的结果 - * @param ServerRequestInterface|null $request 请求示例,如果为null,则自动从上下文中获取 - * @return ValidateCollection 返回验证器集合ValidateCollection类型 - */ - function get_validate_data(ServerRequestInterface $request = null) - { - if (null === $request) { - $request = Context::getRequest(); - } - return validate_collect($request->getAttribute('validate')); - } -} diff --git a/src/Validate.php b/src/Validate.php index 718e43f289faf414bc0fe8652448c8aa34d568e6..5f331bfa2c333fca0e0c551af68e8681dadeba4a 100644 --- a/src/Validate.php +++ b/src/Validate.php @@ -1,471 +1,729 @@ + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + namespace W7\Validate; use Closure; -use Illuminate\Support\Collection; -use Illuminate\Validation\ValidationException; -use LogicException; -use Psr\Http\Message\RequestInterface; -use W7\Core\Facades\Context; -use W7\Core\Facades\Validator; +use Itwmw\Validation\Factory; +use Itwmw\Validation\Support\Str; +use Itwmw\Validation\ValidationData; +use Itwmw\Validation\Support\ValidationException; use W7\Validate\Exception\ValidateException; -use W7\Validate\Support\Event\ValidateResult; -use W7\Validate\Support\Rule\BaseRule; +use W7\Validate\Exception\ValidateRuntimeException; +use W7\Validate\Support\Common; +use W7\Validate\Support\Concerns\DefaultInterface; +use W7\Validate\Support\Concerns\FilterInterface; +use W7\Validate\Support\Concerns\MessageProviderInterface; +use W7\Validate\Support\DataAttribute; +use W7\Validate\Support\Event\ValidateEventAbstract; +use W7\Validate\Support\MessageProvider; +use Itwmw\Validation\Support\Collection\Collection; use W7\Validate\Support\Storage\ValidateConfig; -use W7\Validate\Support\Storage\ValidateHandler; +use W7\Validate\Support\ValidateScene; -class Validate +class Validate extends RuleManager { - /** - * 自定义错误消息 - * @var array - */ - protected $message = []; - - /** - * 验证规则 - * @var array - */ - protected $rule = []; - - /** - * 验证场景数据,key为控制器内的方法 - * @var array - */ - protected $scene = []; - - /** @var array */ - protected $customAttributes = []; - - /** - * 当前验证场景 - * @var string - */ - private $currentScene = null; - - /** - * 验证的规则 - * @var Collection - */ - private $checkRule; - - /** - * sometimes验证 - * @var array - */ - private $sometimes = []; - - /** - * 扩展方法名 - * @var array - */ - private static $extendName = []; - - /** - * 验证器事件处理类 - * @var array - */ - private $handlers = []; - - /** - * Request请求实例 - * @var RequestInterface|null - */ - protected $request = null; - - public function __construct(RequestInterface $request = null) - { - $this->request = $request; - } - - /** - * 自动验证 - * @param array $data - * @return array 返回验证成功后的数据 - * @throws ValidateException - */ - public function check(array $data) - { - try { - $data = $this->handleEvent($data, 'beforeValidate'); - /** @var \Illuminate\Validation\Validator $v */ - $v = Validator::make($data, $this->getSceneRules(), $this->message, $this->customAttributes); - if (!empty($this->sometimes)) { - foreach ($this->sometimes as $sometime) { - $v->sometimes(...$sometime); - } - } - $data = $this->handleEvent($v->validate(), 'afterValidate'); - return $data; - } catch (ValidationException $e) { - $errors = $e->errors(); - $errorMessage = ''; - foreach ($errors as $field => $message) { - $errorMessage = $message[0]; - break; - } - - throw new ValidateException($errorMessage, 403, $errors); - } - } - - private function handleEvent(array $data, string $method) - { - $request = $this->request ?: Context::getRequest(); - $result = (new ValidateHandler($data, $this->handlers ?: [], $request))->handle($method); - if (is_string($result)) { - throw new ValidateException($result, 403); - } elseif ($result instanceof ValidateResult) { - $this->request = $result->getRequest(); - return $result->getData(); - } - - throw new LogicException('Validate event return type error'); - } - - public function getRequest() - { - return $this->request; - } - - private function init() - { - $this->checkRule = []; - $this->handlers = []; - } - - private function getSceneRules() - { - $this->init(); - $this->getScene($this->currentScene); - $this->checkRule->transform(function ($rule, $field) { - if (!is_array($rule) && !$rule instanceof Collection) { - $_rules = collect(explode('|', $rule)); - } else { - $_rules = collect($rule); - } - $_rules->transform(function ($value) use ($field) { - if (is_string($value)) { - # 判断是否为自定义规则 - $ruleClass = $this->getRuleClass($value); - if (false !== $ruleClass) { - # 给自定义规则设置自定义错误消息 - $message = $this->getMessages($field, $value); - if ($message) { - $ruleClass->setMessage($message); - } - return $ruleClass; - } - return $this->getExtendsName($value, $field); - } - return $value; - }); - return $_rules; - }); - return $this->checkRule->toArray(); - } - - private function getExtendsName(string $rule, string $field = null) - { - # 取回真实的自定义规则方法名称,以及修改对应的错误消息 - if (in_array($rule, self::$extendName)) { - $ruleName = md5(get_called_class() . $rule); - # 判断是否为自定义规则方法定义了错误消息 - if (null !== $field && isset($this->message[$field . '.' . $rule])) { - $this->message[$ruleName] = $this->message[$field . '.' . $rule]; - } - return $ruleName; - } - return $rule; - } - - private function makeMessageName(string $field, string $rule) - { - if (false !== strpos($rule, ':')) { - $rule = substr($rule, 0, strpos($rule, ':')); - } - return $field . '.' . $rule; - } - - private function getScene(string $name = null) - { - $this->sometimes = []; - - if (empty($name)) { - $this->checkRule = collect($this->rule); - return $this; - } - # 判断自定义验证场景是否存在 - if (method_exists($this, 'scene' . ucfirst($name))) { - $this->checkRule = collect($this->rule); - call_user_func([$this, 'scene' . ucfirst($name)]); - } elseif (isset($this->scene[$name])) { // 判断验证场景是否存在 - $sceneRule = $this->scene[$name]; - if (isset($sceneRule['use']) && !empty($sceneRule['use'])) { // 判断验证场景是否指定了其他验证场景 - $this->getScene($sceneRule['use']); - } else { - $this->checkRule = collect($this->rule)->only($sceneRule); - } - - # 判断是否定义了事件 - if (isset($sceneRule['handler'])) { - $handler = $sceneRule['handler']; - if (is_string($handler)) { - $this->handlers[] = [$handler,[]]; - } else { - $handlerClass = array_shift($handler); - $this->handler($handlerClass, ...$handler); - } - } - } else { - # 如果验证场景找不到,则默认验证全部规则 - $this->checkRule = collect($this->rule); - } - - return $this; - } - - private function getRuleClass(string $ruleName) - { - $ruleNameSpace = ValidateConfig::instance()->rulesPath . ucfirst($ruleName); - if (class_exists($ruleNameSpace)) { - return new $ruleNameSpace(); - } - return false; - } - - /** - * 注册自定义验证方法 - * - * @param string $rule 规则名称 - * @param Closure|string $extension 闭包规则,回调四个值 $attribute, $value, $parameters, $validator - * @param string|null $message 错误消息 - */ - public static function extend(string $rule, $extension, string $message = null) - { - array_push(self::$extendName, $rule); - self::$extendName = array_unique(self::$extendName); - - # 多个验证器使用了同样的rule会导致后面的方法无法生效 - # 故这里根据命名空间生成一个独一无二的rule名称 - $ruleName = md5(get_called_class() . $rule); - Validator::extend($ruleName, $extension, $message); - } - - /** - * 设置验证场景 - * - * @param string $name - * @return $this - */ - public function scene(string $name) - { - $this->currentScene = $name; - return $this; - } - - /** - * @param string $handler - * @param mixed ...$params - * @return $this - */ - public function handler(string $handler, ...$params) - { - $this->handlers[] = [$handler,$params]; - return $this; - } - - /** - * 指定需要验证的字段列表 - * - * @param array $fields 字段名 - * @return $this - */ - public function only(array $fields) - { - $this->checkRule = $this->checkRule->only($fields); - return $this; - } - - /** - * 追加某个字段的验证规则 - * - * @param string $field 字段名 - * @param string $rule 验证规则 - * @return $this - */ - public function append(string $field, string $rule) - { - if (isset($this->checkRule[$field])) { - if ($this->checkRule[$field] instanceof Collection) { - $appendRule = $rule; - if (!is_array($appendRule)) { - $appendRule = explode('|', $appendRule); - } - $this->checkRule[$field] = $this->checkRule[$field]->concat($appendRule); - } else { - $this->checkRule[$field] = $this->checkRule[$field] . '|' . $rule; - } - } - - return $this; - } - - /** - * 移除某个字段的验证规则 - * - * @param string $field 字段名 - * @param string|null $rule 验证规则 null 移除所有规则 - * @return $this - */ - public function remove(string $field, string $rule = null) - { - if (isset($this->checkRule[$field])) { - if (null === $rule) { - unset($this->checkRule[$field]); - } else { - $rules = $this->checkRule[$field]; - if (!is_array($rules)) { - $rules = explode('|', $rules); - } - $rules = collect($rules); - $this->checkRule[$field] = $rules->diff(explode('|', $rule)); - } - } - return $this; - } - - /** - * 复杂条件验证 - * - * @param string|string[] $attribute 字段 - * @param string|array|BaseRule $rules 规则 - * @param callable $callback 回调方法,返回true规则生效 - * @return $this - */ - public function sometimes($attribute, $rules, callable $callback) - { - if (is_string($rules)) { - $rules = collect(explode('|', $rules)); - } elseif (is_array($rules)) { - $rules = collect($rules); - } - - if ($rules instanceof Collection) { - $rules->transform(function ($rule) use ($attribute) { - if (is_string($rule)) { - $ruleClass = $this->getRuleClass($rule); - if (false !== $ruleClass) { - if (is_array($attribute) && !empty($attribute)) { - $attr = $attribute[0]; - } else { - $attr = $attribute; - } - $message = $this->getMessages($attr, $rule); - if (false !== $message) { - $ruleClass->setMessage($message); - } - return $ruleClass; - } - } - return $rule; - }); - $rules = $rules->toArray(); - } - - $this->sometimes[] = [$attribute,$rules,$callback]; - return $this; - } - - /** - * 获取验证规则 - * - * @param null $rule 验证字段 - * @return array|mixed|null - */ - public function getRules($rule = null) - { - if (null === $rule) { - return $this->rule; - } - - if (is_array($rule)) { - return collect($this->rule)->only($rule)->toArray(); - } else { - return $this->rule[$rule] ?? null; - } - } - - /** - * 设置验证规则(叠加) - * - * @param array|null $rules [字段 => 规则] 如果$rules为null,则清空全部验证规则 - * @return $this - */ - public function setRules(array $rules = null) - { - if (null === $rules) { - $this->rule = []; - } - $this->rule = array_merge($this->rule, $rules); - return $this; - } - /** - * 获取验证消息 - * - * @param null|string|string[] $key 完整消息Key值 - * @param string|null $rule 如果第一个值为字段名,则第二个值则为规则,否则请留空 - * @return array|mixed|null - */ - public function getMessages($key = null, string $rule = null) - { - if (null === $key) { - return $this->message; - } - - if (null !== $rule) { - $messageName = $this->makeMessageName($key, $rule); - } else { - $messageName = $key; - } - - if (is_array($messageName)) { - return collect($this->message)->only($messageName)->toArray(); - } else { - return $this->message[$messageName] ?? null; - } - } - - /** - * 设置错误消息(叠加) - * - * @param array|null $message [字段.规则 => 验证消息] 如果$message为null,则清空全部验证消息 - * @return $this - */ - public function setMessages(array $message = null) - { - if (null === $message) { - $this->message = []; - } - - $this->message = array_merge($this->message, $message); - return $this; - } - - /** - * 设置验证场景(叠加) - * - * @param array|null $scene [场景 => [字段]] 如果$scene为null,则清空全部验证场景 - * @return $this - */ - public function setScene(array $scene = null) - { - if (null === $scene) { - $this->scene = []; - } - - $this->scene = array_merge($this->scene, $scene); - return $this; - } + /** + * Global Event Handler + * + * @link https://v.neww7.com/en/3/Validate.html#event + * @var array + */ + protected $event = []; + + /** + * All validated fields cannot be empty when present + * + * @link https://v.neww7.com/en/3/Validate.html#filled + * @var bool + */ + protected $filled = true; + + /** + * The filter. This can be a global function name, anonymous function, etc. + * + * @link https://v.neww7.com/en/3/Validate.html#filter + * @var array + */ + protected $filter = []; + + /** + * Sets the specified property to the specified default value. + * + * @link https://v.neww7.com/en/3/Validate.html#default + * @var array + */ + protected $default = []; + + /** + * Event Priority + * + * @var bool + */ + private $eventPriority = true; + + /** + * Events to be processed for this validate + * + * @var array + */ + private $events = []; + + /** + * Methods to be executed before this validate + * + * @var array + */ + private $befores = []; + + /** + * Methods to be executed after this validate + * + * @var array + */ + private $afters = []; + + /** + * This validation requires a default value for the value + * + * @var array + */ + private $defaults = []; + + /** + * Filters to be passed for this validation + * + * @var array + */ + private $filters = []; + + /** + * Error Message Provider + * + * @var MessageProviderInterface + */ + private $messageProvider = null; + + /** + * Data to be validated + * + * @var array + */ + private $checkData = []; + + /** + * Data validated this time + * + * @var array + */ + private $validatedData = []; + + /** + * Fields validated this time + * + * @var array + */ + private $validateFields = []; + + /** + * {@inheritdoc } + */ + protected $sceneProvider = ValidateScene::class; + + /** + * Create a validator + * + * @param array $rules Validation rules + * @param array $messages Error message + * @param array $customAttributes Field Name + * @return Validate + */ + public static function make(array $rules = [], array $messages = [], array $customAttributes = []): Validate + { + if (empty($rules)) { + return new static(); + } + return (new static())->setRules($rules)->setMessages($messages)->setCustomAttributes($customAttributes); + } + + /** + * Get Validator Factory + * + * @return Factory + */ + private static function getValidationFactory(): Factory + { + return ValidateConfig::instance()->getFactory(); + } + + /** + * Auto validate + * + * @param array $data Data to be verified + * @return array + * @throws ValidateException + */ + public function check(array $data): array + { + try { + $this->init(); + $this->checkData = $data; + $this->addEvent($this->event); + $this->handleEvent($data, 'beforeValidate'); + $events = $this->events; + $this->events = []; + $rule = $this->getSceneRules(); + $data = $this->pass($data, $rule); + $this->events = $events; + $this->handleEvent($data, 'afterValidate'); + return $data; + } catch (ValidationException $e) { + $error = $this->getMessageProvider()->handleMessage($e->getMessage()); + throw new ValidateException($error, 403, $e->getAttribute(), $e); + } + } + + /** + * Perform data validation and processing + * + * @param array $data Data to be verified + * @param array $rules Rules for validation + * @return array + * @throws ValidateException + * @throws ValidationException + */ + private function pass(array $data, array $rules): array + { + $this->defaults = array_merge($this->default, $this->defaults); + $this->filters = array_merge($this->filter, $this->filters); + + // Validated fields are not re-validated + $checkFields = array_diff(array_keys($rules), $this->validateFields); + $checkRules = array_intersect_key($rules, array_flip($checkFields)); + $checkRules = $this->getCheckRules($checkRules); + $this->validateFields = array_merge($this->validateFields, $checkFields); + + if ($this->filled) { + $checkRules = $this->addFilledRule($checkRules); + } + + // Defaults and filters only handle the fields that are being validated now + $fields = array_keys($checkRules); + $data = $this->handleDefault($data, $fields); + + if ($this->eventPriority) { + $this->handleEvent($data, 'beforeValidate'); + $this->handleCallback($data, 1); + } else { + $this->handleCallback($data, 1); + $this->handleEvent($data, 'beforeValidate'); + } + + $validatedData = $this->validatedData; + if (!empty($checkRules)) { + $validatedData = $this->getValidationFactory()->make($data, $checkRules, $this->message, $this->customAttributes)->validate(); + $validatedData = array_merge($this->validatedData, $validatedData); + } + + if ($this->eventPriority) { + $this->handleCallback($validatedData, 2); + $this->handleEvent($validatedData, 'afterValidate'); + } else { + $this->handleEvent($validatedData, 'afterValidate'); + $this->handleCallback($validatedData, 2); + } + + $validatedData = $this->handlerFilter($validatedData, $fields); + $this->initScene(); + $this->scene(null); + return $validatedData; + } + + /** + * Add a custom implicit validation rule + * + * @param string $rule Rule Name + * @param Closure|string|array $extension Closure rules, providing four parameters:$attribute, $value, $parameters, $validator + * @param string|null $message Error Message + * @return Validate + */ + protected function extendImplicitRule(string $rule, $extension, ?string $message = null): Validate + { + self::validatorExtend('Implicit', $rule, $extension, $message, true); + return $this; + } + + /** + * Add a custom dependent validation rule. + * + * @param string $rule Rule Name + * @param Closure|string|array $extension Closure rules, providing four parameters:$attribute, $value, $parameters, $validator + * @param string|null $message Error Message + * @return Validate + */ + protected function extendDependentRule(string $rule, $extension, ?string $message = null): Validate + { + self::validatorExtend('Dependent', $rule, $extension, $message, true); + return $this; + } + + /** + * Get the rules that need to be validation in the scene + * + * @param string|null $sceneName The scene name, or the current scene name if not provided. + * @return array + * @throws ValidationException|ValidateException + */ + private function getSceneRules(?string $sceneName = ''): array + { + if ('' === $sceneName) { + $sceneName = $this->getCurrentSceneName(); + } + + if (empty($sceneName)) { + return $this->rule; + } + + if (method_exists($this, 'scene' . ucfirst($sceneName))) { + $scene = new $this->sceneProvider($this->rule, $this->checkData); + call_user_func([$this, 'scene' . ucfirst($sceneName)], $scene); + $this->events = $scene->events; + $this->afters = $scene->afters; + $this->befores = $scene->befores; + $this->defaults = $scene->defaults; + $this->filters = $scene->filters; + $this->eventPriority = $scene->eventPriority; + $next = $scene->next; + $sceneRule = $scene->getRules(); + if (!empty($next)) { + return $this->next($scene->next, $sceneRule, $sceneName); + } + return $sceneRule; + } + + if (isset($this->scene[$sceneName])) { + $sceneRule = $this->scene[$sceneName]; + + // Determine if an event is defined + if (isset($sceneRule['event'])) { + $events = $sceneRule['event']; + $this->addEvent($events); + unset($sceneRule['event']); + } + + // Methods to be executed before determining the presence or absence of authentication + if (isset($sceneRule['before'])) { + $callback = $sceneRule['before']; + $this->addBefore($callback); + unset($sceneRule['before']); + } + + // Methods to be executed after determining the existence of validation + if (isset($sceneRule['after'])) { + $callback = $sceneRule['after']; + $this->addAfter($callback); + unset($sceneRule['after']); + } + + if (isset($sceneRule['next']) && !empty($sceneRule['next'])) { + $next = $sceneRule['next']; + unset($sceneRule['next']); + $rules = Common::getRulesAndFill($this->rule, $sceneRule); + return $this->next($next, $rules, $sceneName); + } else { + return Common::getRulesAndFill($this->rule, $sceneRule); + } + } + + return $this->rule; + } + + /** + * Processing the next scene + * + * @param string $next Next scene name or scene selector + * @param array $rules Validation rules + * @param string $currentSceneName Current scene name + * @return array + * @throws ValidateException + * @throws ValidationException + */ + private function next(string $next, array $rules, string $currentSceneName = ''): array + { + if ($next === $currentSceneName) { + throw new ValidateRuntimeException('The scene used cannot be the same as the current scene.'); + } + + // Pre-validation + $data = $this->pass($this->checkData, $rules); + $this->validatedData = array_merge($this->validatedData, $data); + + // If a scene selector exists + if (method_exists($this, lcfirst($next) . 'Selector')) { + $next = call_user_func([$this, lcfirst($next) . 'Selector'], $this->validatedData); + if (is_array($next)) { + return Common::getRulesAndFill($this->rule, $next); + } + } + + if (empty($next)) { + return []; + } + + return $this->getSceneRules($next); + } + + /** + * Processing method + * + * @param array $data + * @param int $type 1 before method 2 after method + * @throws ValidateException + */ + private function handleCallback(array $data, int $type) + { + switch ($type) { + case 1: + $callbacks = $this->befores; + $typeName = 'before'; + break; + case 2: + $callbacks = $this->afters; + $typeName = 'after'; + break; + } + + if (empty($callbacks)) { + return; + } + + foreach ($callbacks as $callback) { + list($callback, $param) = $callback; + if (!is_callable($callback)) { + $callback = $typeName . ucfirst($callback); + if (!method_exists($this, $callback)) { + throw new ValidateRuntimeException('Method Not Found'); + } + $callback = [$this, $callback]; + } + + if (($result = call_user_func($callback, $data, ...$param)) !== true) { + if (isset($this->message[$result])) { + $result = $this->getMessageProvider()->handleMessage($this->message[$result]); + } + throw new ValidateException($result, 403); + } + } + } + + /** + * validate event handling + * + * @param array $data Validated data + * @param string $method Event Name + * @throws ValidateException + */ + private function handleEvent(array $data, string $method) + { + if (empty($this->events)) { + return; + } + + foreach ($this->events as $events) { + list($callback, $param) = $events; + if (class_exists($callback) && is_subclass_of($callback, ValidateEventAbstract::class)) { + /** @var ValidateEventAbstract $handler */ + $handler = new $callback(...$param); + $handler->sceneName = $this->getCurrentSceneName(); + $handler->data = $data; + if (true !== call_user_func([$handler, $method])) { + $message = $handler->message; + if (isset($this->message[$message])) { + $message = $this->getMessageProvider()->handleMessage($this->message[$message]); + } + throw new ValidateException($message, 403); + } + } else { + throw new ValidateRuntimeException('Event error or nonexistence'); + } + } + } + + /** + * Filters for processing settings + * + * @param array $data + * @param array $fields + * @return array + */ + private function handlerFilter(array $data, array $fields): array + { + if (empty($this->filters)) { + return $data; + } + + $newData = validate_collect($data); + $filters = array_intersect_key($this->filters, array_flip($fields)); + foreach ($filters as $field => $callback) { + if (null === $callback) { + continue; + } + if (false !== strpos($field, '*')) { + $flatData = ValidationData::initializeAndGatherData($field, $data); + $pattern = str_replace('\*', '[^\.]*', preg_quote($field)); + foreach ($flatData as $key => $value) { + if (Str::startsWith($key, $field) || preg_match('/^' . $pattern . '\z/', $key)) { + $this->filterValue($key, $callback, $newData); + } + } + } else { + $this->filterValue($field, $callback, $newData); + } + } + + return $newData->toArray(); + } + + /** + * Filter the given value + * + * @param string $field Name of the data field to be processed + * @param callable|Closure|FilterInterface $callback The filter. This can be a global function name, anonymous function, etc. + * @param Collection $data + */ + private function filterValue(string $field, $callback, Collection $data) + { + if (!$data->has($field)) { + return; + } + $value = $data->get($field); + $dataAttribute = new DataAttribute(); + + if (is_callable($callback)) { + $value = call_user_func($callback, $value); + } elseif ((is_string($callback) || is_object($callback)) && class_exists($callback) && is_subclass_of($callback, FilterInterface::class)) { + /** @var FilterInterface $filter */ + $filter = new $callback($dataAttribute); + $value = $filter->handle($value); + } elseif (is_string($callback) && method_exists($this, 'filter' . ucfirst($callback))) { + $value = call_user_func([$this, 'filter' . ucfirst($callback)], $value, $dataAttribute); + } else { + throw new ValidateRuntimeException('The provided filter is wrong'); + } + + if (true === $dataAttribute->deleteField) { + $data->forget($field); + } else { + $data->set($field, $value); + } + } + + /** + * Defaults for processing settings + * + * @param array $data + * @param array $fields + * @return array + */ + private function handleDefault(array $data, array $fields): array + { + if (empty($this->defaults)) { + return $data; + } + + $newData = validate_collect($data); + $defaults = array_intersect_key($this->defaults, array_flip($fields)); + foreach ($defaults as $field => $value) { + // Skip array members + if (null === $value || false !== strpos($field, '*')) { + continue; + } + + if (is_array($value) && isset($value['any']) && isset($value['value'])) { + $this->setDefaultData($field, $value['value'], $newData, (bool)$value['any']); + } else { + $this->setDefaultData($field, $value, $newData); + } + } + + return $newData->toArray(); + } + + /** + * Applying default settings to data + * + * @param string $field Name of the data field to be processed + * @param callable|Closure|DefaultInterface|mixed $callback The default value or an anonymous function that returns the default value which will + * @param Collection $data Data to be processed + * @param bool $any Whether to handle arbitrary values, default only handle values that are not null + */ + private function setDefaultData(string $field, $callback, Collection $data, bool $any = false) + { + $isEmpty = function ($value) { + return null === $value || [] === $value || '' === $value; + }; + $value = $data->get($field); + $dataAttribute = new DataAttribute(); + + if ($isEmpty($value) || true === $any) { + if (is_callable($callback)) { + $value = call_user_func($callback, $value, $field, $this->checkData, $dataAttribute); + } elseif ((is_string($callback) || is_object($callback)) && class_exists($callback) && is_subclass_of($callback, DefaultInterface::class)) { + /** @var DefaultInterface $default */ + $default = new $callback($dataAttribute); + $value = $default->handle($value, $field, $this->checkData); + } elseif (is_string($callback) && method_exists($this, 'default' . ucfirst($callback))) { + $value = call_user_func([$this, 'default' . ucfirst($callback)], $value, $field, $this->checkData, $dataAttribute); + } else { + $value = $callback; + } + } + + if (true === $dataAttribute->deleteField) { + $data->forget($field); + } else { + $data->set($field, $value); + } + } + + /** + * Initialization validate + */ + private function init() + { + $this->validatedData = []; + $this->validateFields = []; + $this->initScene(); + } + + /** + * Initialization validation scene + */ + private function initScene() + { + $this->afters = []; + $this->befores = []; + $this->events = []; + $this->defaults = []; + $this->filters = []; + $this->eventPriority = true; + } + + /** + * Set the message provider for the validator. + * + * @param MessageProviderInterface|string|callable $messageProvider + * @return $this + * @throws ValidateException + */ + public function setMessageProvider($messageProvider): RuleManager + { + if (is_string($messageProvider) && is_subclass_of($messageProvider, MessageProviderInterface::class)) { + $this->messageProvider = new $messageProvider(); + } elseif (is_object($messageProvider) && is_subclass_of($messageProvider, MessageProviderInterface::class)) { + $this->messageProvider = $messageProvider; + } elseif (is_callable($messageProvider)) { + $messageProvider = call_user_func($messageProvider); + $this->setMessageProvider($messageProvider); + return $this; + } else { + throw new ValidateRuntimeException('The provided message processor needs to implement the MessageProviderInterface interface'); + } + + return $this; + } + + /** + * Get the message provider for the validator. + * + * @return MessageProviderInterface + */ + public function getMessageProvider(): MessageProviderInterface + { + if (empty($this->messageProvider)) { + $this->messageProvider = new MessageProvider(); + } + + $messageProvider = $this->messageProvider; + $messageProvider->setMessage($this->message); + $messageProvider->setCustomAttributes($this->customAttributes); + $messageProvider->setData($this->checkData); + return $messageProvider; + } + + /** + * Add Event + * + * @param $handlers + */ + private function addEvent($handlers) + { + $this->addCallback(0, $handlers); + } + + /** + * Methods to be executed before adding validation + * + * @param $callback + */ + private function addBefore($callback) + { + $this->addCallback(1, $callback); + } + + /** + * Add the method that needs to be executed after verification + * + * @param $callback + */ + private function addAfter($callback) + { + $this->addCallback(2, $callback); + } + + /** + * Add method + * + * @param int $intType 0 event 1 before 2 after + * @param $callback + */ + private function addCallback(int $intType, $callback) + { + switch ($intType) { + case 0: + $type = 'events'; + break; + case 1: + $type = 'befores'; + break; + case 2: + $type = 'afters'; + break; + } + + if (is_string($callback)) { + $this->$type[] = [$callback, []]; + } else { + foreach ($callback as $classOrMethod => $param) { + if (is_int($classOrMethod)) { + $this->$type[] = [$param, []]; + } elseif (is_string($classOrMethod)) { + if (is_array($param)) { + $this->$type[] = [$classOrMethod, $param]; + } else { + $this->$type[] = [$classOrMethod, [$param]]; + } + } + } + } + } } diff --git a/tests/Material/ArticleValidate.php b/tests/Material/ArticleValidate.php new file mode 100644 index 0000000000000000000000000000000000000000..3379ef70599090fce95519338f69d96006b817d4 --- /dev/null +++ b/tests/Material/ArticleValidate.php @@ -0,0 +1,66 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material; + +use W7\Validate\Support\ValidateScene; +use W7\Validate\Validate; + +class ArticleValidate extends Validate +{ + protected $rule = [ + 'id' => 'required|numeric', + 'content' => 'required|between:1,2000', + 'title' => 'required|between:4,50|chs|checkTitle', + 'type' => 'required|numeric', + ]; + + protected $message = [ + 'id.required' => '缺少参数:文章Id', + 'id.numeric' => '参数错误:文章Id', + 'content.required' => '文章内容必须填写', + 'content.digits_between' => '文章长度为1~2000个字符', + 'title.required' => '文章标题必须填写', + 'title.digits_between' => '文章标题格式错误', + 'title.between' => '文章标题长度为4~50个字符', + 'title.chs' => '文章标题只能为中文', + 'type.required' => '文章分类必须填写', + 'type.numeric' => '文章分类错误', + 'title.checkTitle' => '有错误啦' + ]; + + protected $scene = [ + 'add' => ['content', 'title'], + 'save' => ['next' => 'edit'], + 'del' => ['id'], + ]; + + public function ruleCheckTitle() + { + return true; + } + + public function sceneEdit(ValidateScene $scene) + { + return $scene->only(['id', 'content', 'title']) + ->append('id', 'max:10000') + ->remove('content', 'between') + ->remove('title', null) + ->append('title', 'required|between:4,50|alpha'); + } + + public function sceneDynamic(ValidateScene $scene) + { + return $scene->only(['title', 'content']) + ->remove('content', 'between'); + } +} diff --git a/tests/Material/BaseTestValidate.php b/tests/Material/BaseTestValidate.php new file mode 100644 index 0000000000000000000000000000000000000000..5deccac1554b1f116902cf4a681b168b8c3410d7 --- /dev/null +++ b/tests/Material/BaseTestValidate.php @@ -0,0 +1,29 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material; + +use PHPUnit\Framework\TestCase; +use W7\Validate\Support\Storage\ValidateConfig; +use Itwmw\Validation\Factory; + +class BaseTestValidate extends TestCase +{ + public function __construct($name = null, array $data = [], $dataName = '') + { + $factory = new Factory(); + ValidateConfig::instance()->setFactory($factory) + ->setRulesPath('W7\\Tests\\Material\\Rules\\'); + + parent::__construct($name, $data, $dataName); + } +} diff --git a/tests/Material/Count.php b/tests/Material/Count.php new file mode 100644 index 0000000000000000000000000000000000000000..a240c2213f5d0212cb645b29dbb688f66d55c0d0 --- /dev/null +++ b/tests/Material/Count.php @@ -0,0 +1,56 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material; + +use PHPUnit\Framework\Assert; + +class Count +{ + private static $count; + + public static function incremental(string $name) + { + self::$count[$name] = (self::$count[$name] ?? 0) + 1; + } + + public static function decrease(string $name) + { + self::$count[$name] = (self::$count[$name] ?? 0) - 1; + } + + public static function reset(string $name) + { + self::$count[$name] = 0; + } + + public static function value(string $name, int $value = null) + { + if (null === $value) { + return self::$count[$name] ?? 0; + } + + self::$count[$name] = $value; + return $value; + } + + public static function __callStatic($name, $arguments) + { + return self::value($name, ...$arguments); + } + + public static function assertEquals($expected, string $name, string $message = '', float $delta = 0.0, int $maxDepth = 10, bool $canonicalize = false, bool $ignoreCase = false) + { + $actual = static::value($name); + Assert::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase); + } +} diff --git a/tests/Material/Event/CheckIsChs.php b/tests/Material/Event/CheckIsChs.php new file mode 100644 index 0000000000000000000000000000000000000000..d712fca8ee0a5c798280478ff1c24e1068241139 --- /dev/null +++ b/tests/Material/Event/CheckIsChs.php @@ -0,0 +1,32 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material\Event; + +use W7\Validate\Support\Event\ValidateEventAbstract; + +class CheckIsChs extends ValidateEventAbstract +{ + protected $field; + + public $message = '不是中文'; + + public function __construct($field) + { + $this->field = $field; + } + + public function afterValidate(): bool + { + return is_scalar($this->data[$this->field]) && 1 === preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', (string)$this->data[$this->field]); + } +} diff --git a/tests/Material/Rules/AlphaNum.php b/tests/Material/Rules/AlphaNum.php new file mode 100644 index 0000000000000000000000000000000000000000..dbaf89649a197a68d719146224deb27da096f5c4 --- /dev/null +++ b/tests/Material/Rules/AlphaNum.php @@ -0,0 +1,25 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material\Rules; + +use W7\Validate\Support\Rule\BaseRule; + +class AlphaNum extends BaseRule +{ + protected $message = ':attribute的值只能具有英文字母,数字'; + + public function passes($attribute, $value): bool + { + return is_scalar($value) && 1 === preg_match('/^[A-Za-z0-9]+$/', (string)$value); + } +} diff --git a/tests/Material/Rules/Chs.php b/tests/Material/Rules/Chs.php new file mode 100644 index 0000000000000000000000000000000000000000..636d868d0dd6b7b36a87f3b4261988dfff4bdaf8 --- /dev/null +++ b/tests/Material/Rules/Chs.php @@ -0,0 +1,36 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material\Rules; + +use W7\Validate\Support\Rule\BaseRule; + +class Chs extends BaseRule +{ + /** + * 默认错误消息 + * @var string + */ + protected $message = ':attribute的值只能具有中文'; + + /** + * 确定验证规则是否通过。 + * + * @param mixed $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value): bool + { + return is_scalar($value) && 1 === preg_match('/^[\x{4e00}-\x{9fa5}]+$/u', (string)$value); + } +} diff --git a/tests/Material/Rules/ChsAlphaNum.php b/tests/Material/Rules/ChsAlphaNum.php new file mode 100644 index 0000000000000000000000000000000000000000..72c47ff0d51f1e9cda1d281fd1cb6c10977099eb --- /dev/null +++ b/tests/Material/Rules/ChsAlphaNum.php @@ -0,0 +1,25 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material\Rules; + +use W7\Validate\Support\Rule\BaseRule; + +class ChsAlphaNum extends BaseRule +{ + protected $message = ':attribute的值只能具有中文,字母,数字'; + + public function passes($attribute, $value): bool + { + return is_scalar($value) && 1 === preg_match('/^[\x{4e00}-\x{9fa5}a-zA-Z0-9]+$/u', (string)$value); + } +} diff --git a/tests/Material/Rules/Length.php b/tests/Material/Rules/Length.php new file mode 100644 index 0000000000000000000000000000000000000000..38c8bba24e61fb132b06458f79637a2800de5838 --- /dev/null +++ b/tests/Material/Rules/Length.php @@ -0,0 +1,37 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material\Rules; + +use W7\Validate\Support\Rule\BaseRule; + +/** + * 通过字节的方式来限定长度 + * @package W7\App\Model\Validate\Rules + */ +class Length extends BaseRule +{ + protected $message = ':attribute的长度需为%d个字节'; + + protected $size; + + public function __construct(int $size) + { + $this->size = $size; + $this->messageParam = [$size]; + } + + public function passes($attribute, $value): bool + { + return strlen($value) === $this->size; + } +} diff --git a/tests/Material/Rules/LengthBetween.php b/tests/Material/Rules/LengthBetween.php new file mode 100644 index 0000000000000000000000000000000000000000..92738b3f3d1977e0feaa0fb3956995ff991cebb0 --- /dev/null +++ b/tests/Material/Rules/LengthBetween.php @@ -0,0 +1,40 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material\Rules; + +use W7\Validate\Support\Rule\BaseRule; + +/** + * 通过字节的方式来限定长度的合法范围 + * @package W7\App\Model\Validate\Rules + */ +class LengthBetween extends BaseRule +{ + protected $message = ':attribute的长度需为%d~%d字节之间'; + + protected $min; + + protected $max; + + public function __construct(int $min, int $max) + { + $this->min = $min; + $this->max = $max; + $this->messageParam = [$min, $max]; + } + + public function passes($attribute, $value): bool + { + return strlen($value) >= $this->min && strlen($value) <= $this->max; + } +} diff --git a/tests/Material/TestValidate.php b/tests/Material/TestValidate.php new file mode 100644 index 0000000000000000000000000000000000000000..7f6729bda51b93ee0cfbe99246ae328e4cef245e --- /dev/null +++ b/tests/Material/TestValidate.php @@ -0,0 +1,43 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material; + +use W7\Tests\Material\Event\CheckIsChs; +use W7\Validate\Validate; + +class TestValidate extends Validate +{ + protected $rule = [ + 'name' => 'required' + ]; + + protected $scene = [ + 'errorEvent' => ['name', 'event' => [CheckIsChs::class => ['name']]], + 'checkName' => ['name', 'after' => ['checkNameIsAdmin' => 'name']], + 'beforeThrowError' => ['before' => 'throwError'] + ]; + + protected function afterCheckNameIsAdmin($data, $field) + { + if (($data[$field] ?? '') === 'admin') { + return true; + } + + return '用户名不是admin'; + } + + protected function beforeThrowError($data) + { + return 'error'; + } +} diff --git a/tests/Material/UserRulesManager.php b/tests/Material/UserRulesManager.php new file mode 100644 index 0000000000000000000000000000000000000000..5e49684a3e169edd1b42cf3bf8db885a1c03c0c7 --- /dev/null +++ b/tests/Material/UserRulesManager.php @@ -0,0 +1,63 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Material; + +use W7\Validate\RuleManager; +use W7\Validate\Support\Concerns\SceneInterface; + +class UserRulesManager extends RuleManager +{ + protected $rule = [ + 'user' => 'required|email', + 'pass' => 'required|lengthBetween:6,16', + 'name' => 'required|chs|lengthBetween:2,4', + 'remark' => 'required|alpha_dash', + 'captcha' => 'required|length:4|checkCaptcha', + ]; + + protected $scene = [ + 'login' => ['user', 'pass'], + 'captcha' => ['captcha'] + ]; + + protected $customAttributes = [ + 'user' => '用户名', + 'pass' => '密码', + 'name' => '昵称', + 'remark' => '备注', + 'captcha' => '验证码', + ]; + + protected $message = [ + 'captcha.checkCaptcha' => '验证码错误', + 'user.email' => '用户名必须为邮箱', + 'pass.lengthBetween' => '密码长度错误' + ]; + + protected function sceneRegister(SceneInterface $scene) + { + return $scene->only(['user', 'pass', 'name', 'remark']) + ->remove('remark', 'required|alpha_dash') + ->append('remark', 'chs'); + } + + protected function sceneRegisterNeedCaptcha(SceneInterface $scene) + { + return $this->sceneRegister($scene)->appendCheckField('captcha'); + } + + public function ruleCheckCaptcha($att, $value): bool + { + return true; + } +} diff --git a/tests/Test/Fixer/TestScene.php b/tests/Test/Fixer/TestScene.php new file mode 100644 index 0000000000000000000000000000000000000000..5f12bdbae49d83e914e995d586cbbe0ab72f9395 --- /dev/null +++ b/tests/Test/Fixer/TestScene.php @@ -0,0 +1,170 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test\Fixer; + +use W7\Tests\Material\BaseTestValidate; +use W7\Tests\Material\Count; +use W7\Validate\Exception\ValidateException; +use W7\Validate\Support\ValidateScene; +use W7\Validate\Validate; + +class TestScene extends BaseTestValidate +{ + /** + * @test 测试在场景中获取当前验证数据为空的问题 + * @see https://gitee.com/we7coreteam/w7-engine-validate/pulls/5 + * @throws ValidateException + */ + public function testSceneCheckDataIsEmpty() + { + $v = new class extends Validate { + public $checkData = []; + + protected $rule = [ + 'name' => 'required' + ]; + + protected function sceneTest(ValidateScene $scene) + { + $this->checkData = $scene->getData(); + $scene->only(['name']); + } + }; + + $v->scene('test')->check([ + 'name' => 123 + ]); + + $this->assertArrayHasKey('name', $v->checkData); + $this->assertEquals(123, $v->checkData['name']); + } + + /** + * @test 测试场景中添加字段 + * @see https://gitee.com/we7coreteam/w7-engine-validate/pulls/3 + * @see https://gitee.com/we7coreteam/w7-engine-validate/pulls/6 + * @throws ValidateException + */ + public function testSceneAppendCheckField() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => 'required', + 'b' => 'required', + 'c' => 'required', + ]; + + protected $message = [ + 'a.required' => 'a不能为空', + 'b.required' => 'b不能为空', + 'c.required' => 'c不能为空', + ]; + + protected function sceneTest(ValidateScene $scene) + { + $scene->only(['a']) + ->appendCheckField('b') + ->appendCheckField('c'); + } + }; + + try { + $v->scene('test')->check([ + 'a' => 1 + ]); + } catch (ValidateException $e) { + $this->assertEquals('b不能为空', $e->getMessage()); + } + + try { + $v->scene('test')->check([ + 'a' => 1, + 'b' => 1 + ]); + } catch (ValidateException $e) { + $this->assertEquals('c不能为空', $e->getMessage()); + } + + $data = $v->scene('test')->check([ + 'a' => 1, + 'b' => 1, + 'c' => 1, + ]); + + $this->assertEmpty(array_diff_key($data, array_flip(['a', 'b', 'c']))); + } + + /** + * @test 测试在next场景中,如果返回空场景名导致的问题 + * @see https://gitee.com/we7coreteam/w7-engine-validate/commit/db5ba9de603ac9e167fd46d2b90826595060813b + * @throws ValidateException + */ + public function testNextSceneIsEmpty() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => 'required' + ]; + + protected $scene = [ + 'testA' => ['a', 'next' => 'test'], + ]; + + protected function testSelector(): string + { + Count::incremental('emptyScene'); + return ''; + } + }; + + $data = $v->scene('testA')->check(['a' => 1]); + $this->assertEquals(1, Count::value('emptyScene')); + $this->assertEquals(1, $data['a']); + } + + /** + * @test 测试在next中规则为原始规则的BUG + * @see https://gitee.com/we7coreteam/w7-engine-validate/commit/f0cefc381e3dd90a8faf69514eb6a2d6016ede77 + * @throws ValidateException + */ + public function testRulesAreNotParsedForNext() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => 'required|numeric|min:1|test', + 'b' => 'required|numeric|min:1', + ]; + + protected $scene = [ + 'testA' => ['a', 'next' => 'testNext'], + 'testNext' => ['a', 'b'] + ]; + + protected function sceneTestB(ValidateScene $scene) + { + $scene->only(['a'])->next('testNext'); + } + + protected function ruleTest() + { + return true; + } + }; + + $data = $v->scene('testA')->check(['a' => 2, 'b' => 3]); + $this->assertEquals(2, $data['a']); + + $data = $v->scene('testB')->check(['a' => 2, 'b' => 3]); + $this->assertEquals(2, $data['a']); + } +} diff --git a/tests/Test/TestCustomMessageProvider.php b/tests/Test/TestCustomMessageProvider.php new file mode 100644 index 0000000000000000000000000000000000000000..e1ccc3c965ac5c0cfd7adef17f4cbf3b2c4b55c8 --- /dev/null +++ b/tests/Test/TestCustomMessageProvider.php @@ -0,0 +1,60 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; +use W7\Validate\Exception\ValidateRuntimeException; +use W7\Validate\Support\Concerns\MessageProviderInterface; +use W7\Validate\Support\MessageProvider; +use W7\Validate\Validate; + +class TestMessageProvider extends MessageProvider implements MessageProviderInterface +{ +} + +class TestCustomMessageProvider extends BaseTestValidate +{ + /** + * @test 测试多种方式设置消息处理器 + * + * @throws \W7\Validate\Exception\ValidateException + */ + public function testSetMessageProvider() + { + Validate::make()->setMessageProvider(TestMessageProvider::class); + Validate::make()->setMessageProvider(new TestMessageProvider()); + Validate::make()->setMessageProvider(function () { + return new TestMessageProvider(); + }); + + $this->expectException(ValidateRuntimeException::class); + Validate::make()->setMessageProvider('Test'); + } + + public function testGetMessage() + { + $messageProvider = new TestMessageProvider(); + $messageProvider->setData([ + 'user' => 'admin', + 'pass' => '123456' + ]); + + $messageProvider->setCustomAttributes([ + 'user' => '账号', + 'pass' => '密码' + ]); + + $message = $messageProvider->handleMessage('{@user}:{:user},{@pass}:{:pass}'); + $this->assertEquals('账号:admin,密码:123456', $message); + } +} diff --git a/tests/Test/TestCustomRuleAndMessage.php b/tests/Test/TestCustomRuleAndMessage.php new file mode 100644 index 0000000000000000000000000000000000000000..33645abcb610680c640139d4c7fd7625b2e97d25 --- /dev/null +++ b/tests/Test/TestCustomRuleAndMessage.php @@ -0,0 +1,423 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use Itwmw\Validation\Support\Interfaces\ImplicitRule; +use Itwmw\Validation\Support\Arr; +use W7\Tests\Material\BaseTestValidate; +use W7\Tests\Material\Rules\Length; +use W7\Validate\Exception\ValidateException; +use W7\Validate\Support\Rule\BaseRule; +use W7\Validate\Support\ValidateScene; +use W7\Validate\Validate; +use function PHPUnit\Framework\assertEquals; + +class TestCustomRuleA extends Validate +{ + protected $rule = [ + 'num' => 'numberIsTen' + ]; + + protected $message = [ + 'num.numberIsTen' => '给定的参数不是10' + ]; + + protected function ruleNumberIsTen($att, $value): bool + { + return 10 === (int)$value; + } +} + +class TestCustomRuleB extends TestCustomRuleA +{ + protected $message = [ + 'num.numberIsTen' => '给定的参数不是十' + ]; + + protected function ruleNumberIsTen($att, $value): bool + { + return '十' === (string)$value; + } +} +class TestExtendRule extends Validate +{ + public function __construct() + { + self::extend('mobile', function ($attribute, $value) { + return is_scalar($value) && 1 === preg_match('/^1[3-9]\d{9}$/', (string)$value); + }, ':attribute不是有效的手机号码'); + } + + protected $rule = [ + 'bind' => 'mobile' + ]; + + protected $customAttributes = [ + 'bind' => '绑定手机号' + ]; + + protected function sceneReplacerMobileMessage(ValidateScene $scene) + { + $scene->only(['bind']); + self::replacer('mobile', function ($message, $attribute, $rule, $parameters) { + return ($this->customAttributes[$attribute] ?? $attribute) . '是错误的手机号码'; + }); + } +} + +class TestImplicitRule extends Validate +{ + public function __construct() + { + self::extendImplicit('isNotEmpty', function ($attribute, $value) { + return !empty($value); + }, '给定的值为空'); + } + + protected $rule = [ + 'content' => 'isNotEmpty' + ]; +} + +class TestDependentRule extends Validate +{ + public function __construct() + { + self::extendDependent('contains', function ($attribute, $value, $parameters, $validator) { + return str_contains($value, Arr::get($validator->getData(), $parameters[0])); + }, '不支持该域的邮箱'); + } + + protected $rule = [ + '*.email' => 'contains:*.provider' + ]; +} + +class TestImplicitRuleClass extends BaseRule implements ImplicitRule +{ + protected $message = '给定的值为空'; + + public function passes($attribute, $value): bool + { + return !empty($value); + } +} + +class TestCustomRuleAndMessage extends BaseTestValidate +{ + public function testCustomRuleIsObject() + { + $v = Validate::make([ + 'id' => [ + new class extends BaseRule { + protected $message = '输入的字符不合格'; + + public function passes($attribute, $value): bool + { + return is_numeric($value); + } + } + ] + ]); + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^输入的字符不合格$/'); + + $v->check([ + 'id' => 'aaa' + ]); + } + /** + * @test 测试依赖规则 + * + * @throws ValidateException + */ + public function testDependentRule() + { + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^不支持该域的邮箱$/'); + + TestDependentRule::make()->check([ + ['email' => '995645888@qq.com', 'provider' => 'qq.com'], + ['email' => '351409246@qq.com', 'provider' => 'qq.com'], + ['email' => 'admin@itwmw.com', 'provider' => 'qq.com'] + ]); + } + + /** + * @test 测试当值为空,规则也依旧执行(方法扩展) + * @throws ValidateException + */ + public function testImplicitRule() + { + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^给定的值为空$/'); + TestImplicitRule::make()->check([]); + } + + /** + * @test 测试当值为空,规则也依旧执行(规则类) + * + * @throws ValidateException + */ + public function testImplicitRuleForRuleClass() + { + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^给定的值为空$/'); + Validate::make([ + 'a' => [ + new TestImplicitRuleClass() + ] + ])->check([]); + } + + /** + * @test 测试扩展规则和对应的错误消息是否生效 + * @throws ValidateException + */ + public function testExtendRule() + { + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^绑定手机号不是有效的手机号码$/'); + TestExtendRule::make()->check([ + 'bind' => 123 + ]); + } + + /** + * @test 测试修改扩展规则对应的错误消息 + * @throws ValidateException + */ + public function testReplacerErrorMessage() + { + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^绑定手机号是错误的手机号码$/'); + TestExtendRule::make()->scene('replacerMobileMessage')->check([ + 'bind' => 123 + ]); + } + + /** + * @test 测试多个验证器定义相同的规则名,规则是否会冲突 + */ + public function testSameNameRule() + { + try { + $data = TestCustomRuleA::make()->check([ + 'num' => 0 + ]); + } catch (ValidateException $e) { + $this->assertSame('给定的参数不是10', $e->getMessage(), '返回的错误消息不符合预期'); + } + $this->assertFalse(isset($data), '验证错误的通过'); + + try { + $data = TestCustomRuleB::make()->check([ + 'num' => 10 + ]); + } catch (ValidateException $e) { + $this->assertSame('给定的参数不是十', $e->getMessage(), '返回的错误消息不符合预期'); + } + + $this->assertFalse(isset($data), '验证错误的通过'); + } + + /** + * @ test 规则单独使用 + */ + public function testSeparateUseRules() + { + $this->assertTrue(Length::make(5)->check(12345)); + $this->assertFalse(Length::make(5)->check(1234)); + } + + /** + * @test 测试传递参数到自定义规则 + * @throws ValidateException + */ + public function testPassingParamsToCustomRules() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => 'test:111' + ]; + + protected function ruleTest($attribute, $value, $parameters) + { + assertEquals(111, $parameters[0]); + return false; + } + + protected $message = [ + 'a.test' => 'testErrorMessage' + ]; + }; + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^testErrorMessage$/'); + $v->check([ + 'a' => 123 + ]); + } + + /** + * @test 测试全局定义的规则 + * @throws ValidateException + */ + public function testGlobalExtendRule() + { + Validate::extend('sex', function ($attribute, $value) { + return in_array($value, ['男', '女']); + }, '请输入主流性别'); + + $v = new class extends Validate { + protected $rule = [ + 'sex' => 'required|sex' + ]; + }; + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^请输入主流性别$/'); + $v->check([ + 'sex' => 1 + ]); + } + + /** + * @test 测试当全局规则和类规则名称相同时的优先级处理是否符合预期 + * @depends testGlobalExtendRule + */ + public function testExtendRulePriority() + { + $v = new class extends Validate { + protected $rule = [ + 'sex' => 'required|sex' + ]; + + protected function ruleSex($attribute, $value): bool + { + return in_array($value, [0, 1]); + } + }; + + $data = $v->check([ + 'sex' => 1 + ]); + + $this->assertSame(1, $data['sex']); + } + + /** + * @test 测试在类中使用`extendImplicitRule`方法扩展存在规则 + * @throws ValidateException + */ + public function testExtendImplicitRuleInClass() + { + $v = new class extends Validate { + public function __construct() + { + $this->extendImplicitRule('empty', function ($attribute, $value) { + return !empty($value); + }, ':attribute参数不可为空'); + } + + protected $rule = [ + 'name' => 'empty' + ]; + }; + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^name参数不可为空$/'); + $v->check([]); + } + + /** + * @test 测试在类中使用`extendDependentRule`方法扩展依赖规则 + */ + public function testExtendDependentRuleInClass() + { + $v = new class extends Validate { + public function __construct() + { + $this->extendDependentRule('contains', function ($attribute, $value, $parameters, $validator) { + return str_contains($value, Arr::get($validator->getData(), $parameters[0])); + }, '不支持该域的邮箱'); + } + + protected $rule = [ + '*.email' => 'contains:*.provider' + ]; + }; + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^不支持该域的邮箱$/'); + + $v->check([ + ['email' => '995645888@qq.com', 'provider' => 'qq.com'], + ['email' => '351409246@qq.com', 'provider' => 'qq.com'], + ['email' => 'admin@itwmw.com', 'provider' => 'qq.com'] + ]); + } + + /** + * @test 测试替换全局规则的错误消息 + * @depends testGlobalExtendRule + */ + public function testReplacerGlobalRuleMessage() + { + Validate::replacer('sex', function () { + return ':attribute错误,请输入正确性别'; + }); + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^性别错误,请输入正确性别$/'); + + Validate::make([ + 'sex' => 'required|sex' + ], [], [ + 'sex' => '性别' + ])->check([ + 'sex' => 666 + ]); + } + + /** + * @test 为类方法规则定义错误消息 + * @throws ValidateException + */ + public function testCustomRuleMessage() + { + $v = new class extends Validate { + protected $ruleMessage = [ + 'isChildren' => '不是未成年' + ]; + + protected $rule = [ + 'age' => 'numeric|isChildren' + ]; + + protected function ruleIsChildren($attribute, $value): bool + { + return $value < 18; + } + }; + + $data = $v->check(['age' => 15]); + $this->assertSame(15, $data['age']); + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^不是未成年$/'); + + $v->check(['age' => 20]); + } +} diff --git a/tests/Test/TestDataDefault.php b/tests/Test/TestDataDefault.php new file mode 100644 index 0000000000000000000000000000000000000000..2e6aa598b7f9758980a9cf8be3c8bef9ce778561 --- /dev/null +++ b/tests/Test/TestDataDefault.php @@ -0,0 +1,229 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; +use W7\Validate\Exception\ValidateException; +use W7\Validate\Support\Concerns\DefaultInterface; +use W7\Validate\Support\DataAttribute; +use W7\Validate\Support\ValidateScene; +use W7\Validate\Validate; + +class SetDefaultIsHello implements DefaultInterface +{ + public function handle($value, string $attribute, array $originalData) + { + return 'Hello'; + } +} + +class TestDataDefault extends BaseTestValidate +{ + public function testDefaultIsScalar() + { + $v = new class extends Validate { + protected $rule = [ + 'name' => 'required' + ]; + + protected $default = [ + 'name' => '123' + ]; + }; + + $data = $v->check([]); + + $this->assertSame('123', $data['name']); + } + + public function testDefaultIsArray() + { + $v = new class extends Validate { + protected $rule = [ + 'name' => 'required' + ]; + + protected $default = [ + 'name' => ['a', 'b', 'any' => 123] + ]; + }; + + $data = $v->check([]); + + $this->assertEquals(['a', 'b', 'any' => 123], $data['name']); + + $v = new class extends Validate { + protected $rule = [ + 'name' => 'required' + ]; + + protected $default = [ + 'name' => ['value' => ['a', 'b'], 'any' => true] + ]; + }; + + $data = $v->check([]); + + $this->assertEquals(['a', 'b'], $data['name']); + } + + public function testDefaultIsCallback() + { + $v = new class extends Validate { + protected $rule = [ + 'name' => 'required', + 'age' => 'required|numeric', + 'sex' => 'required' + ]; + + public function __construct() + { + $this->default = [ + 'name' => function ($value) { + return '小张'; + }, + 'age' => [$this, 'setAge'], + 'sex' => 'setSex' + ]; + } + + public function setAge($value) + { + return 100; + } + + public function defaultSetSex($value) + { + return '男'; + } + }; + + $data = $v->check([]); + + $this->assertEquals(['name' => '小张', 'age' => 100, 'sex' => '男'], $data); + } + + public function testHandlerData() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required' + ]; + + public function __construct() + { + $this->default = [ + 'id' => ['value' => function ($value) { + if (is_string($value)) { + return explode(',', $value); + } + return $value; + }, 'any' => true] + ]; + } + }; + + $data = $v->check([ + 'id' => '1,2,3,4' + ]); + + $this->assertEquals([1, 2, 3, 4], $data['id']); + } + + public function testDefaultForScene() + { + $v = new class extends Validate { + protected $rule = [ + 'name' => 'required' + ]; + + protected function sceneTest(ValidateScene $scene) + { + $scene->only(['name']) + ->default('name', '小张'); + } + }; + + $data = $v->scene('test')->check([]); + $this->assertSame('小张', $data['name']); + + $this->expectException(ValidateException::class); + $v->check([]); + } + + public function testCancelDefaultValue() + { + $v = new class extends Validate { + protected $rule = [ + 'name' => '' + ]; + + protected $default = [ + 'name' => 1 + ]; + + protected function sceneTest(ValidateScene $scene) + { + $scene->only(['name']) + ->default('name', null); + } + }; + + $data = $v->check([]); + $this->assertEquals(1, $data['name']); + + $data = $v->scene('test')->check([]); + $this->assertArrayNotHasKey('name', $data); + } + + public function testDefaultUseDefaultClass() + { + $v = new class extends Validate { + protected $rule = [ + 'name' => '' + ]; + + protected $default = [ + 'name' => SetDefaultIsHello::class + ]; + }; + + $data = $v->check([]); + $this->assertSame('Hello', $data['name']); + } + + public function testDefaultDeleteField() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => 'numeric' + ]; + + protected $default = [ + 'a' => ['value' => 'deleteField', 'any' => true] + ]; + + public function defaultDeleteField($value, $field, $data, DataAttribute $dataAttribute) + { + $dataAttribute->deleteField = true; + return ''; + } + }; + + $data = $v->check([ + 'a' => 123 + ]); + + $this->assertTrue(empty($data)); + } +} diff --git a/tests/Test/TestDataFilter.php b/tests/Test/TestDataFilter.php new file mode 100644 index 0000000000000000000000000000000000000000..95b8c24f91acfe7228bc0e71308d2588383089da --- /dev/null +++ b/tests/Test/TestDataFilter.php @@ -0,0 +1,212 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; +use W7\Validate\Exception\ValidateRuntimeException; +use W7\Validate\Support\Concerns\FilterInterface; +use W7\Validate\Support\DataAttribute; +use W7\Validate\Support\ValidateScene; +use W7\Validate\Validate; + +class UniqueFilter implements FilterInterface +{ + public function handle($value) + { + return array_unique($value); + } +} +class TestDataFilter extends BaseTestValidate +{ + public function testSetFilterIsSystemMethod() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required|numeric' + ]; + + protected $filter = [ + 'id' => 'intval' + ]; + }; + + $data = $v->check(['id' => '1']); + + $this->assertTrue(1 === $data['id']); + } + + public function testSetFilterIsClassMethod() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required' + ]; + + protected $filter = [ + 'id' => 'toArray' + ]; + + public function filterToArray($value) + { + return explode(',', $value); + } + }; + + $data = $v->check(['id' => '1,2,3,4,5']); + + $this->assertEquals([1, 2, 3, 4, 5], $data['id']); + } + + public function testSetFilterIsFilterClass() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required|array', + 'id.*' => 'numeric' + ]; + + protected $filter = [ + 'id' => UniqueFilter::class + ]; + }; + + $data = $v->check(['id' => [1, 1, 2, 3, 4, 4, 5, 6, 7]]); + + $this->assertEquals([1, 2, 3, 4, 5, 6, 7], array_values($data['id'])); + } + + public function testSetFilterForArrayField() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required|array', + 'id.*' => 'numeric' + ]; + + protected $filter = [ + 'id.*' => 'intval' + ]; + }; + + $data = $v->check(['id' => ['1', '2', 3, '4']]); + + foreach ($data['id'] as $id) { + $this->assertSame('integer', gettype($id)); + } + } + + /** + * @test 测试当数据不存在时,过滤器的处理 + * + * @throws \W7\Validate\Exception\ValidateException + */ + public function testNotHasDataFilter() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'numeric' + ]; + + protected $filter = [ + 'id' => 'intval' + ]; + }; + + $data = $v->check([]); + $this->assertArrayNotHasKey('id', $data); + } + + /** + * @test 测试场景中取消设置过滤器 + * + * @throws \W7\Validate\Exception\ValidateException + */ + public function testCancelFilter() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required' + ]; + + protected $filter = [ + 'id' => 'intval' + ]; + + protected function sceneTest(ValidateScene $scene) + { + $scene->only(['id']) + ->filter('id', null); + } + }; + + $data = $v->check(['id' => '你好']); + $this->assertSame(0, $data['id']); + $data = $v->scene('test')->check(['id' => '你好']); + $this->assertSame('你好', $data['id']); + } + + /** + * @test 测试过滤器中删除字段 + * + * @throws \W7\Validate\Exception\ValidateException + */ + public function testFilterDeleteField() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => '' + ]; + + protected $filter = [ + 'a' => 'deleteField' + ]; + + public function filterDeleteField($value, DataAttribute $dataAttribute) + { + $dataAttribute->deleteField = true; + return ''; + } + }; + + $data = $v->check([ + 'a' => 123 + ]); + + $this->assertTrue(empty($data)); + } + + /** + * @test 测试不存在的过滤器 + * + * @throws \W7\Validate\Exception\ValidateException + */ + public function testNonexistentFilter() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => 'required' + ]; + + protected function sceneTest(ValidateScene $scene) + { + $scene->only(['a'])->filter('a', 'test'); + } + }; + + $this->expectException(ValidateRuntimeException::class); + + $v->scene('test')->check([ + 'a' => 123 + ]); + } +} diff --git a/tests/Test/TestHandlerEvent.php b/tests/Test/TestHandlerEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..b30e2fb7083330b184b03c41f3c1e4267e167482 --- /dev/null +++ b/tests/Test/TestHandlerEvent.php @@ -0,0 +1,300 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use PHPUnit\Framework\Assert; +use W7\Tests\Material\BaseTestValidate; +use W7\Tests\Material\Count; +use W7\Tests\Material\TestValidate; +use W7\Validate\Exception\ValidateException; +use W7\Validate\Exception\ValidateRuntimeException; +use W7\Validate\Support\Event\ValidateEventAbstract; +use W7\Validate\Support\ValidateScene; +use W7\Validate\Validate; + +class TestGlobalEvent extends ValidateEventAbstract +{ + public function afterValidate(): bool + { + Assert::assertEquals(1, Count::value('sceneEventAfter-A')); + Assert::assertEquals(1, Count::value('sceneEventAfter-B')); + Assert::assertEquals(1, Count::value('sceneEventAfter-C')); + Assert::assertEquals(1, Count::value('testAfter')); + Assert::assertEquals(1, Count::value('customSceneEventAfter')); + + Count::incremental('globalEventAfter'); + return true; + } + + public function beforeValidate(): bool + { + Assert::assertEquals(0, Count::value('sceneEventBefore-A')); + Assert::assertEquals(0, Count::value('sceneEventBefore-B')); + Assert::assertEquals(0, Count::value('sceneEventBefore-C')); + Assert::assertEquals(0, Count::value('testBefore')); + Assert::assertEquals(0, Count::value('customSceneEventBefore')); + + Count::incremental('globalEventBefore'); + return true; + } +} + +class TestSceneEventA extends ValidateEventAbstract +{ + public function afterValidate(): bool + { + Count::incremental('sceneEventAfter-A'); + return true; + } + + public function beforeValidate(): bool + { + Count::incremental('sceneEventBefore-A'); + return true; + } +} + +class TestSceneEventB extends ValidateEventAbstract +{ + public function afterValidate(): bool + { + Count::incremental('sceneEventAfter-B'); + return true; + } + + public function beforeValidate(): bool + { + Count::incremental('sceneEventBefore-B'); + return true; + } +} + +class TestSceneEventC extends ValidateEventAbstract +{ + public function afterValidate(): bool + { + Count::incremental('sceneEventAfter-C'); + return true; + } + + public function beforeValidate(): bool + { + Count::incremental('sceneEventBefore-C'); + return true; + } +} + +class TestCustomSceneEvent extends ValidateEventAbstract +{ + public function afterValidate(): bool + { + Count::incremental('customSceneEventAfter'); + return true; + } + + public function beforeValidate(): bool + { + Count::incremental('customSceneEventBefore'); + return true; + } +} + +class TestErrorEventForBefore extends ValidateEventAbstract +{ + public $message = '该操作已完成'; + + public function beforeValidate(): bool + { + Count::incremental('testEventBefore'); + return 1 === Count::value('testEventBefore'); + } +} +class TestHandlerEvent extends BaseTestValidate +{ + public function testErrorEvent() + { + $v = new TestValidate(); + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^不是中文$/'); + $v->scene('errorEvent')->check([ + 'name' => 123 + ]); + } + + public function testErrorEventForBefore() + { + $v = new class extends Validate { + protected $event = [ + TestErrorEventForBefore::class + ]; + }; + + $v->check([]); + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^该操作已完成$/'); + + $v->check([]); + } + + public function testEventIsCheckName() + { + $v = new TestValidate(); + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^用户名不是admin$/'); + $v->scene('checkName')->check([ + 'name' => 123 + ]); + } + + public function testBeforeThrowError() + { + $v = new TestValidate(); + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^error$/'); + $v->scene('beforeThrowError')->check([]); + } + + /** + * @test 测试事件在场景中是否正确运行,以及全局事件和场景事件的执行顺序是否正确 + * + * 全局事件before->场景事件->全局事件after + * @throws ValidateException + */ + public function testEventExecution() + { + $v = new class extends Validate { + protected $event = [ + TestGlobalEvent::class + ]; + + protected $scene = [ + 'testA' => ['event' => TestSceneEventA::class, 'next' => 'testB'], + 'testB' => ['event' => [TestSceneEventB::class, TestSceneEventC::class], 'next' => 'testC'], + 'testC' => ['before' => 'testBefore', 'after' => 'testAfter', 'next' => 'testD'] + ]; + + protected function sceneTestD(ValidateScene $scene) + { + $scene->after('customSceneEvent') + ->before('customSceneEvent') + ->event(TestCustomSceneEvent::class); + } + + protected function beforeCustomSceneEvent() + { + Count::incremental('beforeCustomSceneEvent'); + return true; + } + + protected function afterCustomSceneEvent() + { + Count::incremental('afterCustomSceneEvent'); + return true; + } + + protected function beforeTestBefore() + { + Count::incremental('testBefore'); + return true; + } + + protected function afterTestAfter() + { + Count::incremental('testAfter'); + return true; + } + }; + + $v->scene('testA')->check([]); + + $this->assertEquals(1, Count::value('testBefore')); + $this->assertEquals(1, Count::value('testAfter')); + + $this->assertEquals(1, Count::value('sceneEventAfter-A')); + $this->assertEquals(1, Count::value('sceneEventBefore-A')); + + $this->assertEquals(1, Count::value('sceneEventAfter-B')); + $this->assertEquals(1, Count::value('sceneEventBefore-B')); + + $this->assertEquals(1, Count::value('sceneEventAfter-C')); + $this->assertEquals(1, Count::value('sceneEventBefore-C')); + + $this->assertEquals(1, Count::value('globalEventAfter')); + $this->assertEquals(1, Count::value('globalEventBefore')); + + $this->assertEquals(1, Count::value('customSceneEventAfter')); + $this->assertEquals(1, Count::value('customSceneEventBefore')); + } + + /** + * @test 测试场景中 事件和闭包方法的优先级 + * + * @throws ValidateException + */ + public function testEventPriority() + { + $v = new class extends Validate { + protected function sceneEventCallback(ValidateScene $scene) + { + $scene->setEventPriority(false) + ->event(TestSceneEventA::class) + ->before(function () { + Count::assertEquals(0, 'sceneEventBefore-A'); + return true; + }) + ->after(function () { + Count::assertEquals(1, 'sceneEventAfter-A'); + return true; + }); + } + + protected function sceneEventPriority(ValidateScene $scene) + { + $scene->setEventPriority(true) + ->event(TestSceneEventA::class) + ->before(function () { + Count::assertEquals(1, 'sceneEventBefore-A'); + return true; + }) + ->after(function () { + Count::assertEquals(0, 'sceneEventAfter-A'); + return true; + }); + } + }; + + Count::reset('sceneEventAfter-A'); + Count::reset('sceneEventBefore-A'); + $v->scene('eventPriority')->check([]); + Count::reset('sceneEventAfter-A'); + Count::reset('sceneEventBefore-A'); + $v->scene('eventCallback')->check([]); + } + + /** + * @test 测试当指定的事件类不存在时 + * + * @throws ValidateException + */ + public function testNonexistentEvent() + { + $v = new class extends Validate { + protected $event = [ + 'test' + ]; + }; + $this->expectException(ValidateRuntimeException::class); + $v->check([]); + } +} diff --git a/tests/Test/TestHandlerFunction.php b/tests/Test/TestHandlerFunction.php new file mode 100644 index 0000000000000000000000000000000000000000..35dcd0d3aa1e1592768c777e1f8c66ebfbb8770c --- /dev/null +++ b/tests/Test/TestHandlerFunction.php @@ -0,0 +1,85 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; +use W7\Validate\Exception\ValidateException; +use W7\Validate\Exception\ValidateRuntimeException; +use W7\Validate\Validate; + +class TestHandlerFunction extends BaseTestValidate +{ + public function testAfterFunction() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required' + ]; + + protected $scene = [ + 'testAfter' => ['id', 'after' => 'checkId'] + ]; + + protected function afterCheckId($data) + { + if ($data['id'] < 0) { + return 'ID错误'; + } + return true; + } + }; + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^ID错误$/'); + + $v->scene('testAfter')->check(['id' => -1]); + } + + public function testBeforeFunction() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required' + ]; + + protected $scene = [ + 'testBefore' => ['id', 'before' => 'checkSiteStatus'] + ]; + + protected function beforeCheckSiteStatus(array $data) + { + return '站点未开启'; + } + }; + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^站点未开启$/'); + + $v->scene('testBefore')->check([]); + } + + /** + * @test 测试当指定的方法不存在时 + * + * @throws ValidateException + */ + public function testNonexistentFunction() + { + $v = Validate::make()->setScene([ + 'test' => ['after' => '111', 'before' => '222'] + ]); + + $this->expectException(ValidateRuntimeException::class); + $v->scene('test')->check([]); + } +} diff --git a/tests/Test/TestRegexRule.php b/tests/Test/TestRegexRule.php new file mode 100644 index 0000000000000000000000000000000000000000..c3ddb6130b099622eb2d739bf638f3f871845881 --- /dev/null +++ b/tests/Test/TestRegexRule.php @@ -0,0 +1,123 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; +use W7\Validate\Exception\ValidateException; +use W7\Validate\Support\ValidateScene; +use W7\Validate\Validate; + +class TestRegexRule extends BaseTestValidate +{ + /** + * @test 测试正则表达式`regex`规则是否可以正常使用 + * @throws ValidateException + */ + public function testRegexRule() + { + $v = new class extends Validate { + protected $regex = [ + 'number' => '/^\d+$/' + ]; + + protected $rule = [ + 'num' => 'required|regex:number' + ]; + + protected $message = [ + 'num.regex' => '给定的值必须是数字' + ]; + }; + + $data = $v->check([ + 'num' => '123' + ]); + $this->assertEquals('123', $data['num']); + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^给定的值必须是数字$/'); + $v->check([ + 'num' => 'sss' + ]); + } + + /** + * @test 测试正则表达式`not_regex`规则是否可以正常使用 + * @throws ValidateException + */ + public function testNotRegexRule() + { + $v = new class extends Validate { + protected $regex = [ + 'number' => '/^\d+$/' + ]; + + protected $rule = [ + 'user' => 'required|not_regex:number' + ]; + + protected $message = [ + 'user.not_regex' => '给定的值不可以为纯数字' + ]; + }; + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^给定的值不可以为纯数字$/'); + $v->check([ + 'user' => '123' + ]); + + $data = $v->check([ + 'user' => 'a1b2' + ]); + $this->assertEquals('a1b2', $data['user']); + } + + /** + * @test 测试正则表示式规则在验证场景中的使用 + * @throws ValidateException + */ + public function testRegexRuleInScene() + { + $v = new class extends Validate { + protected $regex = [ + 'status' => '/^0|1|on|off|true|false$/' + ]; + + protected $rule = [ + 'status' => 'required' + ]; + + protected function sceneTest(ValidateScene $scene) + { + $scene->only(['status']) + ->append('status', 'regex:status'); + + $this->setMessages([ + 'status.regex' => '状态不符合要求' + ]); + } + }; + + $data = $v->scene('test')->check([ + 'status' => 1 + ]); + $this->assertSame(1, $data['status']); + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^状态不符合要求$/'); + $v->scene('test')->check([ + 'status' => 'close' + ]); + } +} diff --git a/tests/Test/TestRuleManagerGet.php b/tests/Test/TestRuleManagerGet.php new file mode 100644 index 0000000000000000000000000000000000000000..42dbb960756aa2dc34a48bac1b7a038fcf20f41b --- /dev/null +++ b/tests/Test/TestRuleManagerGet.php @@ -0,0 +1,178 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; +use W7\Tests\Material\UserRulesManager; +use W7\Validate\RuleManager; + +class TestRuleManagerGet extends BaseTestValidate +{ + public function testGetStaticMethodForGetAll() + { + $userRules = new UserRulesManager(); + $ref = new \ReflectionClass($userRules); + $_rule = $ref->getProperty('rule'); + $_rule->setAccessible(true); + $_rule = $_rule->getValue($userRules); + + $_message = $ref->getProperty('message'); + $_message->setAccessible(true); + $_message = $_message->getValue($userRules); + + $_customAttributes = $ref->getProperty('customAttributes'); + $_customAttributes->setAccessible(true); + $_customAttributes = $_customAttributes->getValue($userRules); + + list($rule, $message, $customAttributes) = UserRulesManager::get(null, true); + + $this->assertEquals($_rule, $rule); + $this->assertEquals($_message, $message); + $this->assertEquals($_customAttributes, $customAttributes); + } + + public function testGetStaticMethodForGetOnly() + { + $fields = ['user', 'pass']; + list($rule, $message, $customAttributes) = UserRulesManager::get($fields, true); + $this->assertCount(2, $rule); + $this->assertCount(2, $customAttributes); + + $this->assertEquals('用户名必须为邮箱', $message['user.email']); + $this->assertEquals('密码长度错误', $message['pass.lengthBetween']); + } + + public function testGetMethodForScene() + { + $userRules = new UserRulesManager(); + + $this->assertCount(2, $userRules->scene('login')->getRules()); + $this->assertCount(2, $userRules::login()[0]); + } + + /** + * @test 测试清空规则管理器的场景 + */ + public function testClearScene() + { + $v = new class extends RuleManager { + protected $rule = [ + 'user' => 'required', + 'pass' => 'required', + 'name' => 'required' + ]; + + protected $scene = [ + 'login' => ['user', 'pass'] + ]; + }; + + $rules = $v->setScene()->scene('login')->getRules(); + $this->assertSame(3, \count(array_intersect(array_keys($rules), ['user', 'pass', 'name']))); + } + + /** + * @test 测试清空规则管理器的规则 + */ + public function testClearRules() + { + $v = new class extends RuleManager { + protected $rule = [ + 'user' => 'required', + 'pass' => 'required', + 'name' => 'required' + ]; + }; + + $rules = $v->setRules()->getRules(); + $this->assertEmpty($rules); + } + + /** + * @test 测试Message方法 + */ + public function testMessageMethod() + { + $v = new class extends RuleManager { + protected $rule = [ + 'user' => 'required|number', + 'pass' => 'required', + ]; + + protected $message = [ + 'code.required' => 'code不可为空', + ]; + }; + + $message = $v->setMessages() + ->setMessages([ + 'pass.required' => 'pass不可为空', + 'user.number' => '账号必须为纯数字' + ]) + ->setMessages([ + 'user.required' => '账号不可为空', + 'pass.required' => '密码不可为空' + ])->getMessages(); + + $this->assertEquals([ + 'user.required' => '账号不可为空', + 'pass.required' => '密码不可为空', + 'user.number' => '账号必须为纯数字' + ], $message); + + $this->assertEquals('账号不可为空', $v->getMessages('user.required')); + $this->assertEquals('账号不可为空', $v->getMessages('user', 'required')); + $this->assertEquals([ + 'user.required' => '账号不可为空', + 'pass.required' => '密码不可为空', + ], $v->getMessages(['user.required', 'pass.required'])); + + $this->assertEquals([ + 'user.required' => '账号不可为空', + 'user.number' => '账号必须为纯数字', + ], $v->getMessages('user', null, true)); + } + + /** + * @test 测试CustomAttributes方法 + */ + public function testCustomAttributesMethod() + { + $v = new class extends RuleManager { + protected $customAttributes = [ + 'user' => '账号' + ]; + }; + + $v->setCustomAttributes(); + + $this->assertEmpty($v->getCurrentSceneName()); + + $v->setCustomAttributes([ + 'user' => '账号' + ])->setCustomAttributes([ + 'user' => '用户名', + 'pass' => '密码' + ]); + + $this->assertEquals([ + 'user' => '用户名', + 'pass' => '密码' + ], $v->getCustomAttributes()); + + $this->assertEquals([ + 'user' => '用户名', + 'pass' => '密码' + ], $v->getCustomAttributes(['user', 'pass'])); + } +} diff --git a/tests/Test/TestRuleManagerScene.php b/tests/Test/TestRuleManagerScene.php new file mode 100644 index 0000000000000000000000000000000000000000..abbef8530158092c7e3655d1c037947eaddb751d --- /dev/null +++ b/tests/Test/TestRuleManagerScene.php @@ -0,0 +1,110 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; +use W7\Tests\Material\Rules\Length; +use W7\Tests\Material\UserRulesManager; + +class TestRuleManagerScene extends BaseTestValidate +{ + public function testGetAllRule() + { + $this->assertCount(5, (new UserRulesManager())->getRules()); + } + + public function testSceneIsLogin() + { + $userRule = new UserRulesManager(); + + $needRules = [ + 'user' => 'required|email', + 'pass' => 'required|lengthBetween:6,16' + ]; + + $this->assertEquals($needRules, $userRule->scene('login')->getRules(null, true)); + $this->assertEquals($needRules, $userRule->getRules(null, 'login')); + $this->assertEquals($needRules, $userRule->getInitialRules('login')); + } + + public function testCustomValidateScene() + { + $userRule = new UserRulesManager(); + $rules = $userRule->scene('register')->getRules(null, true); + + $this->assertCount(4, $rules); + $this->assertArrayHasKey('remark', $rules); + $this->assertFalse(in_array('alpha_dash', $rules['remark'])); + $this->assertTrue(in_array('chs', $rules['remark'])); + + $rules = $userRule->scene('registerNeedCaptcha')->getRules(); + $this->assertCount(5, $rules); + $this->assertArrayHasKey('captcha', $rules); + } + + public function testExtendsRule() + { + $userRule = new UserRulesManager(); + + $rules = $userRule->scene('captcha')->getCheckRules(); + + $this->assertArrayHasKey('captcha', $rules); + $haveRequiredRule = $haveCustomRule = $haveExtendRule = $extendRuleName = false; + foreach ($rules['captcha'] as $rule) { + switch ($rule) { + case 'required': + $haveRequiredRule = true; + break; + case $rule instanceof Length: + $haveCustomRule = true; + break; + case 32 === strlen($rule): + $haveExtendRule = true; + $extendRuleName = $rule; + break; + } + } + + $this->assertTrue($haveRequiredRule); + $this->assertTrue($haveCustomRule); + $this->assertTrue($haveExtendRule); + + $messages = $userRule->getMessages(); + + $this->assertEquals('验证码错误', $messages['captcha.' . $extendRuleName]); + } + + /** + * @test 测试当规则管理器使用了不存在的场景名,结果是否取出全部规则 + */ + public function testGetRulesUsingNonExistentSceneName() + { + $v = new class extends UserRulesManager { + protected $rule = [ + 'user' => 'required', + 'pass' => 'required', + 'code' => 'required', + 'name' => 'required' + ]; + + protected $scene = [ + 'login' => ['user', 'pass'] + ]; + }; + + $rules = $v->scene('login')->getRules(); + $this->assertSame(2, \count(array_intersect(array_keys($rules), ['user', 'pass']))); + $rules = $v->scene('nonExistentSceneName')->getRules(); + $this->assertSame(4, \count(array_intersect(array_keys($rules), ['user', 'pass', 'code', 'name']))); + } +} diff --git a/tests/Test/TestValidateCollection.php b/tests/Test/TestValidateCollection.php new file mode 100644 index 0000000000000000000000000000000000000000..905506b3584365e0ff173c3d5fc10b0ae509f87f --- /dev/null +++ b/tests/Test/TestValidateCollection.php @@ -0,0 +1,169 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; + +class TestValidateCollection extends BaseTestValidate +{ + public function testGetMultiDimensionalArrays() + { + $data = [ + 'a' => [ + 'b' => [ + 'c' => 123 + ] + ] + ]; + + $this->assertEquals(123, validate_collect($data)->get('a.b.c')); + } + + public function testGetArrayPluck() + { + $data = [ + 'I7 1700K' => [ + 'vendor' => 'Inter' + ], + 'R7 5800X' => [ + 'vendor' => 'AMD' + ], + 'I9 11900K' => [ + 'vendor' => 'Inter' + ], + 'A10-9700' => [ + 'vendor' => 'AMD' + ] + ]; + + $this->assertEquals([ + 'Inter', + 'AMD', + 'Inter', + 'AMD' + ], validate_collect($data)->get('*.vendor')); + } + + public function testShift() + { + $data = validate_collect([1, 2, 3, 4]); + + $this->assertEquals(1, $data->shift()); + $this->assertEquals(2, $data->shift()); + $this->assertEquals(3, $data->shift()); + $this->assertEquals(4, $data->shift()); + $this->assertEquals(null, $data->shift()); + } + + public function testPop() + { + $data = validate_collect([1, 2, 3, 4]); + + $this->assertEquals(4, $data->pop()); + $this->assertEquals(3, $data->pop()); + $this->assertEquals(2, $data->pop()); + $this->assertEquals(1, $data->pop()); + $this->assertEquals(null, $data->pop()); + } + + public function testPull() + { + $data = validate_collect([ + 'name' => 'yuyu', + 'age' => 2 + ]); + + $this->assertEquals('yuyu', $data->pull('name')); + $this->assertFalse($data->has('name')); + $this->assertEquals([ + 'age' => 2 + ], $data->all()); + } + + public function testHas() + { + $data = validate_collect([ + 'user' => [ + 'name' => 'yuyu', + 'age' => 1 + ] + ]); + + $this->assertTrue($data->has('user')); + $this->assertTrue($data->has('user.name')); + $this->assertTrue($data->has('user.age')); + $this->assertFalse($data->has('user.phone')); + } + + public function testSet() + { + $data = validate_collect([ + 'user' => [ + 'name' => 'yuyu', + 'age' => 1 + ] + ]); + + $this->assertFalse($data->has('user.phone')); + + $data->set('user.phone', '13122223333'); + $this->assertTrue($data->has('user.phone')); + $this->assertEquals('13122223333', $data->get('user.phone')); + + $data->set('count', 1); + $this->assertTrue($data->has('count')); + $this->assertEquals(1, $data->get('count')); + } + + public function testWhenHas() + { + $data = validate_collect([ + 'name' => 'yuyu' + ]); + $this->assertNull($data->get('have')); + $data->whenHas('name', function ($data) { + $data->set('have', true); + }); + + $this->assertTrue($data->get('have')); + } + + public function testWhenNotHas() + { + $data = validate_collect(); + + $this->assertNull($data->get('have')); + + $data->whenNotHas('name', function ($data) { + $data->set('have', false); + }); + + $this->assertFalse($data->get('have')); + } + + public function testGetArray() + { + $data = validate_collect([ + 'user' => 'test', + 'pass' => 123456 + ]); + + $this->assertSame('test', $data->user); + + $data->pass = 'root'; + $this->assertSame('root', $data->pass); + + $this->expectException(\Exception::class); + $this->assertSame('test', $data->nonExistent); + } +} diff --git a/tests/Test/TestValidateMessage.php b/tests/Test/TestValidateMessage.php new file mode 100644 index 0000000000000000000000000000000000000000..eeb89dbf7d9a594065adfb85d7bbf8e11e74c311 --- /dev/null +++ b/tests/Test/TestValidateMessage.php @@ -0,0 +1,142 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; +use W7\Validate\Exception\ValidateException; +use W7\Validate\Support\Event\ValidateEventAbstract; +use W7\Validate\Support\MessageProvider; +use W7\Validate\Support\ValidateScene; +use W7\Validate\Validate; + +class TestErrorMessageEvent extends ValidateEventAbstract +{ + public function afterValidate(): bool + { + $this->message = 'user.required'; + return false; + } +} +class TestMessage extends Validate +{ + protected $rule = [ + 'user' => 'required|email', + 'pass' => 'required|lengthBetween:6,16', + 're_pass' => 'required|eq:pass', + 'name' => 'required|chs|lengthBetween:2,4', + 'remark' => 'required|alpha_dash', + 'captcha' => 'required|length:4|checkCaptcha', + ]; + + protected $message = [ + 'user.required' => '用户名必须填写', + 'user.email' => '你输入的:{:user},不是有效的:attribute', + 'pass.required' => '密码必须填写', + 're_pass.eq' => '你输入的{@pass}与:attribute不一致' + ]; + + protected $customAttributes = [ + 'user' => '用户名', + 'pass' => '密码', + 're_pass' => '确认密码', + 'name' => '昵称', + 'remark' => '备注', + 'captcha' => '验证码', + ]; + + protected function sceneTestSceneEventClosure(ValidateScene $scene) + { + $scene->after(function () { + return 'user.required'; + }); + } + + protected function sceneTestSceneEventCallable(ValidateScene $scene) + { + $scene->before('testMessage'); + } + + protected function sceneTestSceneEvent(ValidateScene $scene) + { + $scene->event(TestErrorMessageEvent::class); + } + + protected function beforeTestMessage(): string + { + return 'pass.required'; + } +} + +class TestValidateMessage extends BaseTestValidate +{ + /** @var TestMessage */ + protected $testMessage; + + public function __construct($name = null, array $data = [], $dataName = '') + { + parent::__construct($name, $data, $dataName); + $this->testMessage = new TestMessage(); + } + + /** + * @test 测试消息处理器对错误消息的处理是否符合预期 + */ + public function testGetCustomMessage() + { + $message = (new MessageProvider())->setRuleManager($this->testMessage); + $this->assertEquals('用户名必须填写', $message->getInitialMessage('user', 'required')); + $this->assertEquals('你输入的:{:user},不是有效的:attribute', $message->getInitialMessage('user', 'email')); + + $this->assertEquals('你输入的:123456,不是有效的:attribute', $message->setData([ + 'user' => '123456' + ])->getMessage('user', 'email')); + + $this->assertEquals('你输入的密码与:attribute不一致', $message->getMessage('re_pass', 'eq')); + } + + /** + * @test 测试在验证场景中的事件方法中(闭包方式)返回错误 + * + * @throws ValidateException + */ + public function testMessageInSceneEventClosure() + { + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^用户名必须填写$/'); + $this->testMessage->scene('testSceneEventClosure')->check([]); + } + + /** + * @test 测试在验证场景中的事件方法中(Callable方式)返回错误 + * + * @throws ValidateException + */ + public function testMessageInSceneEventCallable() + { + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^密码必须填写$/'); + $this->testMessage->scene('testSceneEventCallable')->check([]); + } + + /** + * @test 测试在验证场景中的事件返回错误 + * + * @throws ValidateException + */ + public function testMessageInSceneEvent() + { + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^用户名必须填写$/'); + $this->testMessage->scene('testSceneEvent')->check([]); + } +} diff --git a/tests/Test/TestValidateScene.php b/tests/Test/TestValidateScene.php new file mode 100644 index 0000000000000000000000000000000000000000..ffb5571ddf41c6fd26a9d4e2542272380b4e7f84 --- /dev/null +++ b/tests/Test/TestValidateScene.php @@ -0,0 +1,383 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\ArticleValidate; +use W7\Tests\Material\BaseTestValidate; +use W7\Tests\Material\Count; +use W7\Validate\Exception\ValidateException; +use W7\Validate\Exception\ValidateRuntimeException; +use W7\Validate\Support\ValidateScene; +use W7\Validate\Validate; +use function PHPUnit\Framework\assertEquals; + +class TestSomeTimes extends Validate +{ + protected $message = [ + 'a.required' => 'a不能为空', + 'b.required' => 'b不能为空', + ]; + protected function sceneTest(ValidateScene $scene) + { + $scene->appendCheckField('a') + ->appendCheckField('aIsRequired') + ->append('aIsRequired', 'required') + ->sometimes('a', 'required', function ($data) { + return 1 == $data['aIsRequired']; + }); + } + + protected function sceneTestSomeTimesMultiField(ValidateScene $scene) + { + $scene->only(['a', 'b']) + ->sometimes(['a', 'b'], 'required', function () { + return true; + }); + } +} +class TestValidateScene extends BaseTestValidate +{ + protected $userInput; + + public function __construct($name = null, array $data = [], $dataName = '') + { + parent::__construct($name, $data, $dataName); + $this->userInput = [ + 'content' => '内容', + 'title' => '这是一个标题' + ]; + } + + /** + * @test 测试没有指定验证场景的情况 + * @throws ValidateException + */ + public function testNotScene() + { + $v = new ArticleValidate(); + $this->expectException(ValidateException::class); + $v->check($this->userInput); + } + + /** + * @test 测试制定验证场景 + * @throws ValidateException + */ + public function testScene() + { + $v = new ArticleValidate(); + $data = $v->scene('add')->check($this->userInput); + $this->assertEquals('内容', $data['content']); + } + + /** + * @test 测试自定义验证场景 edit + * @throws ValidateException + */ + public function testCustomScene() + { + $validate = new ArticleValidate(); + $this->expectException(ValidateException::class); + $validate->scene('edit')->check($this->userInput); + } + + /** + * @test 测试Use自定义验证场景 save + * @throws ValidateException + */ + public function testUseScene() + { + $validate = new ArticleValidate(); + $this->expectExceptionMessageMatches('/^缺少参数:文章Id$/'); + $validate->scene('save')->check($this->userInput); + } + + /** + * @test 测试移除规则 + * @throws ValidateException + */ + public function testDynamicScene() + { + $validate = new ArticleValidate(); + $data = $validate->scene('dynamic')->check([ + 'title' => '标题标题标题标题', + 'content' => '1' + ]); + $this->assertEquals('1', $data['content']); + } + + /** + * @test 测试当指定的验证场景不存在时,是否验证全部的规则 + */ + public function testNotFountSceneName() + { + $v = new class extends Validate { + protected $rule = [ + 'user' => 'required', + 'pass' => 'required' + ]; + }; + + try { + $v->scene('notFount')->check([]); + } catch (ValidateException $e) { + $this->assertSame('user', $e->getAttribute()); + } + try { + $v->scene('notFount')->check([ + 'user' => 123 + ]); + } catch (ValidateException $e) { + $this->assertSame('pass', $e->getAttribute()); + } + } + + /** + * @test 测试sometimes规则 + * + * @throws ValidateException + */ + public function testSometimesRule() + { + $v = TestSomeTimes::make(); + $data = $v->scene('test')->check([ + 'aIsRequired' => 0 + ]); + + $this->assertSame(0, $data['aIsRequired']); + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^a不能为空$/'); + $v->scene('test')->check([ + 'aIsRequired' => 1 + ]); + } + + /** + * @test 测试sometimes为多个字段附加规则 + */ + public function testSometimesMultiField() + { + $v = TestSomeTimes::make(); + try { + $v->scene('testSomeTimesMultiField')->check([]); + } catch (ValidateException $e) { + $this->assertSame('a', $e->getAttribute()); + } + + try { + $v->scene('testSomeTimesMultiField')->check([ + 'a' => 123 + ]); + } catch (ValidateException $e) { + $this->assertSame('b', $e->getAttribute()); + } + } + + /** + * @test 测试在场景中增加闭包规则 + * @throws ValidateException + */ + public function testAppendClosureRule() + { + $v = new class extends Validate { + protected function sceneTest(ValidateScene $scene) + { + $scene->appendCheckField('a') + ->append('a', function ($attribute, $value, $fail) { + Count::incremental('testAppendClosureRule'); + if (!is_numeric($value)) { + $fail('a必须为数字'); + } + }); + } + }; + + $data = $v->scene('test')->check([ + 'a' => 123 + ]); + + $this->assertSame(123, $data['a']); + Count::assertEquals(1, 'testAppendClosureRule'); + + $this->expectException(ValidateException::class); + $this->expectExceptionMessageMatches('/^a必须为数字$/'); + $v->scene('test')->check([ + 'a' => 'aaa' + ]); + } + + /** + * @test 测试在场景中获取验证的值 + * @throws ValidateException + */ + public function testGetDataForScene() + { + $v = new class extends Validate { + protected $rule = [ + 'name' => 'required' + ]; + + protected function sceneGetData(ValidateScene $scene) + { + $scene->only(['name']); + + assertEquals('名字', $scene->getData('name')); + } + + protected function sceneGetNonExistentData(ValidateScene $scene) + { + $scene->only(['name']); + $test = $scene->test; + } + }; + + $v->scene('getData')->check([ + 'name' => '名字' + ]); + + $this->expectException(ValidateRuntimeException::class); + $v->scene('getNonExistentData')->check([ + 'name' => '名字' + ]); + } + + /** + * @test 测试在场景中动态移除验证规则 + */ + public function testRemoveRuleForScene() + { + $v = new class extends Validate { + protected $rule = [ + 'id' => 'required|max:100|min:1' + ]; + + protected function sceneRemoveRule(ValidateScene $scene) + { + $scene->only(['id']) + ->remove('id', 'required'); + } + + protected function sceneRemoveRuleForArray(ValidateScene $scene) + { + $scene->only(['id']) + ->remove('id', ['max', 'min']); + } + + protected function sceneRemoveRuleForHasParams(ValidateScene $scene) + { + $scene->only(['id']) + ->remove('id', 'max:100'); + } + }; + + $rules = $v->scene('removeRule')->getRules(); + $this->assertEquals(['max:100', 'min:1'], array_values($rules['id'])); + + $rules = $v->scene('removeRuleForArray')->getRules(); + $this->assertEquals(['required'], array_values($rules['id'])); + + $rules = $v->scene('removeRuleForHasParams')->getRules(); + $this->assertEquals(['required', 'min:1'], array_values($rules['id'])); + } + + /** + * @test 测试在自定义场景中动态移除字段 + */ + public function testRemoveCheckField() + { + $v = new class extends Validate { + protected $rule = [ + 'user' => 'required', + 'pass' => 'required' + ]; + + protected function sceneTest(ValidateScene $scene) + { + $scene->only(['user', 'pass']) + ->removeCheckField('pass'); + } + }; + $rules = $v->getRules(); + $this->assertEquals(['user', 'pass'], array_keys($rules)); + + $rules = $v->scene('test')->getRules(); + $this->assertEquals(['user'], array_keys($rules)); + } + + /** + * @test 测试当场景指定的字段没有定义规则 + * @throws ValidateException + */ + public function testSpecifyFieldUndefinedRule() + { + $v = new class extends Validate { + protected $scene = [ + 'login' => ['user', 'pass'] + ]; + }; + + $data = $v->scene('login')->check([ + 'user' => 1, + 'pass' => 2 + ]); + + $this->assertEquals(['user', 'pass'], array_keys($data)); + } + + /** + * 测试当自定义场景中指定的字段没有定义规则 + * @throws ValidateException + */ + public function testSpecifyFieldUndefinedRuleForCustomScene() + { + $v = new class extends Validate { + protected function sceneLogin(ValidateScene $scene) + { + $scene->only(['user', 'pass']); + } + }; + + $data = $v->scene('login')->check([ + 'user' => 1, + 'pass' => 2 + ]); + + $this->assertEquals(['user', 'pass'], array_keys($data)); + } + + /** + * @test 测试当验证规则为空时最后结果是否为空 + * @throws ValidateException + */ + public function testValidateRuleIsEmpty() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => 'required', + 'b' => 'required' + ]; + + protected $scene = [ + 'empty' => [] + ]; + }; + + $data = $v->scene('empty')->check([ + 'a' => 1, + 'c' => 2 + ]); + + $this->assertEmpty($data); + } +} diff --git a/tests/Test/TestValidateSceneNext.php b/tests/Test/TestValidateSceneNext.php new file mode 100644 index 0000000000000000000000000000000000000000..96989d7b9eb9a74bdd17a14f5f7865986ec660b3 --- /dev/null +++ b/tests/Test/TestValidateSceneNext.php @@ -0,0 +1,162 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use W7\Tests\Material\BaseTestValidate; +use W7\Tests\Material\Count; +use W7\Validate\Exception\ValidateException; +use W7\Validate\Exception\ValidateRuntimeException; +use W7\Validate\Validate; + +class TestValidateSceneNext extends BaseTestValidate +{ + /** + * @test 测试Next以及场景选择器的连续使用 + * @throws ValidateException + */ + public function testNext() + { + $testValidate = new class extends Validate { + protected $rule = [ + 'a' => 'required', + 'b' => 'required', + 'c' => 'required', + 'd' => 'required', + 'e' => 'required', + + 'f' => 'required', + 'g' => 'required', + + 'h' => 'required', + 'i' => 'required', + + 'not' => ' required' + ]; + + protected $scene = [ + 'testA' => ['a', 'b', 'c', 'next' => 'testB'], + 'testB' => ['c', 'd', 'next' => 'testC'], + 'testC' => ['e', 'next' => 'checkCode'], + 'testD' => ['f', 'g', 'next' => 'testE'], + 'testE' => ['h', 'i'], + ]; + + protected function checkCodeSelector(): string + { + return 'testD'; + } + }; + + $data = $testValidate::make()->scene('testA')->check([ + 'a' => 1, + 'b' => '666', + 'c' => '456', + 'd' => '585', + 'e' => 1, + + 'f' => 2, + 'g' => 2, + + 'h' => 12, + 'i' => 23 + ]); + + $this->assertCount(9, $data); + } + + /** + * @test 测试多个场景指定了同一个字段,是否在一个验证链中,只验证一次 + * @throws ValidateException + */ + public function testNextValidationCountIsOnce() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => 'required|tests' + ]; + + protected $scene = [ + 'testA' => ['a', 'next' => 'testB'], + 'testB' => ['a', 'next' => 'testC'], + 'testC' => ['a'] + ]; + + protected function ruleTests() + { + Count::incremental('ruleTest'); + return true; + } + }; + + $data = $v->scene('testA')->check(['a' => 1]); + $this->assertEquals(1, $data['a']); + Count::assertEquals(1, 'ruleTest'); + + Count::reset('ruleTest'); + $data = $v->scene('testB')->check(['a' => 1]); + $this->assertEquals(1, $data['a']); + Count::assertEquals(1, 'ruleTest'); + + Count::reset('ruleTest'); + $data = $v->scene('testC')->check(['a' => 1]); + $this->assertEquals(1, $data['a']); + Count::assertEquals(1, 'ruleTest'); + } + + /** + * @test 测试在场景中,当next指定的场景和当前的场景名一致时,是否会导致死循环 + * + * @throws ValidateException + */ + public function testNextSceneNameEqCurrentSceneName() + { + $v = new class extends Validate { + protected $scene = [ + 'test' => ['next' => 'test'] + ]; + }; + + $this->expectException(ValidateRuntimeException::class); + $this->expectExceptionMessageMatches('/^The scene used cannot be the same as the current scene.$/'); + $v->scene('test')->check([]); + } + + /** + * @test 测试场景选择器中直接返回字段数组 + * + * @throws ValidateException + */ + public function testSceneSelectorFields() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => 'required' + ]; + + protected $scene = [ + 'test' => ['next' => 'getFields'] + ]; + + protected function getFieldsSelector(array $data): array + { + return ['a']; + } + }; + + $data = $v->scene('test')->check([ + 'a' => 123 + ]); + + $this->assertSame(123, $data['a']); + } +} diff --git a/tests/Test/TestValidateSceneNextAndEvent.php b/tests/Test/TestValidateSceneNextAndEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..ebe18712cbf18df11838e8eefacc5f1d313ce715 --- /dev/null +++ b/tests/Test/TestValidateSceneNextAndEvent.php @@ -0,0 +1,238 @@ + + * + * This is not a free software + * Using it under the license terms + * visited https://www.w7.cc for more details + */ + +namespace W7\Tests\Test; + +use PHPUnit\Framework\Assert; +use W7\Tests\Material\BaseTestValidate; +use W7\Tests\Material\Count; +use W7\Validate\Exception\ValidateException; +use W7\Validate\Support\Event\ValidateEventAbstract; +use W7\Validate\Support\ValidateScene; +use W7\Validate\Validate; + +class TestEvent extends ValidateEventAbstract +{ + public function afterValidate(): bool + { + Count::incremental('globalEventCount'); + return true; + } + + public function beforeValidate(): bool + { + Count::incremental('globalEventCount'); + return true; + } +} + +class EventInScene extends ValidateEventAbstract +{ + public function afterValidate(): bool + { + Count::incremental('eventInSceneCount'); + return true; + } + + public function beforeValidate(): bool + { + Count::incremental('eventInSceneCount'); + return true; + } +} + +class StandaloneEvent extends ValidateEventAbstract +{ + public function afterValidate(): bool + { + Count::incremental('standaloneEventCount'); + return true; + } + + public function beforeValidate(): bool + { + Count::incremental('standaloneEventCount'); + return true; + } +} +class TestValidateSceneNextAndEvent extends BaseTestValidate +{ + public function testAssociatedSceneEvents() + { + $v = new class extends Validate { + public $afters = []; + + public $befores = []; + + protected $rule = [ + 'a' => 'required', + 'b' => 'required', + 'c' => 'required', + ]; + + protected $event = [ + TestEvent::class + ]; + + protected $scene = [ + 'testA' => ['before' => 'aEvent', 'a', 'next' => 'testB', 'after' => 'aEvent'], + 'testB' => ['before' => 'bEvent', 'b', 'next' => 'testC', 'after' => 'bEvent', 'event' => EventInScene::class], + 'testC' => ['c', 'event' => EventInScene::class], + + 'standalone' => ['a', 'before' => 'standalone', 'after' => 'standalone', 'event' => StandaloneEvent::class] + ]; + + protected $filter = [ + 'a' => 'intval' + ]; + + protected function afterStandalone(array $data) + { + Assert::assertEquals('string', gettype($data['a'])); + $this->afters['standalone'] = ($this->afters['standalone'] ?? 0) + 1; + return true; + } + + protected function beforeStandalone() + { + $this->befores['standalone'] = ($this->befores['standalone'] ?? 0) + 1; + return true; + } + + protected function afterAEvent($data) + { + Assert::assertEquals('string', gettype($data['a'])); + $this->afters['a'] = ($this->afters['a'] ?? 0) + 1; + return true; + } + + protected function beforeAEvent($data) + { + $this->befores['a'] = ($this->befores['a'] ?? 0) + 1; + return true; + } + + protected function afterBEvent($data) + { + $this->afters['b'] = ($this->afters['b'] ?? 0) + 1; + return true; + } + + protected function beforeBEvent($data) + { + Assert::assertEquals('string', gettype($data['a'])); + $this->befores['b'] = ($this->befores['b'] ?? 0) + 1; + return true; + } + }; + + $this->assertEquals(0, Count::globalEventCount()); + $this->assertEquals(0, Count::eventInSceneCount()); + $data = $v->scene('testA')->check([ + 'a' => '1', + 'b' => 2, + 'c' => 3 + ]); + $this->assertEquals(2, Count::globalEventCount()); + $this->assertEquals(4, Count::eventInSceneCount()); + + $this->assertArrayHasKey('a', $v->afters); + $this->assertArrayHasKey('a', $v->befores); + + $this->assertArrayHasKey('b', $v->afters); + $this->assertArrayHasKey('b', $v->befores); + + $this->assertEquals(1, $v->afters['a']); + $this->assertEquals(1, $v->afters['b']); + + $this->assertEquals(1, $v->befores['a']); + $this->assertEquals(1, $v->befores['b']); + + $this->assertEquals('integer', gettype($data['a'])); + $this->assertTrue(empty(array_diff_key($data, array_flip(['a', 'b', 'c'])))); + + $this->assertEquals(0, Count::standaloneEventCount()); + $data = $v->scene('standalone')->check([ + 'a' => '1' + ]); + $this->assertEquals(2, Count::standaloneEventCount()); + + $this->assertArrayHasKey('standalone', $v->afters); + $this->assertArrayHasKey('standalone', $v->befores); + + $this->assertEquals(1, $v->afters['standalone']); + $this->assertEquals(1, $v->befores['standalone']); + + $this->assertEquals(1, $data['a']); + } + + /** + * @test 测试自定义场景中指定Next,检查默认值,过滤器以及场景验证等功能是否正常 + */ + public function testSceneNextForCustomScenes() + { + $v = new class extends Validate { + protected $rule = [ + 'a' => 'required', + 'b' => '', + 'c' => 'required' + ]; + + protected $default = [ + 'a' => 1 + ]; + + protected $filter = [ + 'c' => 'intval' + ]; + + protected $scene = [ + 'testA' => ['a', 'next' => 'testB'], + 'testC' => ['c'] + ]; + + protected function sceneTestB(ValidateScene $scene) + { + $scene->only(['b']) + ->append('b', 'required') + ->filter('b', 'intval') + ->next('testC'); + } + }; + + try { + $v->scene('testA')->check([ + + ]); + } catch (ValidateException $e) { + $this->assertSame('b', $e->getAttribute()); + } + + try { + $v->scene('testA')->check([ + 'b' => '123' + ]); + } catch (ValidateException $e) { + $this->assertSame('c', $e->getAttribute()); + } + + $data = $v->scene('testA')->check([ + 'b' => '123', + 'c' => '256' + ]); + + $this->assertCount(3, $data); + foreach ($data as $value) { + $this->assertEquals('integer', gettype($value)); + } + } +}