├── .github ├── changelog.yml ├── dependabot.yml └── workflows │ ├── php.yml │ └── release.yml ├── .gitignore ├── .nojekyll ├── .php_cs ├── LICENSE ├── README.en.md ├── README.md ├── _navbar.md ├── composer.json ├── docs ├── api.md ├── help.md └── idea.md ├── index.html ├── phpunit.xml ├── src ├── AbstractValidation.php ├── Exception │ ├── ArrayValueNotExists.php │ └── ValidateException.php ├── FV.php ├── FieldValidation.php ├── Filter │ ├── FilteringTrait.php │ ├── Filters.php │ ├── Filtration.php │ └── UserFilters.php ├── GlobalOption.php ├── Helper.php ├── Locale │ └── LocaleZhCN.php ├── RV.php ├── RuleValidation.php ├── Simple │ ├── VData.php │ ├── ValidData.php │ └── ValidValue.php ├── Traits │ ├── ErrorMessageTrait.php │ ├── MultipleRulesTrait.php │ ├── NameAliasTrait.php │ └── ScopedValidatorsTrait.php ├── Validation.php ├── ValidationInterface.php ├── ValidationTrait.php ├── Validator │ ├── AbstractValidator.php │ ├── GlobalMessage.php │ ├── UserValidators.php │ └── ValidatorInterface.php └── Validators.php └── test ├── BaseValidateTestCase.php ├── Example ├── DataModel.php ├── FieldExample.php ├── PageRequest.php └── RuleExample.php ├── FieldValidationTest.php ├── Filter ├── FiltersTest.php ├── FiltrationTest.php └── UserFiltersTest.php ├── FiltrationTest.php ├── HelperTest.php ├── IssuesTest.php ├── RuleValidationTest.php ├── Simple └── ValidDataTest.php ├── Traits ├── ErrorMessageTraitTest.php └── ScopedValidatorsTest.php ├── ValidationTraitTest.php ├── Validator ├── AdemoValidator.php ├── GlobalMessageTest.php ├── Rule.php └── UserValidatorsTest.php ├── ValidatorsTest.php ├── avatar.jpeg └── bootstrap.php /.github/changelog.yml: -------------------------------------------------------------------------------- 1 | title: '## Change Log' 2 | # style allow: simple, markdown(mkdown), ghr(gh-release) 3 | style: gh-release 4 | # group names 5 | names: [Refactor, Fixed, Feature, Update, Other] 6 | #repo_url: https://github.com/gookit/gcli 7 | 8 | filters: 9 | # message length should >= 12 10 | - name: msg_len 11 | min_len: 12 12 | # message words should >= 3 13 | - name: words_len 14 | min_len: 3 15 | - name: keyword 16 | keyword: format code 17 | exclude: true 18 | - name: keywords 19 | keywords: format code, action test 20 | exclude: true 21 | 22 | # group match rules 23 | # not matched will use 'Other' group. 24 | rules: 25 | - name: Refactor 26 | start_withs: [refactor, break] 27 | contains: ['refactor:'] 28 | - name: Fixed 29 | start_withs: [fix] 30 | contains: ['fix:'] 31 | - name: Feature 32 | start_withs: [feat, new] 33 | contains: [feature] 34 | - name: Update 35 | start_withs: [update, 'up:'] 36 | contains: [] 37 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | # Check for updates to GitHub Actions every weekday 13 | interval: "daily" 14 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Unit-tests 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**.php' 7 | - 'composer.json' 8 | - '**.yml' 9 | # Trigger the workflow on any pull request 10 | pull_request: 11 | 12 | jobs: 13 | test: 14 | name: Test on php ${{ matrix.php}} 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 10 17 | strategy: 18 | fail-fast: true 19 | matrix: 20 | php: [ 8.1, 8.2, 8.3, 8.4] # 7.1, 7.2, 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | # usage refer https://github.com/shivammathur/setup-php 27 | - name: Setup PHP 28 | timeout-minutes: 5 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: ${{ matrix.php}} 32 | tools: pecl, php-cs-fixer, phpunit:${{ matrix.phpunit }} 33 | extensions: mbstring, dom, fileinfo, mysql, openssl, igbinary, redis # , swoole-4.4.19 #optional, setup extensions 34 | ini-values: post_max_size=56M, short_open_tag=On #optional, setup php.ini configuration 35 | coverage: none #optional, setup coverage driver: xdebug, none 36 | 37 | - name: Display Env 38 | run: env 39 | 40 | - name: Install dependencies 41 | run: composer install --no-progress 42 | 43 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 44 | # Docs: https://getcomposer.org/doc/articles/scripts.md 45 | 46 | - name: Run unit tests 47 | # run: composer run test 48 | run: phpunit 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Tag-release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | name: New tag release 11 | runs-on: ubuntu-latest 12 | timeout-minutes: 10 13 | strategy: 14 | fail-fast: true 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Set ENV for github-release 23 | # https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable 24 | run: | 25 | echo "RELEASE_TAG=${GITHUB_REF:10}" >> $GITHUB_ENV 26 | echo "RELEASE_NAME=$GITHUB_WORKFLOW" >> $GITHUB_ENV 27 | 28 | - name: Generate changelog 29 | run: | 30 | curl https://github.com/gookit/gitw/releases/latest/download/chlog-linux-amd64 -L -o /usr/local/bin/chlog 31 | chmod a+x /usr/local/bin/chlog 32 | chlog -c .github/changelog.yml -o changelog.md prev last 33 | 34 | # https://github.com/softprops/action-gh-release 35 | - name: Create release and upload assets 36 | uses: softprops/action-gh-release@v2 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | with: 40 | name: ${{ env.RELEASE_TAG }} 41 | tag_name: ${{ env.RELEASE_TAG }} 42 | body_path: changelog.md 43 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### OSX template 3 | *.DS_Store 4 | 5 | .idea/ 6 | .phpintel/ 7 | /vendor/ 8 | !README.md 9 | !.gitkeep 10 | composer.lock 11 | *.swp 12 | *.swo 13 | .DS_Store 14 | .phpunit.result.cache 15 | *.cache -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-validate/7db74fec9cf4fa041544e8569ee874e8529e4ad1/.nojekyll -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true)->setRules([ 9 | '@PSR2' => true, 10 | // 'header_comment' => [ 11 | // 'comment_type' => 'PHPDoc', 12 | // 'header' => $header, 13 | // 'separate' => 'none' 14 | // ], 15 | 'array_syntax' => [ 16 | 'syntax' => 'short' 17 | ], 18 | 'single_quote' => true, 19 | 'class_attributes_separation' => true, 20 | 'no_unused_imports' => true, 21 | 'standardize_not_equals' => true, 22 | 'declare_strict_types' => true, 23 | ])->setFinder(PhpCsFixer\Finder::create() 24 | // ->exclude('test') 25 | ->exclude('docs')->exclude('vendor')->in(__DIR__))->setUsingCache(false); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 inhere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | # PHP Validate 2 | 3 | [![License](https://img.shields.io/packagist/l/inhere/php-validate.svg?style=flat-square)](LICENSE) 4 | [![Php Version](https://img.shields.io/packagist/php-v/inhere/php-validate?maxAge=2592000)](https://packagist.org/packages/inhere/php-validate) 5 | [![Latest Stable Version](http://img.shields.io/packagist/v/inhere/php-validate.svg)](https://packagist.org/packages/inhere/php-validate) 6 | [![Coverage Status](https://coveralls.io/repos/github/inhere/php-validate/badge.svg?branch=master)](https://coveralls.io/github/inhere/php-validate?branch=master) 7 | [![Github Actions Status](https://github.com/inhere/php-validate/workflows/Unit-tests/badge.svg)](https://github.com/inhere/php-validate/actions) 8 | [![zh-CN readme](https://img.shields.io/badge/中文-Readme-brightgreen.svg?style=for-the-badge&maxAge=2592000)](README.md) 9 | 10 | Lightweight and feature-rich PHP validation and filtering library. 11 | 12 | - Simple and convenient, support to add custom validator 13 | - Support pre-verification check, customize how to judge non-empty 14 | - Support grouping rules by scene. Or partial verification 15 | - Supports the use of filters to purify and filter values before verification [built-in filter](#built-in-filters) 16 | - Support pre-processing and post-processing of verification [independent verification processing](#on-in-Validation) 17 | - Support to customize the error message, field translation, message translation of each verification, and support the default value 18 | - Supports basic array checking, checking of children (`'goods.apple'`) values ​​of arrays, checking of children of wildcards (`'users.*.id' 'goods.*'`) 19 | - Easy access to error information and secure data after verification (only data that has been checked regularly) 20 | - More than 60 commonly used validators have been built-in [built-in validator](#built-in-validators) 21 | - Rule setting reference `yii`, `laravel`, `Respect/Validation` 22 | - Independent filter `Inhere\Validate\Filter\Filtration`, can be used for data filtering alone 23 | 24 | ## License 25 | 26 | [MIT](LICENSE) 27 | -------------------------------------------------------------------------------- /_navbar.md: -------------------------------------------------------------------------------- 1 | 2 | 我的其他项目 3 | 4 | * [EasyTpl](https://phppkg.github.io/easytpl/ "template engine") 5 | * [pflag](https://php-toolkit.github.io/pflag/ "console option and argument parse") 6 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inhere/php-validate", 3 | "type": "library", 4 | "description": "generic data validate, filter library of the php", 5 | "keywords": [ 6 | "php-library", 7 | "library", 8 | "validate", 9 | "form-validate", 10 | "validation" 11 | ], 12 | "homepage": "https://github.com/inhere/php-validate", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "inhere", 17 | "email": "in.798@qq.com", 18 | "homepage": "https://github.com/inhere" 19 | } 20 | ], 21 | "require": { 22 | "php": ">8.1.0", 23 | "toolkit/stdlib": "~2.0" 24 | }, 25 | "require-dev": {}, 26 | "autoload": { 27 | "psr-4": { 28 | "Inhere\\Validate\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "Inhere\\ValidateTest\\": "test/" 34 | } 35 | }, 36 | "scripts": { 37 | "check-cs": "./php-cs-fixer fix --dry-run --diff --diff-format=udiff", 38 | "cs-fix": "./php-cs-fixer fix", 39 | "test": "vendor/bin/phpunit" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | # methods API 2 | -------------------------------------------------------------------------------- /docs/help.md: -------------------------------------------------------------------------------- 1 | # help 2 | 3 | 4 | ```php 5 | 6 | /** 7 | * @link http://php.net/manual/zh/function.filter-input.php 8 | * @param int $type INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV 9 | * @param $varName 10 | * @param array $filter 过滤/验证器 {@link http://php.net/manual/zh/filter.filters.php} 11 | * @param array $options 一个选项的关联数组,或者按位区分的标示。 12 | * 如果过滤器接受选项,可以通过数组的 "flags" 位去提供这些标示。 13 | * 如果成功的话返回所请求的变量。 14 | * 如果成功的话返回所请求的变量。 15 | * 如果过滤失败则返回 FALSE , 16 | * 如果 varName 不存在的话则返回 NULL 。 17 | * 如果标示 FILTER_NULL_ON_FAILURE 被使用了,那么当变量不存在时返回 FALSE ,当过滤失败时返回 NULL 。 18 | */ 19 | public static function input($type, $varName, $filter, array $options = []) 20 | { 21 | } 22 | 23 | public static function multi(array $data, array $filters = []) 24 | { 25 | } 26 | 27 | /** 28 | * @link http://php.net/manual/zh/function.filter-input-array.php 29 | * 检查(验证/过滤)输入数据中的多个变量名 like filter_input_array() 30 | * 当需要获取很多变量却不想重复调用 filter_input()时很有用。 31 | * @param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV. 要检查的输入数据 32 | * @param mixed $definition 一个定义参数的数组。 33 | * 一个有效的键必须是一个包含变量名的string, 34 | * 一个有效的值要么是一个filter type,或者是一个array 指明了过滤器、标示和选项。 35 | * 如果值是一个数组,那么它的有效的键可以是 : 36 | * filter, 用于指明 filter type, 37 | * flags 用于指明任何想要用于过滤器的标示, 38 | * options 用于指明任何想要用于过滤器的选项。 39 | * 参考下面的例子来更好的理解这段说明。 40 | * @param bool $addEmpty 在返回值中添加 NULL 作为不存在的键。 41 | * 如果成功的话返回一个所请求的变量的数组, 42 | * 如果失败的话返回 FALSE 。 43 | * 对于数组的值, 44 | * 如果过滤失败则返回 FALSE , 45 | * 如果 variable_name 不存在的话则返回 NULL 。 46 | * 如果标示 FILTER_NULL_ON_FAILURE 被使用了,那么当变量不存在时返回 FALSE ,当过滤失败时返回 NULL 。 47 | */ 48 | public static function inputMulti($type, $definition, $addEmpty = true) 49 | { 50 | } 51 | 52 | /** 53 | * 检查变量名是否存在 54 | * @param int $type One of INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER, or INPUT_ENV. 要检查的输入数据 55 | * @param string $varName Name of a variable to check. 要检查的变量名 56 | */ 57 | public static function inputHasVar($type, $varName) 58 | { 59 | } 60 | ``` -------------------------------------------------------------------------------- /docs/idea.md: -------------------------------------------------------------------------------- 1 | # Idea 2 | 3 | ## ruleEngine 4 | 5 | - string rules. like Laravel 6 | - array rules. like Yii 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Easy Template 9 | 10 | 11 |
12 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | test 6 | 7 | 8 | 9 | 10 | src 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/AbstractValidation.php: -------------------------------------------------------------------------------- 1 | data = $data; 48 | $this->atScene($scene)->setRules($rules)->setTranslates($translates); 49 | 50 | if ($startValidate) { 51 | $this->validate(); 52 | } 53 | } 54 | 55 | /** 56 | * @param array $data 57 | * @param string $scene 58 | * @param bool $startValidate 59 | * 60 | * @return self 61 | */ 62 | public static function quick(array $data, string $scene = '', bool $startValidate = false): self 63 | { 64 | return new static($data, [], [], $scene, $startValidate); 65 | } 66 | 67 | /** 68 | * @param array $data 69 | * @param array $rules 70 | * @param array $translates 71 | * @param string $scene 72 | * @param bool $startValidate 立即开始验证 73 | * 74 | * @return static 75 | * @throws InvalidArgumentException 76 | */ 77 | public static function make( 78 | array $data, 79 | array $rules = [], 80 | array $translates = [], 81 | string $scene = '', 82 | bool $startValidate = false 83 | ): self { 84 | return new static($data, $rules, $translates, $scene, $startValidate); 85 | } 86 | 87 | /** 88 | * Create and start verification immediately 89 | * 90 | * @param array $data 91 | * @param array $rules 92 | * @param array $translates 93 | * @param string $scene 94 | * 95 | * @return static 96 | * @throws InvalidArgumentException 97 | */ 98 | public static function makeAndValidate(array $data, array $rules = [], array $translates = [], string $scene = ''): self 99 | { 100 | return new static($data, $rules, $translates, $scene, true); 101 | } 102 | 103 | /** 104 | * Create and start verification immediately. alias of makeAndValidate() 105 | * 106 | * @param array $data 107 | * @param array $rules 108 | * @param array $translates 109 | * @param string $scene 110 | * 111 | * @return static 112 | * @throws InvalidArgumentException 113 | */ 114 | public static function check(array $data, array $rules = [], array $translates = [], string $scene = ''): self 115 | { 116 | return new static($data, $rules, $translates, $scene, true); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/Exception/ArrayValueNotExists.php: -------------------------------------------------------------------------------- 1 | field = $field; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/FV.php: -------------------------------------------------------------------------------- 1 | validate(); 20 | * ``` 21 | * 22 | * @package Inhere\Validate 23 | */ 24 | class FieldValidation extends AbstractValidation 25 | { 26 | use MultipleRulesTrait; 27 | 28 | /* examples: 29 | public function rules() 30 | { 31 | return [ 32 | ['field', 'required|string:5,10|...', ...], 33 | ['field0', ['required', 'string:5,10'], ...], 34 | ['field1', 'rule1|rule2|...', ...], 35 | ['field2', 'rule1|rule3|...', ...], 36 | ['field3', function($val) {}, ...], 37 | ]; 38 | } 39 | */ 40 | } 41 | -------------------------------------------------------------------------------- /src/Filter/FilteringTrait.php: -------------------------------------------------------------------------------- 1 | ['arg1', 'arg2'], 43 | * function($val) { 44 | * return str_replace(' ', '', $val); 45 | * }, 46 | * ] 47 | * 48 | * @param mixed $value 49 | * @param array|string|callable $filters 50 | * 51 | * @return mixed 52 | */ 53 | protected function valueFiltering(mixed $value, array|string|callable $filters): mixed 54 | { 55 | if (is_string($filters)) { 56 | $filters = Filters::explode($filters, '|'); 57 | } elseif (!is_array($filters)) { 58 | $filters = [$filters]; 59 | } 60 | 61 | foreach ($filters as $key => $filter) { 62 | // key is a filter. ['myFilter' => ['arg1', 'arg2']] 63 | if (is_string($key)) { 64 | $args = (array)$filter; 65 | $value = $this->callStringCallback($key, $value, ...$args); 66 | 67 | // closure 68 | } elseif (is_object($filter) && method_exists($filter, '__invoke')) { 69 | $value = $filter($value); 70 | // string, trim, .... 71 | } elseif (is_string($filter)) { 72 | $value = $this->callStringCallback($filter, $value); 73 | 74 | // e.g ['Class', 'method'], 75 | } else { 76 | $value = Helper::call($filter, $value); 77 | } 78 | } 79 | 80 | return $value; 81 | } 82 | 83 | /** 84 | * @param mixed $filter 85 | * @param array ...$args 86 | * 87 | * @return mixed 88 | * @throws InvalidArgumentException 89 | */ 90 | protected function callStringCallback(string $filter, ...$args): mixed 91 | { 92 | // if is alias name 93 | $filterName = Filters::realName($filter); 94 | 95 | // if $filter is a custom by addFiler() 96 | if ($callback = $this->getFilter($filter)) { 97 | $value = $callback(...$args); 98 | // if $filter is a custom method of the subclass. 99 | } elseif (method_exists($this, $filter . 'Filter')) { 100 | $filter .= 'Filter'; 101 | $value = $this->$filter(...$args); 102 | 103 | // if $filter is a custom add callback in the property {@see $_filters}. 104 | } elseif ($callback = UserFilters::get($filter)) { 105 | $value = $callback(...$args); 106 | 107 | // if $filter is a custom add callback in the property {@see $_filters}. 108 | // $filter is a method of the class 'FilterList' 109 | } elseif (method_exists(Filters::class, $filterName)) { 110 | $value = Filters::$filterName(...$args); 111 | 112 | // it is function name 113 | } elseif (function_exists($filter)) { 114 | $value = $filter(...$args); 115 | } else { 116 | throw new InvalidArgumentException("The filter [$filter] don't exists!"); 117 | } 118 | 119 | return $value; 120 | } 121 | 122 | /******************************************************************************* 123 | * custom filters 124 | ******************************************************************************/ 125 | 126 | /** 127 | * @param string $name 128 | * 129 | * @return callable|null 130 | */ 131 | public function getFilter(string $name): ?callable 132 | { 133 | return $this->_filters[$name] ?? null; 134 | } 135 | 136 | /** 137 | * @param string $name 138 | * @param callable $filter 139 | * 140 | * @return static 141 | */ 142 | public function addFilter(string $name, callable $filter): static 143 | { 144 | return $this->setFilter($name, $filter); 145 | } 146 | 147 | /** 148 | * @param string $name 149 | * @param callable $filter 150 | * 151 | * @return static 152 | */ 153 | public function setFilter(string $name, callable $filter): static 154 | { 155 | if ($name = trim($name)) { 156 | $this->_filters[$name] = $filter; 157 | } 158 | return $this; 159 | } 160 | 161 | /** 162 | * @param string $name 163 | */ 164 | public function delFilter(string $name): void 165 | { 166 | if (isset($this->_filters[$name])) { 167 | unset($this->_filters[$name]); 168 | } 169 | } 170 | 171 | /** 172 | * clear filters 173 | */ 174 | public function clearFilters(): void 175 | { 176 | $this->_filters = []; 177 | } 178 | 179 | /** 180 | * @return array 181 | */ 182 | public function getFilters(): array 183 | { 184 | return $this->_filters; 185 | } 186 | 187 | /** 188 | * @param array $filters 189 | */ 190 | public function addFilters(array $filters): void 191 | { 192 | $this->setFilters($filters); 193 | } 194 | 195 | /** 196 | * @param array $filters 197 | */ 198 | public function setFilters(array $filters): void 199 | { 200 | foreach ($filters as $name => $filter) { 201 | $this->setFilter($name, $filter); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/Filter/Filters.php: -------------------------------------------------------------------------------- 1 | 'subStr', 63 | 'substring' => 'subStr', 64 | 'str2list' => 'explode', 65 | 'str2array' => 'explode', 66 | 'string2list' => 'explode', 67 | 'string2array' => 'explode', 68 | 'toUpper' => 'uppercase', 69 | 'str2upper' => 'uppercase', 70 | 'strToUpper' => 'uppercase', 71 | 'toLower' => 'lowercase', 72 | 'str2lower' => 'lowercase', 73 | 'strToLower' => 'lowercase', 74 | 'clearNl' => 'clearNewline', 75 | 'str2time' => 'strToTime', 76 | 'strtotime' => 'strToTime', 77 | ]; 78 | 79 | /** 80 | * 布尔值验证,转换成字符串后是下列的一个,就认为他是个bool值 81 | * - "1"、"true"、"on" 和 "yes" (equal TRUE) 82 | * - "0"、"false"、"off"、"no" 和 ""(equal FALSE) 83 | * 注意: NULL 不是标量类型 84 | * 85 | * @param mixed $val 86 | * @param bool $nullAsFalse 87 | * 88 | * @return bool 89 | */ 90 | public static function boolean(mixed $val, bool $nullAsFalse = false): bool 91 | { 92 | if ($val !== null && !is_scalar($val)) { 93 | return (bool)$val; 94 | } 95 | 96 | return filter_var($val, FILTER_VALIDATE_BOOLEAN, [ 97 | 'flags' => $nullAsFalse ? FILTER_NULL_ON_FAILURE : 0 98 | ]); 99 | } 100 | 101 | /** 102 | * @see Validators::boolean() 103 | */ 104 | public static function bool(mixed $val, $nullAsFalse = false): bool 105 | { 106 | return self::boolean($val, $nullAsFalse); 107 | } 108 | 109 | /** 110 | * 过滤器删除数字中所有非法的字符。 111 | * 112 | * @note 该过滤器允许所有数字以及 . + - 113 | * 114 | * @param mixed $val 要过滤的变量 115 | * 116 | * @return int|array 117 | */ 118 | public static function integer(mixed $val): array|int 119 | { 120 | if (is_array($val)) { 121 | return array_map(self::class . '::integer', $val); 122 | } 123 | 124 | return (int)filter_var($val, FILTER_SANITIZE_NUMBER_INT); 125 | } 126 | 127 | /** 128 | * @see Filters::integer() 129 | */ 130 | public static function int(mixed $val): array|int 131 | { 132 | return self::integer($val); 133 | } 134 | 135 | /** 136 | * @param mixed $val 137 | * 138 | * @return int 139 | */ 140 | public static function abs(mixed $val): int 141 | { 142 | return abs((int)$val); 143 | } 144 | 145 | /** 146 | * 过滤器删除浮点数中所有非法的字符。 147 | * 148 | * @note 该过滤器默认允许所有数字以及 + - 149 | * 150 | * @param mixed $val 要过滤的变量 151 | * @param int|null $decimal 152 | * @param int|string $flags 标志 153 | * FILTER_FLAG_ALLOW_FRACTION - 允许小数分隔符 (比如 .) 154 | * FILTER_FLAG_ALLOW_THOUSAND - 允许千位分隔符(比如 ,) 155 | * FILTER_FLAG_ALLOW_SCIENTIFIC - 允许科学记数法(比如 e 和 E) 156 | * 157 | * @return int|float 158 | */ 159 | public static function float(mixed $val, ?int $decimal = null, int|string $flags = FILTER_FLAG_ALLOW_FRACTION): int|float 160 | { 161 | $options = (int)$flags !== 0 ? ['flags' => (int)$flags] : []; 162 | 163 | $ret = filter_var($val, FILTER_SANITIZE_NUMBER_FLOAT, $options); 164 | $new = strpos($ret, '.') ? (float)$ret : (int)$ret; 165 | 166 | if (is_int($decimal)) { 167 | return round($new, $decimal); 168 | } 169 | 170 | return $new; 171 | } 172 | 173 | /** 174 | * 去除标签,去除或编码特殊字符。 175 | * 176 | * @param mixed $val 177 | * @param int|string $flags 标志 178 | * FILTER_FLAG_NO_ENCODE_QUOTES - 该标志不编码引号 179 | * FILTER_FLAG_STRIP_LOW - 去除 ASCII 值在 32 以下的字符 180 | * FILTER_FLAG_STRIP_HIGH - 去除 ASCII 值在 127 以上的字符 181 | * FILTER_FLAG_ENCODE_LOW - 编码 ASCII 值在 32 以下的字符 182 | * FILTER_FLAG_ENCODE_HIGH - 编码 ASCII 值在 127 以上的字符 183 | * FILTER_FLAG_ENCODE_AMP - 把 & 字符编码为 & 184 | * 185 | * @return string|array 186 | */ 187 | public static function string(mixed $val, int|string $flags = 0): array|string 188 | { 189 | if (is_array($val)) { 190 | return array_map(self::class . '::string', $val); 191 | } 192 | 193 | $options = (int)$flags !== 0 ? ['flags' => (int)$flags] : []; 194 | 195 | return (string)filter_var((string)$val, FILTER_SANITIZE_FULL_SPECIAL_CHARS, $options); 196 | } 197 | 198 | /** 199 | * @see Filters::string() 200 | */ 201 | public static function stripped($val, int|string $flags = 0): array|string 202 | { 203 | return self::string($val, $flags); 204 | } 205 | 206 | /** 207 | * Convert \n and \r\n and \r to
208 | * 209 | * @param string $str String to transform 210 | * 211 | * @return string New string 212 | */ 213 | public static function nl2br(string $str): string 214 | { 215 | return str_replace(["\r\n", "\r", "\n"], '
', $str); 216 | } 217 | 218 | /** 219 | * simple trim space 220 | * 221 | * @param array|string $val 222 | * 223 | * @return string|array 224 | */ 225 | public static function trim(array|string $val): array|string 226 | { 227 | return is_array($val) ? array_map(static function ($val) { 228 | return is_string($val) ? trim($val) : $val; 229 | }, $val) : trim($val); 230 | } 231 | 232 | /** 233 | * clear space 234 | * 235 | * @param string $val 236 | * 237 | * @return mixed 238 | */ 239 | public static function clearSpace(string $val): string 240 | { 241 | return str_replace(' ', '', trim($val)); 242 | } 243 | 244 | /** 245 | * clear newline `\n` `\r\n` `\r` 246 | * 247 | * @param string $val 248 | * 249 | * @return mixed 250 | */ 251 | public static function clearNewline(string $val): string 252 | { 253 | return str_replace(["\r\n", "\r", "\n"], '', trim($val)); 254 | } 255 | 256 | /** 257 | * string to lowercase 258 | * 259 | * @param string $val 260 | * 261 | * @return string 262 | */ 263 | public static function lower(string $val): string 264 | { 265 | return self::lowercase($val); 266 | } 267 | 268 | /** 269 | * string to lowercase 270 | * 271 | * @param int|string $val 272 | * 273 | * @return string 274 | */ 275 | public static function lowercase(int|string $val): string 276 | { 277 | if (!$val || !is_string($val)) { 278 | return is_int($val) ? (string)$val : ''; 279 | } 280 | 281 | if (function_exists('mb_strtolower')) { 282 | return mb_strtolower($val, 'utf-8'); 283 | } 284 | 285 | return strtolower($val); 286 | } 287 | 288 | /** 289 | * string to uppercase 290 | * 291 | * @param string $val 292 | * 293 | * @return string 294 | */ 295 | public static function upper(string $val): string 296 | { 297 | return self::uppercase($val); 298 | } 299 | 300 | /** 301 | * string to uppercase 302 | * 303 | * @param int|string $str 304 | * 305 | * @return string 306 | */ 307 | public static function uppercase(int|string $str): string 308 | { 309 | if (!$str || !is_string($str)) { 310 | return is_int($str) ? (string)$str : ''; 311 | } 312 | 313 | if (function_exists('mb_strtoupper')) { 314 | return mb_strtoupper($str, 'utf-8'); 315 | } 316 | 317 | return strtoupper($str); 318 | } 319 | 320 | /** 321 | * @param string $str 322 | * 323 | * @return string 324 | */ 325 | public static function ucfirst(string $str): string 326 | { 327 | if (!$str) { 328 | return ''; 329 | } 330 | 331 | return self::uppercase(self::subStr($str, 0, 1)) . self::subStr($str, 1); 332 | } 333 | 334 | /** 335 | * @param string $str 336 | * 337 | * @return string 338 | */ 339 | public static function ucwords(string $str): string 340 | { 341 | if (!$str) { 342 | return ''; 343 | } 344 | 345 | if (function_exists('mb_convert_case')) { 346 | return mb_convert_case($str, MB_CASE_TITLE); 347 | } 348 | 349 | return ucwords(self::lowercase($str)); 350 | } 351 | 352 | /** 353 | * string to snake case 354 | * 355 | * @param string|mixed $val 356 | * @param string $sep 357 | * 358 | * @return string 359 | */ 360 | public static function snake(string $val, string $sep = '_'): string 361 | { 362 | return self::snakeCase($val, $sep); 363 | } 364 | 365 | /** 366 | * Transform a CamelCase string to underscore_case string 367 | * 'CMSCategories' => 'cms_categories' 368 | * 'RangePrice' => 'range_price' 369 | * 370 | * @param string $val 371 | * @param string $sep 372 | * 373 | * @return string 374 | */ 375 | public static function snakeCase(string $val, string $sep = '_'): string 376 | { 377 | if (!$val) { 378 | return ''; 379 | } 380 | 381 | $val = preg_replace('/([A-Z][a-z])/', $sep . '$1', $val); 382 | 383 | return self::lowercase(trim($val, $sep)); 384 | } 385 | 386 | /** 387 | * string to camelcase 388 | * 389 | * @param string $val 390 | * @param bool $ucFirst 391 | * 392 | * @return string 393 | */ 394 | public static function camel(string $val, bool $ucFirst = false): string 395 | { 396 | return self::camelCase($val, $ucFirst); 397 | } 398 | 399 | /** 400 | * Translates a string with underscores into camel case (e.g. first_name -> firstName) 401 | * 402 | * @param string $val 403 | * @param bool $ucFirst 404 | * 405 | * @return string 406 | */ 407 | public static function camelCase(string $val, bool $ucFirst = false): string 408 | { 409 | if (!$val) { 410 | return ''; 411 | } 412 | 413 | $str = self::lowercase($val); 414 | if ($ucFirst) { 415 | $str = self::ucfirst($str); 416 | } 417 | 418 | return preg_replace_callback('/_+([a-z])/', static function ($c) { 419 | return strtoupper($c[1]); 420 | }, $str); 421 | } 422 | 423 | /** 424 | * string to time 425 | * 426 | * @param string $val 427 | * 428 | * @return int 429 | */ 430 | public static function timestamp(string $val): int 431 | { 432 | return self::strToTime($val); 433 | } 434 | 435 | /** 436 | * string to time 437 | * 438 | * @param string $val 439 | * 440 | * @return int 441 | */ 442 | public static function strToTime(string $val): int 443 | { 444 | if (!$val) { 445 | return 0; 446 | } 447 | 448 | return (int)strtotime($val); 449 | } 450 | 451 | /** 452 | * @param string $str 453 | * @param int $start 454 | * @param int $length 455 | * @param string $encoding 456 | * 457 | * @return bool|string 458 | */ 459 | public static function subStr(string $str, int $start, int $length = 0, string $encoding = 'utf-8'): bool|string 460 | { 461 | $length = $length === 0 ? Helper::strlen($str) : $length; 462 | 463 | if (function_exists('mb_substr')) { 464 | return mb_substr($str, $start, $length, $encoding); 465 | } 466 | 467 | return substr($str, $start, $length); 468 | } 469 | 470 | /** 471 | * @param string $string 472 | * @param string $delimiter 473 | * @param int $limit 474 | * 475 | * @return array 476 | */ 477 | public static function explode(string $string, string $delimiter = ',', int $limit = 0): array 478 | { 479 | $string = trim($string, "$delimiter "); 480 | if ($string === '') { 481 | return []; 482 | } 483 | 484 | $values = []; 485 | $rawList = $limit < 1 ? explode($delimiter, $string) : explode($delimiter, $string, $limit); 486 | 487 | foreach ($rawList as $val) { 488 | if (($val = trim($val)) !== '') { 489 | $values[] = $val; 490 | } 491 | } 492 | 493 | return $values; 494 | } 495 | 496 | public static function str2list(string $str, string $sep = ',', int $limit = 0): array 497 | { 498 | return self::explode($str, $sep, $limit); 499 | } 500 | 501 | public static function str2array(string $string, string $delimiter = ',', int $limit = 0): array 502 | { 503 | return self::explode($string, $delimiter, $limit); 504 | } 505 | 506 | /** 507 | * @param mixed $val 508 | * @param string|null $allowedTags 509 | * 510 | * @return string 511 | */ 512 | public static function clearTags(mixed $val, ?string $allowedTags = null): string 513 | { 514 | return self::stripTags($val, $allowedTags); 515 | } 516 | 517 | /** 518 | * @param mixed $val 519 | * @param string|null $allowedTags e.g '

' 允许

520 | * 521 | * @return string 522 | */ 523 | public static function stripTags(mixed $val, ?string $allowedTags = null): string 524 | { 525 | if (!$val || !is_string($val)) { 526 | return ''; 527 | } 528 | 529 | return $allowedTags ? strip_tags($val, $allowedTags) : strip_tags($val); 530 | } 531 | 532 | /** 533 | * 去除 URL 编码不需要的字符。 534 | * 535 | * @note 与 urlencode() 函数很类似。 536 | * 537 | * @param string $val 要过滤的数据 538 | * @param int $flags 标志 539 | * FILTER_FLAG_STRIP_LOW - 去除 ASCII 值在 32 以下的字符 540 | * FILTER_FLAG_STRIP_HIGH - 去除 ASCII 值在 32 以上的字符 541 | * FILTER_FLAG_ENCODE_LOW - 编码 ASCII 值在 32 以下的字符 542 | * FILTER_FLAG_ENCODE_HIGH - 编码 ASCII 值在 32 以上的字符 543 | * 544 | * @return string 545 | */ 546 | public static function encoded(string $val, int $flags = 0): string 547 | { 548 | $settings = $flags !== 0 ? ['flags' => $flags] : []; 549 | 550 | return (string)filter_var($val, FILTER_SANITIZE_ENCODED, $settings); 551 | } 552 | 553 | /** 554 | * 应用 addslashes() 转义数据 555 | * 556 | * @param string $val 557 | * 558 | * @return string 559 | */ 560 | public static function quotes(string $val): string 561 | { 562 | $flag = FILTER_SANITIZE_ADD_SLASHES; 563 | 564 | return (string)filter_var($val, $flag); 565 | } 566 | 567 | /** 568 | * like htmlspecialchars(), HTML 转义字符 '"<>& 以及 ASCII 值小于 32 的字符。 569 | * 570 | * @param string $val 571 | * @param int $flags 标志 572 | * FILTER_FLAG_STRIP_LOW - 去除 ASCII 值在 32 以下的字符 573 | * FILTER_FLAG_STRIP_HIGH - 去除 ASCII 值在 32 以上的字符 574 | * FILTER_FLAG_ENCODE_HIGH - 编码 ASCII 值在 32 以上的字符 575 | * 576 | * @return string 577 | */ 578 | public static function specialChars(string $val, int $flags = 0): string 579 | { 580 | $settings = $flags !== 0 ? ['flags' => $flags] : []; 581 | 582 | return (string)filter_var($val, FILTER_SANITIZE_SPECIAL_CHARS, $settings); 583 | } 584 | 585 | /** 586 | * @param string $val 587 | * @param int $flags 588 | * 589 | * @return string 590 | */ 591 | public static function escape(string $val, int $flags = 0): string 592 | { 593 | return self::specialChars($val, $flags); 594 | } 595 | 596 | /** 597 | * HTML 转义字符 '"<>& 以及 ASCII 值小于 32 的字符。 598 | * 599 | * @param string $val 600 | * @param int $flags 标志 FILTER_FLAG_NO_ENCODE_QUOTES 601 | * 602 | * @return string 603 | */ 604 | public static function fullSpecialChars(string $val, int $flags = 0): string 605 | { 606 | $settings = $flags !== 0 ? ['flags' => $flags] : []; 607 | 608 | return (string)filter_var($val, FILTER_SANITIZE_FULL_SPECIAL_CHARS, $settings); 609 | } 610 | 611 | /** 612 | * 字符串长度过滤截取 613 | * 614 | * @param string $string 615 | * @param int|string $start 616 | * @param int|string $length 617 | * 618 | * @return string 619 | */ 620 | public static function stringCute(string $string, int|string $start = 0, int|string $length = 0): string 621 | { 622 | return self::subStr($string, (int)$start, (int)$length); 623 | } 624 | 625 | /** 626 | * @param string $string 627 | * @param int|string $start 628 | * @param int|string $length 629 | * 630 | * @return string 631 | */ 632 | public static function cut(string $string, int|string $start = 0, int|string $length = 0): string 633 | { 634 | return self::stringCute($string, $start, $length); 635 | } 636 | 637 | /** 638 | * url地址过滤 移除所有不符合 url 的字符 639 | * 640 | * - 该过滤器允许所有的字母、数字以及 $-_.+!*'(),{}|\^~[]`"><#%;/?:@&= 641 | * 642 | * @param string $val 要过滤的数据 643 | * 644 | * @return string 645 | */ 646 | public static function url(string $val): string 647 | { 648 | return (string)filter_var($val, FILTER_SANITIZE_URL); 649 | } 650 | 651 | /** 652 | * email 地址过滤 移除所有不符合 email 的字符 653 | * 654 | * @param string $val 要过滤的数据 655 | * 656 | * @return string 657 | */ 658 | public static function email(string $val): string 659 | { 660 | return (string)filter_var($val, FILTER_SANITIZE_EMAIL); 661 | } 662 | 663 | /** 664 | * 不进行任何过滤,去除或编码特殊字符。这个过滤器也是FILTER_DEFAULT别名。 665 | * 该过滤器删除那些对应用程序有潜在危害的数据。它用于去除标签以及删除或编码不需要的字符。 666 | * 如果不规定标志,则该过滤器没有任何行为。 667 | * 668 | * @param string $string 669 | * @param int|string $flags 标志 670 | * FILTER_FLAG_STRIP_LOW - 去除 ASCII 值在 32 以下的字符 671 | * FILTER_FLAG_STRIP_HIGH - 去除 ASCII 值在 32 以上的字符 672 | * FILTER_FLAG_ENCODE_LOW - 编码 ASCII 值在 32 以下的字符 673 | * FILTER_FLAG_ENCODE_HIGH - 编码 ASCII 值在 32 以上的字符 674 | * FILTER_FLAG_ENCODE_AMP - 把 & 字符编码为 & 675 | * 676 | * @return string|mixed 677 | */ 678 | public static function unsafeRaw(string $string, int|string $flags = 0): mixed 679 | { 680 | $settings = (int)$flags !== 0 ? ['flags' => (int)$flags] : []; 681 | 682 | return filter_var($string, FILTER_UNSAFE_RAW, $settings); 683 | } 684 | 685 | /** 686 | * 自定义回调过滤 687 | * 688 | * @param mixed $val 689 | * @param callable $callback 690 | * 691 | * @return bool|mixed 692 | */ 693 | public static function callback(mixed $val, callable $callback): mixed 694 | { 695 | return filter_var($val, FILTER_CALLBACK, ['options' => $callback]); 696 | } 697 | 698 | /** 699 | * 去除数组中的重复值 700 | * 701 | * @param mixed $val 702 | * 703 | * @return array 704 | */ 705 | public static function unique(mixed $val): array 706 | { 707 | if (!$val || !is_array($val)) { 708 | return (array)$val; 709 | } 710 | 711 | return array_unique($val); 712 | } 713 | } 714 | -------------------------------------------------------------------------------- /src/Filter/Filtration.php: -------------------------------------------------------------------------------- 1 | 'tom'], 23 | * ['email', 'string|email'], 24 | * ])->filtering(); 25 | */ 26 | class Filtration 27 | { 28 | use FilteringTrait; 29 | 30 | /** @var array raw data */ 31 | private array $_data; 32 | 33 | /** @var array the rules is by setRules() */ 34 | private array $_rules; 35 | 36 | /** 37 | * @param array $data 38 | * @param array $rules 39 | * 40 | * @return Filtration 41 | */ 42 | public static function make(array $data = [], array $rules = []): self 43 | { 44 | return new self($data, $rules); 45 | } 46 | 47 | /** 48 | * Filtration constructor. 49 | * 50 | * @param array $data 51 | * @param array $rules 52 | */ 53 | public function __construct(array $data = [], array $rules = []) 54 | { 55 | $this->_data = $data; 56 | $this->_rules = $rules; 57 | } 58 | 59 | /** 60 | * @param array $data 61 | * 62 | * @return $this 63 | */ 64 | public function load(array $data): self 65 | { 66 | $this->_data = array_merge($this->_data, $data); 67 | return $this; 68 | } 69 | 70 | /** 71 | * @param array $rules 72 | * 73 | * @return array 74 | */ 75 | public function filtering(array $rules = []): array 76 | { 77 | return $this->applyRules($rules); 78 | } 79 | 80 | /** 81 | * Apply a series of filtering rules to the input data 82 | * 83 | * @param array $rules 84 | * @param array $data 85 | * 86 | * @return array Return filtered data 87 | */ 88 | public function applyRules(array $rules = [], array $data = []): array 89 | { 90 | $data = $data ?: $this->_data; 91 | $rules = $rules ?: $this->_rules; 92 | // save clean data 93 | $filtered = []; 94 | 95 | foreach ($rules as $rule) { 96 | if (!isset($rule[0], $rule[1])) { 97 | continue; 98 | } 99 | 100 | if (!$fields = $rule[0]) { 101 | continue; 102 | } 103 | 104 | $fields = is_string($fields) ? Filters::explode($fields) : (array)$fields; 105 | 106 | foreach ($fields as $field) { 107 | if (!isset($data[$field]) && isset($rule['default'])) { 108 | $filtered[$field] = $rule['default']; 109 | } else { 110 | $filtered[$field] = $this->valueFiltering($data[$field], $rule[1]); 111 | } 112 | } 113 | } 114 | 115 | return $filtered; 116 | } 117 | 118 | /** 119 | * value sanitize Filter the value directly 120 | * 121 | * @param mixed $value 122 | * @param array|string $filters 123 | * 124 | * @return mixed 125 | */ 126 | public function sanitize(mixed $value, array|string $filters): mixed 127 | { 128 | return $this->valueFiltering($value, $filters); 129 | } 130 | 131 | /** 132 | * get a field value from {@see $data} 133 | * 134 | * @param int|string $field 135 | * @param array|string|null $filters 136 | * @param mixed|null $default 137 | * 138 | * @return mixed 139 | */ 140 | public function get(int|string $field, array|string $filters = null, mixed $default = null): mixed 141 | { 142 | if (!isset($this->_data[$field])) { 143 | return $default; 144 | } 145 | 146 | $value = $this->_data[$field]; 147 | 148 | if (!$filters) { 149 | return $value; 150 | } 151 | 152 | return $this->valueFiltering($value, $filters); 153 | } 154 | 155 | /** 156 | * @param int|string $field 157 | * 158 | * @return bool 159 | */ 160 | public function has(int|string $field): bool 161 | { 162 | return isset($this->_data[$field]); 163 | } 164 | 165 | /** 166 | * @param bool $clearFilters 167 | * 168 | * @return $this 169 | */ 170 | public function reset(bool $clearFilters = false): self 171 | { 172 | $this->_data = $this->_rules = []; 173 | 174 | if ($clearFilters) { 175 | $this->clearFilters(); 176 | } 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * @return array 183 | */ 184 | public function getData(): array 185 | { 186 | return $this->_data; 187 | } 188 | 189 | /** 190 | * @return array 191 | */ 192 | public function all(): array 193 | { 194 | return $this->_data; 195 | } 196 | 197 | /** 198 | * @param array $rules 199 | * 200 | * @return self 201 | */ 202 | public function setRules(array $rules): self 203 | { 204 | $this->_rules = $rules; 205 | return $this; 206 | } 207 | 208 | /** 209 | * @return array 210 | */ 211 | public function getRules(): array 212 | { 213 | return $this->_rules; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Filter/UserFilters.php: -------------------------------------------------------------------------------- 1 | $filter) { 79 | self::set($name, $filter); 80 | } 81 | } 82 | 83 | /** 84 | * @param string $name 85 | */ 86 | public static function remove(string $name): void 87 | { 88 | if (isset(self::$filters[$name])) { 89 | unset(self::$filters[$name]); 90 | } 91 | } 92 | 93 | /** 94 | * clear all filters 95 | */ 96 | public static function removeAll(): void 97 | { 98 | self::$filters = []; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/GlobalOption.php: -------------------------------------------------------------------------------- 1 | 'image/bmp', 60 | 'gif' => 'image/gif', 61 | 'ief' => 'image/ief', 62 | 'jpeg' => 'image/jpeg', 63 | 'jpg' => 'image/jpeg', 64 | 'jpe' => 'image/jpeg', 65 | 'png' => 'image/png', 66 | 'svg' => 'image/svg+xml', 67 | 'ico' => 'image/x-icon', 68 | ]; 69 | 70 | /** 71 | * @var array 72 | */ 73 | public static array $imgMimeConstants = [ 74 | IMAGETYPE_GIF, 75 | IMAGETYPE_JPEG, 76 | IMAGETYPE_PNG, 77 | IMAGETYPE_BMP, 78 | IMAGETYPE_WBMP, 79 | IMAGETYPE_ICO 80 | ]; 81 | 82 | /** @var string */ 83 | public static string $fileValidators = '|file|image|mimeTypes|mimes|'; 84 | 85 | /** 86 | * @param string $ext 87 | * 88 | * @return string 89 | */ 90 | public static function getImageMime(string $ext): string 91 | { 92 | return self::$imgMimeTypes[$ext] ?? ''; 93 | } 94 | 95 | /** 96 | * @param string $mime 97 | * 98 | * @return string|null 99 | */ 100 | public static function getImageExtByMime(string $mime): string 101 | { 102 | $key = array_search($mime, self::$imgMimeTypes, true); 103 | return (string)$key; 104 | } 105 | 106 | /** 107 | * @param string $file 108 | * 109 | * @return string eg: 'image/gif' 110 | */ 111 | public static function getMimeType(string $file): string 112 | { 113 | if (!file_exists($file)) { 114 | return ''; 115 | } 116 | 117 | if (function_exists('mime_content_type')) { 118 | /** @noinspection PhpComposerExtensionStubsInspection */ 119 | return mime_content_type($file); 120 | } 121 | 122 | if (function_exists('finfo_file')) { 123 | /** @noinspection PhpComposerExtensionStubsInspection */ 124 | return (string)finfo_file(finfo_open(FILEINFO_MIME_TYPE), $file); 125 | } 126 | 127 | return ''; 128 | } 129 | 130 | /** 131 | * @param array|int|string $val 132 | * 133 | * @return int 134 | */ 135 | public static function length(array|int|string $val): int 136 | { 137 | if (is_int($val)) { 138 | return $val; 139 | } 140 | 141 | if (is_string($val)) { 142 | return self::strlen($val); 143 | } 144 | 145 | return is_array($val) ? count($val) : -1; 146 | } 147 | 148 | /** 149 | * @param string $str 150 | * @param string $encoding 151 | * 152 | * @return int 153 | */ 154 | public static function strlen(string $str, string $encoding = 'UTF-8'): int 155 | { 156 | $str = html_entity_decode($str, ENT_COMPAT, 'UTF-8'); 157 | 158 | if (function_exists('mb_strlen')) { 159 | return mb_strlen($str, $encoding); 160 | } 161 | 162 | return strlen($str); 163 | } 164 | 165 | /** 166 | * @param string $str 167 | * @param string $find 168 | * @param int $offset 169 | * @param string $encoding 170 | * 171 | * @return bool|int 172 | */ 173 | public static function strPos(string $str, string $find, int $offset = 0, string $encoding = 'UTF-8'): bool|int 174 | { 175 | if (function_exists('mb_strpos')) { 176 | return mb_strpos($str, $find, $offset, $encoding); 177 | } 178 | 179 | return strpos($str, $find, $offset); 180 | } 181 | 182 | /** 183 | * @param string $str 184 | * @param string $find 185 | * @param int $offset 186 | * @param string $encoding 187 | * 188 | * @return bool|int 189 | */ 190 | public static function strrpos(string $str, string $find, int $offset = 0, string $encoding = 'utf-8'): bool|int 191 | { 192 | if (function_exists('mb_strrpos')) { 193 | return mb_strrpos($str, $find, $offset, $encoding); 194 | } 195 | 196 | return strrpos($str, $find, $offset); 197 | } 198 | 199 | /** 200 | * @param string $field 201 | * 202 | * @return string 203 | */ 204 | public static function prettifyFieldName(string $field): string 205 | { 206 | $str = Filters::snakeCase($field, ' '); 207 | 208 | return strpos($str, '_') ? str_replace('_', ' ', $str) : $str; 209 | } 210 | 211 | /** 212 | * @param string $scene Current scene value 213 | * @param array|string $ruleOn 214 | * 215 | * @return bool 216 | */ 217 | public static function ruleIsAvailable(string $scene, array|string $ruleOn): bool 218 | { 219 | // - rule is not limit scene 220 | if (!$ruleOn) { 221 | return true; 222 | } 223 | 224 | // - $ruleOn is not empty 225 | 226 | // current scene is empty 227 | if (!$scene) { 228 | return false; 229 | } 230 | 231 | $scenes = is_string($ruleOn) ? Filters::explode($ruleOn) : $ruleOn; 232 | 233 | return in_array($scene, $scenes, true); 234 | } 235 | 236 | /** 237 | * getValueOfArray 支持以 '.' 分割进行子级值获取 eg: 'goods.apple' 238 | * 239 | * @param array $array 240 | * @param array|string|null $key 241 | * @param mixed|null $default 242 | * 243 | * @return mixed 244 | */ 245 | public static function getValueOfArray(array $array, array|string|null $key, mixed $default = null): mixed 246 | { 247 | if (null === $key) { 248 | return $array; 249 | } 250 | 251 | if (array_key_exists($key, $array)) { 252 | return $array[$key]; 253 | } 254 | 255 | if (!strpos($key, '.')) { 256 | return $default; 257 | } 258 | 259 | $found = false; 260 | foreach (explode('.', $key) as $segment) { 261 | if (is_array($array) && array_key_exists($segment, $array)) { 262 | $found = true; 263 | $array = $array[$segment]; 264 | } else { 265 | return $default; 266 | } 267 | } 268 | 269 | return $found ? $array : $default; 270 | } 271 | 272 | /** 273 | * @param callable|mixed $cb 274 | * @param array $args 275 | * 276 | * @return mixed 277 | * @throws InvalidArgumentException 278 | */ 279 | public static function call(mixed $cb, ...$args): mixed 280 | { 281 | if (is_string($cb)) { 282 | // className::method 283 | if (strpos($cb, '::') > 0) { 284 | $cb = explode('::', $cb, 2); 285 | // function 286 | } elseif (function_exists($cb)) { 287 | return $cb(...$args); 288 | } 289 | } elseif (is_object($cb) && method_exists($cb, '__invoke')) { 290 | return $cb(...$args); 291 | } 292 | 293 | if (is_array($cb)) { 294 | [$obj, $mhd] = $cb; 295 | 296 | return is_object($obj) ? $obj->$mhd(...$args) : $obj::$mhd(...$args); 297 | } 298 | 299 | throw new InvalidArgumentException('The parameter is not a callable'); 300 | } 301 | 302 | /** 303 | * compare of size 304 | * - int Compare size 305 | * - string Compare length 306 | * - array Compare length 307 | * 308 | * @param int|string|array $val 309 | * @param string $operator 310 | * @param int|string|array $expected 311 | * 312 | * @return bool 313 | */ 314 | public static function compareSize(int|string|array $val, string $operator, int|string|array $expected): bool 315 | { 316 | // type must be same 317 | if (gettype($val) !== gettype($expected)) { 318 | return false; 319 | } 320 | 321 | // not in: int, string, array 322 | if (($len = self::length($val)) < 0) { 323 | return false; 324 | } 325 | 326 | $wantLen = self::length($expected); 327 | 328 | switch ($operator) { 329 | case '>': 330 | $ok = $len > $wantLen; 331 | break; 332 | case '>=': 333 | $ok = $len >= $wantLen; 334 | break; 335 | case '<': 336 | $ok = $len < $wantLen; 337 | break; 338 | case '<=': 339 | $ok = $len <= $wantLen; 340 | break; 341 | default: 342 | $ok = false; 343 | } 344 | 345 | return $ok; 346 | } 347 | 348 | /** 349 | * @param mixed $val 350 | * @param array $list 351 | * 352 | * @return bool 353 | */ 354 | public static function inArray(mixed $val, array $list): bool 355 | { 356 | if (!is_scalar($val)) { 357 | return false; 358 | } 359 | 360 | $valType = gettype($val); 361 | foreach ($list as $item) { 362 | if (!is_scalar($item)) { 363 | continue; 364 | } 365 | 366 | // compare value 367 | switch ($valType) { 368 | case 'integer': 369 | $exist = $val === (int)$item; 370 | break; 371 | case 'float': 372 | case 'double': 373 | case 'string': 374 | $exist = (string)$val === (string)$item; 375 | break; 376 | default: 377 | return false; 378 | } 379 | 380 | if ($exist) { 381 | return true; 382 | } 383 | } 384 | 385 | return false; 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /src/Locale/LocaleZhCN.php: -------------------------------------------------------------------------------- 1 | [ 22 | '{attr} 必须是整数!', 23 | '{attr} 必须是整数并且最小值是 {min}', 24 | '{attr} 必须是整数并且值的范围必须在 {min} ~ {max}', 25 | ], 26 | // 'num' 27 | 'number' => [ 28 | '{attr} 必须是整数并且大于0!', 29 | '{attr} 必须是整数并且最小值是 {min}', 30 | '{attr} 必须是整数并且值的范围必须在 {min} ~ {max}', 31 | ], 32 | // 'bool', 'boolean', 33 | 'boolean' => '{attr} 必须是布尔类型!', 34 | 'float' => '{attr} 必须是浮点数!', 35 | 'url' => '{attr} 不是url地址!', 36 | 'email' => '{attr} 不是电子邮件地址!', 37 | 'date' => '{attr} 不是日期格式!', 38 | 'dateFormat' => '{attr} is not in a {value0} date format!', 39 | 'ip' => '{attr} 不是IP地址!', 40 | 'ipv4' => '{attr} 不是 IPv4 地址!', 41 | 'ipv6' => '{attr} 不是IPv6地址!', 42 | 'required' => '字段 {attr} 是必须的!', 43 | 'requiredIf' => '字段 {attr} 是必须的!', 44 | 'length' => [ 45 | '{attr} 长度验证未通过!', 46 | '{attr} 必须是一个字符串/数组,最小长度是 {min}', 47 | '{attr} 值不合法,长度范围只允许是 {min} ~ {max}', 48 | ], 49 | // 'range', 'between' 50 | 'size' => [ 51 | '{attr} size validation is not through!', 52 | '{attr} 值不合法,最小是 {min}', 53 | // '{attr} must be an integer/string/array and value/length range {min} ~ {max}', 54 | '{attr} 值不合法,大小范围只允许是 {min} ~ {max}', 55 | ], 56 | 57 | // 'lengthEq', 'sizeEq' 58 | 'fixedSize' => '{attr} length must is {value0}', 59 | 60 | 'eq' => '{attr} must be equals to {value0}', 61 | // 'different' 62 | 'ne' => '{attr} can not be equals to {value0}', 63 | 'min' => '{attr} minimum boundary is {value0}', 64 | 'max' => '{attr} maximum boundary is {value0}', 65 | 'lt' => '{attr} value must be less than {value0}', 66 | 'lte' => '{attr} value must be less than or equals to {value0}', 67 | 'gt' => '{attr} value must be greater than {value0}', 68 | 'gte' => '{attr} value must be greater than or equals to {value0}', 69 | 70 | // field compare 71 | 'eqField' => '{attr} 值必须等于 {value0} 的值', 72 | 'neqField' => '{attr} 值不能等于 {value0} 的值', 73 | 'ltField' => '{attr} 值必须小于 {value0} 的值', 74 | 'lteField' => '{attr} 值必须小于或等于 {value0} 的值', 75 | 'gtField' => '{attr} 值必须大于 {value0} 的值', 76 | 'gteField' => '{attr} 值必须大于或等于 {value0} 的值', 77 | 78 | // 'in', 'enum', 79 | 'enum' => '{attr} must in ({value0})', 80 | 'notIn' => '{attr} cannot in ({value0})', 81 | 82 | 'string' => [ 83 | '{attr} 必须是字符串', 84 | '{attr} 必须是字符串并且最小长度为 {min}', 85 | '{attr} 必须是字符串并且长度范围必须是 {min} ~ {max}', 86 | ], 87 | 88 | // 'regex', 'regexp', 89 | 'regexp' => '{attr} does not match the {value0} conditions', 90 | 91 | 'mustBe' => '{attr} must be equals to {value0}', 92 | 'notBe' => '{attr} can not be equals to {value0}', 93 | 94 | 'compare' => '{attr} must be equals to {value0}', 95 | 'same' => '{attr} must be equals to {value0}', 96 | 97 | 'isArray' => '{attr} 必须是数组', 98 | 'isMap' => '{attr} 必须是数组并且是键-值对格式', 99 | 'isList' => '{attr} 必须是自然数组', 100 | 'intList' => '{attr} 必须是一个数组并且值都是整数', 101 | 'numList' => '{attr} 必须是一个数组并且值都是大于0的数字', 102 | 'strList' => '{attr} 必须是一个数组并且值都是字符串', 103 | 'arrList' => '{attr} 必须是一个数组,并且值也都是数组', 104 | 105 | 'each' => '{attr} each value must be through the "{value0}" verify', 106 | 'hasKey' => '{attr} 必须包含键字段 {value0}', 107 | 'distinct' => '{attr} 中不应该有重复的值', 108 | 109 | 'json' => '{attr} 必须是 json 字符串', 110 | 111 | 'file' => '{attr} 必须是上传的文件', 112 | 'image' => '{attr} 必须是上传的图片文件', 113 | 114 | 'callback' => '{attr} don\'t pass the test and verify!', 115 | '__default' => '{attr} validation is not through!', 116 | ]; 117 | 118 | /** 119 | * register to global message 120 | */ 121 | public static function register(): void 122 | { 123 | GlobalMessage::setMessages(self::$messages); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/RV.php: -------------------------------------------------------------------------------- 1 | = $min 204 | * @param int|null $max The value length must be <= $min 205 | * @param array|null $default 206 | * 207 | * @return array 208 | */ 209 | public static function getStrings(string $field, ?int $min = null, ?int $max = null, ?array $default = null): array 210 | { 211 | if (!isset(self::$data[$field])) { 212 | if ($default === null) { 213 | throw static::newEx($field, 'is required and must be string array'); 214 | } 215 | return $default; 216 | } 217 | 218 | if (!$val = self::$data[$field]) { 219 | if ($default === null) { 220 | throw static::newEx($field, 'is required and must be string array'); 221 | } 222 | return $default; 223 | } 224 | 225 | $arr = is_scalar($val) ? [trim((string)$val)] : (array)$val; 226 | 227 | if (!Validators::strList($arr)) { 228 | throw static::newEx($field, 'must be string array'); 229 | } 230 | 231 | // check min and max value 232 | if (!Validators::integer(count($arr), $min, $max)) { 233 | throw static::newEx($field, self::fmtMinMaxToMsg('must be array value and length', $min, $max)); 234 | } 235 | 236 | return $arr; 237 | } 238 | 239 | /** 240 | * @param string $field 241 | * @param int|null $min 242 | * @param int|null $max 243 | * @param array|null $default 244 | * 245 | * @return array 246 | */ 247 | public static function getArray(string $field, ?int $min = null, ?int $max = null, ?array $default = null): array 248 | { 249 | if (!isset(self::$data[$field])) { 250 | if ($default === null) { 251 | throw static::newEx($field, 'is required and must be array'); 252 | } 253 | 254 | return $default; 255 | } 256 | 257 | if (!$val = self::$data[$field]) { 258 | if ($default === null) { 259 | throw static::newEx($field, 'is required and must be array'); 260 | } 261 | return $default; 262 | } 263 | 264 | $arr = is_scalar($val) ? [$val] : (array)$val; 265 | 266 | // check min and max value 267 | if (!Validators::integer(count($arr), $min, $max)) { 268 | throw static::newEx($field, self::fmtMinMaxToMsg('must be array value and length', $min, $max)); 269 | } 270 | 271 | return $arr; 272 | } 273 | 274 | /** 275 | * @param string $field 276 | * @param int|null $min 277 | * @param int|null $max 278 | * @param array|null $default 279 | * @param string $sep 280 | * 281 | * @return array 282 | */ 283 | public static function getArrayBySplit(string $field, ?int $min = null, ?int $max = null, ?array $default = null, string $sep = ','): array 284 | { 285 | if (!isset(self::$data[$field])) { 286 | if ($default === null) { 287 | throw self::newEx($field, 'is required and must be string'); 288 | } 289 | return $default; 290 | } 291 | 292 | $val = self::$data[$field]; 293 | if (!$val || !is_string($val)) { 294 | if ($default === null) { 295 | throw self::newEx($field, 'is required and must be string'); 296 | } 297 | return $default; 298 | } 299 | 300 | $arr = Str::explode($val, $sep); 301 | 302 | // check min and max value 303 | if (!Validators::integer(count($arr), $min, $max)) { 304 | throw static::newEx($field, self::fmtMinMaxToMsg('array value length', $min, $max)); 305 | } 306 | 307 | return $arr; 308 | } 309 | 310 | /** 311 | * @param string $field 312 | * @param int|null $min 313 | * @param int|null $max 314 | * @param array|null $default 315 | * 316 | * @return array 317 | */ 318 | public static function getArrayByJSON(string $field, ?int $min = null, ?int $max = null, ?array $default = null): array 319 | { 320 | if (!isset(self::$data[$field])) { 321 | if ($default === null) { 322 | throw new ValidateException($field, 'is required and must be JSON string'); 323 | } 324 | 325 | return $default; 326 | } 327 | 328 | $val = self::$data[$field]; 329 | if (!$val || !is_string($val)) { 330 | if ($default === null) { 331 | throw self::newEx($field, 'is required and must be JSON string'); 332 | } 333 | return $default; 334 | } 335 | 336 | // must start with: { OR [ 337 | if ('[' !== $val[0] && '{' !== $val[0]) { 338 | throw self::newEx($field, 'must be valid JSON string'); 339 | } 340 | 341 | $arr = json_decode($val, true, 512, JSON_THROW_ON_ERROR); 342 | if (json_last_error() !== JSON_ERROR_NONE) { 343 | throw self::newEx($field, 'must be valid JSON string'); 344 | } 345 | 346 | // check min and max value 347 | if (!Validators::integer(count($arr), $min, $max)) { 348 | throw static::newEx($field, self::fmtMinMaxToMsg('array value length', $min, $max)); 349 | } 350 | 351 | return $arr; 352 | } 353 | 354 | /** 355 | * @param string $prefix 356 | * @param int|null $min 357 | * @param int|null $max 358 | * @param string $sepMsg 359 | * 360 | * @return string 361 | */ 362 | public static function fmtMinMaxToMsg(string $prefix, ?int $min = null, ?int $max = null, string $sepMsg = 'and'): string 363 | { 364 | if ($min !== null && $max !== null) { 365 | return "$prefix $sepMsg must be >= $min and <= $max"; 366 | } 367 | 368 | // check min and max value 369 | if ($min !== null) { 370 | return "$prefix $sepMsg must be greater or equal to $min"; 371 | } 372 | 373 | if ($max !== null) { 374 | return "$prefix $sepMsg must be less than or equal to $max"; 375 | } 376 | 377 | return $prefix; 378 | } 379 | 380 | /** 381 | * @param string $field 382 | * @param string $errMsg 383 | * 384 | * @return ValidateException 385 | */ 386 | public static function newEx(string $field, string $errMsg): ValidateException 387 | { 388 | /** @psalm-var class-string $class */ 389 | $class = self::$throwClass ?: ValidateException::class; 390 | return new $class($field, $errMsg, self::$throwCode); 391 | } 392 | 393 | /** 394 | * @return int 395 | */ 396 | public static function getThrowCode(): int 397 | { 398 | return self::$throwCode; 399 | } 400 | 401 | /** 402 | * @param int $throwCode 403 | */ 404 | public static function setThrowCode(int $throwCode): void 405 | { 406 | self::$throwCode = $throwCode; 407 | } 408 | 409 | /** 410 | * @return string 411 | */ 412 | public static function getThrowClass(): string 413 | { 414 | return self::$throwClass; 415 | } 416 | 417 | /** 418 | * @param string $throwClass 419 | */ 420 | public static function setThrowClass(string $throwClass): void 421 | { 422 | self::$throwClass = $throwClass; 423 | } 424 | } 425 | -------------------------------------------------------------------------------- /src/Simple/ValidValue.php: -------------------------------------------------------------------------------- 1 | ?string, 30 | * 'message' => string, 31 | * ]; 32 | * ``` 33 | * 34 | * @param string $value 35 | * @param int|null $min 36 | * @param int|null $max 37 | * @param array $opt 38 | * 39 | * @return string 40 | */ 41 | public static function getString(string $value, ?int $min, ?int $max, array $opt = []): string 42 | { 43 | return ''; 44 | } 45 | 46 | /** 47 | * @return int 48 | */ 49 | public static function getThrowCode(): int 50 | { 51 | return self::$throwCode; 52 | } 53 | 54 | /** 55 | * @param int $throwCode 56 | */ 57 | public static function setThrowCode(int $throwCode): void 58 | { 59 | self::$throwCode = $throwCode; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public static function getThrowClass(): string 66 | { 67 | return self::$throwClass; 68 | } 69 | 70 | /** 71 | * @param string $throwClass 72 | */ 73 | public static function setThrowClass(string $throwClass): void 74 | { 75 | self::$throwClass = $throwClass; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Traits/ErrorMessageTrait.php: -------------------------------------------------------------------------------- 1 | 'field', 'msg' => 'error Message1' ], 45 | * ['name' => 'field2', 'msg' => 'error Message2' ], 46 | * ] 47 | * 48 | * @var array[] 49 | */ 50 | private array $_errors = []; 51 | 52 | /** 53 | * Whether there is error stop validation 是否出现验证失败就立即停止验证 54 | * True -- 出现一个验证失败即停止验证,并退出 55 | * False -- 全部验证并将错误信息保存到 {@see $_errors} 56 | * 57 | * @var boolean 58 | */ 59 | private bool $_stopOnError = true; 60 | 61 | /** 62 | * Prettify field name on get error message 63 | * 64 | * @var bool 65 | */ 66 | private bool $_prettifyName = true; 67 | 68 | protected function prepareValidation(): void 69 | { 70 | // error message 71 | $this->_messages = array_merge($this->messages(), $this->_messages); 72 | // field translate 73 | $this->_translates = array_merge($this->translates(), $this->_translates); 74 | } 75 | 76 | /******************************************************************************* 77 | * Errors Information 78 | ******************************************************************************/ 79 | 80 | /** 81 | * @return bool 82 | */ 83 | protected function shouldStop(): bool 84 | { 85 | return $this->isFail() && $this->_stopOnError; 86 | } 87 | 88 | /** 89 | * Is there an error? 90 | * 91 | * @return boolean 92 | */ 93 | public function hasError(): bool 94 | { 95 | return $this->isFail(); 96 | } 97 | 98 | /** 99 | * @return bool 100 | */ 101 | public function isFail(): bool 102 | { 103 | return count($this->_errors) > 0; 104 | } 105 | 106 | /** 107 | * @return bool 108 | */ 109 | public function failed(): bool 110 | { 111 | return $this->isFail(); 112 | } 113 | 114 | /** 115 | * @return bool 116 | * @deprecated will delete, please use isOk() or isPassed() instead 117 | */ 118 | public function ok(): bool 119 | { 120 | return !$this->isFail(); 121 | } 122 | 123 | /** 124 | * @return bool 125 | */ 126 | public function isOk(): bool 127 | { 128 | return !$this->isFail(); 129 | } 130 | 131 | /** 132 | * @return bool 133 | * @deprecated will delete, please use isOk() or isPassed() instead 134 | */ 135 | public function passed(): bool 136 | { 137 | return !$this->isFail(); 138 | } 139 | 140 | /** 141 | * @return bool 142 | */ 143 | public function isPassed(): bool 144 | { 145 | return !$this->isFail(); 146 | } 147 | 148 | /** 149 | * check field whether in the errors 150 | * 151 | * @param string $field 152 | * 153 | * @return bool 154 | */ 155 | public function inError(string $field): bool 156 | { 157 | foreach ($this->_errors as $item) { 158 | if ($field === $item['name']) { 159 | return true; 160 | } 161 | } 162 | return false; 163 | } 164 | 165 | /** 166 | * @param string $field 167 | * @param string $msg 168 | */ 169 | public function addError(string $field, string $msg): void 170 | { 171 | $this->_errors[] = [ 172 | 'name' => $field, 173 | 'msg' => $msg, 174 | ]; 175 | } 176 | 177 | /** 178 | * @param string $field Only get errors of the field. 179 | * 180 | * @return array 181 | */ 182 | public function getErrors(string $field = ''): array 183 | { 184 | if ($field) { 185 | $errors = []; 186 | foreach ($this->_errors as $item) { 187 | if ($field === $item['name']) { 188 | $errors[] = $item['msg']; 189 | } 190 | } 191 | 192 | return $errors; 193 | } 194 | 195 | return $this->_errors; 196 | } 197 | 198 | /** 199 | * clear errors 200 | */ 201 | public function clearErrors(): void 202 | { 203 | $this->_errors = []; 204 | } 205 | 206 | /** 207 | * Get the first error message 208 | * 209 | * @param bool $onlyMsg 210 | * 211 | * @return array|string 212 | */ 213 | public function firstError(bool $onlyMsg = true): array|string 214 | { 215 | if (!$errors = $this->_errors) { 216 | return $onlyMsg ? '' : []; 217 | } 218 | 219 | $first = array_shift($errors); 220 | return $onlyMsg ? $first['msg'] : $first; 221 | } 222 | 223 | /** 224 | * Get the last error message 225 | * 226 | * @param bool $onlyMsg 227 | * 228 | * @return array|string 229 | */ 230 | public function lastError(bool $onlyMsg = true): array|string 231 | { 232 | if (!$errors = $this->_errors) { 233 | return $onlyMsg ? '' : []; 234 | } 235 | 236 | $last = array_pop($errors); 237 | return $onlyMsg ? $last['msg'] : $last; 238 | } 239 | 240 | /** 241 | * @param mixed|null $_stopOnError 242 | * 243 | * @return static 244 | */ 245 | public function setStopOnError(mixed $_stopOnError = null): static 246 | { 247 | if (null !== $_stopOnError) { 248 | $this->_stopOnError = (bool)$_stopOnError; 249 | } 250 | return $this; 251 | } 252 | 253 | /** 254 | * @return bool 255 | */ 256 | public function isStopOnError(): bool 257 | { 258 | return $this->_stopOnError; 259 | } 260 | 261 | /******************************************************************************* 262 | * Error Messages 263 | ******************************************************************************/ 264 | 265 | /** 266 | * @param string $key 267 | * @param array|string $message 268 | */ 269 | public function setMessage(string $key, array|string $message): void 270 | { 271 | if ($key && $message) { 272 | $this->_messages[$key] = $message; 273 | } 274 | } 275 | 276 | /** 277 | * @return array 278 | */ 279 | public function getMessages(): array 280 | { 281 | return $this->_messages; 282 | } 283 | 284 | /** 285 | * @param array $messages 286 | * 287 | * @return static 288 | */ 289 | public function setMessages(array $messages): static 290 | { 291 | foreach ($messages as $key => $value) { 292 | $this->setMessage($key, $value); 293 | } 294 | return $this; 295 | } 296 | 297 | /** 298 | * 各个验证器的提示消息 299 | * 300 | * @param callable|string $validator 验证器 301 | * @param string $field 302 | * @param array $args 303 | * @param array|string|null $message 自定义提示消息 304 | * 305 | * @return string 306 | */ 307 | public function getMessage(callable|string $validator, string $field, array $args = [], array|string $message = null): string 308 | { 309 | $rawName = is_string($validator) ? $validator : 'callback'; 310 | $params = [ 311 | '{attr}' => $this->getTranslate($field) 312 | ]; 313 | 314 | // get message from built in dict. 315 | if (!$message) { 316 | $message = $this->findMessage($field, $rawName) ?: GlobalMessage::getDefault(); 317 | // is array. It's defined multi error messages 318 | } elseif (is_array($message)) { 319 | $message = $message[$field] ?? $message[$rawName] ?? $this->findMessage($field, $rawName); 320 | 321 | if (!$message) { // use default 322 | return strtr(GlobalMessage::getDefault(), $params); 323 | } 324 | } 325 | 326 | /** @see GlobalMessage::$messages['size'] */ 327 | if (is_array($message)) { 328 | $msgKey = count($args); 329 | $message = $message[$msgKey] ?? $message[0]; 330 | } 331 | 332 | if (!str_contains($message, '{')) { 333 | return $message; 334 | } 335 | 336 | foreach ($args as $key => $value) { 337 | $key = is_int($key) ? "value$key" : $key; 338 | // build params 339 | $params['{' . $key . '}'] = is_array($value) ? implode(',', $value) : $value; 340 | } 341 | 342 | return strtr($message, $params); 343 | } 344 | 345 | /** 346 | * @param string $field 347 | * @param string $rawName 348 | * 349 | * @return string|array 350 | */ 351 | protected function findMessage(string $field, string $rawName): array|string 352 | { 353 | // allow define a message for a validator. 354 | // eg: 'username.required' => 'some message ...' 355 | $fullKey = $field . '.' . $rawName; 356 | $realName = Validators::realName($rawName); 357 | 358 | // get from default 359 | if (!$this->_messages) { 360 | return GlobalMessage::get($realName); 361 | } 362 | 363 | if (isset($this->_messages[$fullKey])) { 364 | $message = $this->_messages[$fullKey]; 365 | // eg 'required' => 'some message ...' 366 | } elseif (isset($this->_messages[$rawName])) { 367 | $message = $this->_messages[$rawName]; 368 | } elseif (isset($this->_messages[$realName])) { 369 | $message = $this->_messages[$realName]; 370 | } else { // get from default 371 | $message = GlobalMessage::get($realName); 372 | } 373 | 374 | return $message; 375 | } 376 | 377 | /** 378 | * set the attrs translation data 379 | * 380 | * @param array $fieldTrans 381 | * 382 | * @return static 383 | */ 384 | public function setTranslates(array $fieldTrans): static 385 | { 386 | return $this->addTranslates($fieldTrans); 387 | } 388 | 389 | /** 390 | * add the attrs translation data 391 | * 392 | * @param array $fieldTrans 393 | * 394 | * @return static 395 | */ 396 | public function addTranslates(array $fieldTrans): static 397 | { 398 | foreach ($fieldTrans as $field => $tran) { 399 | $this->_translates[$field] = $tran; 400 | } 401 | return $this; 402 | } 403 | 404 | /** 405 | * @return array 406 | */ 407 | public function getTranslates(): array 408 | { 409 | return $this->_translates; 410 | } 411 | 412 | /** 413 | * get field translate string. 414 | * 415 | * @param string $field 416 | * 417 | * @return string 418 | */ 419 | public function getTranslate(string $field): string 420 | { 421 | $trans = $this->getTranslates(); 422 | 423 | if (isset($trans[$field])) { 424 | return $trans[$field]; 425 | } 426 | 427 | if ($this->_prettifyName) { 428 | return Helper::prettifyFieldName($field); 429 | } 430 | 431 | return $field; 432 | } 433 | 434 | /** 435 | * @return array 436 | */ 437 | public function clearTranslates(): array 438 | { 439 | return $this->_translates = []; 440 | } 441 | 442 | /** 443 | * @return bool 444 | */ 445 | public function isPrettifyName(): bool 446 | { 447 | return $this->_prettifyName; 448 | } 449 | 450 | /** 451 | * @param bool $prettifyName 452 | */ 453 | public function setPrettifyName(bool $prettifyName = true): void 454 | { 455 | $this->_prettifyName = $prettifyName; 456 | } 457 | } 458 | -------------------------------------------------------------------------------- /src/Traits/MultipleRulesTrait.php: -------------------------------------------------------------------------------- 1 | scene; 55 | 56 | // 循环规则, 搜集当前场景可用的规则 57 | foreach ($this->getRules() as $rule) { 58 | // check field 59 | if (!isset($rule[0]) || !$rule[0]) { 60 | throw new InvalidArgumentException('Please setting the field(string) to wait validate! position: rule[0]'); 61 | } 62 | 63 | // check validators 64 | if (!isset($rule[1]) || !$rule[1]) { 65 | throw new InvalidArgumentException('Please setting the validator(s) for validate field! position: rule[1]'); 66 | } 67 | 68 | // rule only allow use to special scene. 69 | if (isset($rule['on'])) { 70 | if (!Helper::ruleIsAvailable($scene, $rule['on'])) { 71 | continue; 72 | } 73 | 74 | unset($rule['on']); 75 | } 76 | 77 | $this->_usedRules[] = $rule; 78 | // field 79 | $field = array_shift($rule); 80 | 81 | // if is a Closure 82 | if (is_object($rule[0])) { 83 | yield $field => $rule; 84 | } else { 85 | // 'required|string:5,10;' OR 'required|in:5,10' 86 | $rules = is_array($rule[0]) ? $rule[0] : array_map('trim', explode('|', $rule[0])); 87 | 88 | foreach ($rules as $aRule) { 89 | yield $field => $this->parseRule($aRule, $rule); 90 | } 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * @param string $rule 97 | * @param array $row 98 | * 99 | * @return array 100 | */ 101 | protected function parseRule(string $rule, array $row): array 102 | { 103 | $rule = trim($rule, ': '); 104 | if (!str_contains($rule, ':')) { 105 | $row[0] = $rule; 106 | return $row; 107 | } 108 | 109 | [$name, $args] = Filters::explode($rule, ':', 2); 110 | $args = trim($args, ', '); 111 | $row[0] = $name; 112 | 113 | switch ($name) { 114 | case 'in': 115 | case 'enum': 116 | case 'ontIn': 117 | $row[] = Filters::explode($args); 118 | break; 119 | 120 | case 'size': 121 | case 'range': 122 | case 'string': 123 | case 'between': 124 | if (strpos($args, ',')) { 125 | [$row['min'], $row['max']] = Filters::explode($args, ',', 2); 126 | } else { 127 | $row['min'] = $args; 128 | } 129 | break; 130 | default: 131 | $row = array_merge($row, Filters::explode($args)); 132 | break; 133 | } 134 | 135 | return $row; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Traits/NameAliasTrait.php: -------------------------------------------------------------------------------- 1 | $alias) { 57 | static::$aliases[$name] = $alias; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Traits/ScopedValidatorsTrait.php: -------------------------------------------------------------------------------- 1 | addValidator('my-validator', function($val [, $arg1, $arg2 ... ]){ 57 | * return $val === 23; 58 | * }); 59 | * $v->validate(); 60 | * ``` 61 | * 62 | * @param string $name 63 | * @param callable $callback 64 | * @param string $message 65 | * 66 | * @return AbstractValidation|ScopedValidatorsTrait|DataModel 67 | */ 68 | public function addValidator(string $name, callable $callback, string $message = ''): self 69 | { 70 | return $this->setValidator($name, $callback, $message); 71 | } 72 | 73 | /** 74 | * add a custom validator 75 | * 76 | * @param string $name 77 | * @param callable $callback 78 | * @param string $message 79 | * 80 | * @return static 81 | */ 82 | public function setValidator(string $name, callable $callback, string $message = ''): self 83 | { 84 | if ($name = trim($name)) { 85 | $this->_validators[$name] = $callback; 86 | 87 | if ($message) { 88 | $this->setMessage($name, $message); 89 | } 90 | } 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * @param string $name 97 | * 98 | * @return null|callable 99 | */ 100 | public function getValidator(string $name): ?callable 101 | { 102 | return $this->_validators[$name] ?? null; 103 | } 104 | 105 | /** 106 | * @param string $name 107 | * 108 | * @return bool 109 | */ 110 | public function hasValidator(string $name): bool 111 | { 112 | return isset($this->_validators[$name]); 113 | } 114 | 115 | /** 116 | * @param array $validators 117 | * 118 | * @return $this 119 | */ 120 | public function addValidators(array $validators): self 121 | { 122 | foreach ($validators as $name => $validator) { 123 | $this->addValidator($name, $validator); 124 | } 125 | return $this; 126 | } 127 | 128 | /** 129 | * @return array 130 | */ 131 | public function getValidators(): array 132 | { 133 | return $this->_validators; 134 | } 135 | 136 | /** 137 | * @param string $name 138 | */ 139 | public function delValidator(string $name): void 140 | { 141 | if (isset($this->_validators[$name])) { 142 | unset($this->_validators[$name]); 143 | } 144 | } 145 | 146 | /** 147 | * clear validators 148 | */ 149 | public function clearValidators(): void 150 | { 151 | $this->_validators = []; 152 | } 153 | 154 | /******************************************************************************* 155 | * fields(required*, file) validators 156 | ******************************************************************************/ 157 | 158 | /** 159 | * 验证字段必须存在,且输入数据不为空。 160 | * The verification field must exist and the input data is not empty. 161 | * 162 | * @param string $field 163 | * @param mixed|null $value 164 | * 165 | * @return bool 166 | * - True field exists and value is not empty. 167 | * - False field not exists or value is empty. 168 | * @see Validators::isEmpty() 如何鉴定为空 169 | */ 170 | public function required(string $field, mixed $value = null): bool 171 | { 172 | if (null !== $value) { 173 | $val = $value; 174 | } elseif (null === ($val = $this->getByPath($field))) { 175 | // check uploaded files 176 | if (!isset($this->uploadedFiles[$field])) { 177 | return false; 178 | } 179 | 180 | $val = $this->uploadedFiles[$field]; 181 | } 182 | 183 | return !Validators::isEmpty($val); 184 | } 185 | 186 | /** 187 | * 如果指定的另一个字段( anotherField )值等于任何一个 value 时,此字段为 必填 (refer laravel) 188 | * 189 | * returns: 190 | * - TRUE check successful 191 | * - FALSE check failed 192 | * - NULL skip check the field 193 | * 194 | * @param string $field 195 | * @param mixed $fieldVal 196 | * @param string $anotherField 197 | * @param array|string|int $values 198 | * 199 | * @return bool|null 200 | */ 201 | public function requiredIf(string $field, mixed $fieldVal, string $anotherField, array|string|int $values): ?bool 202 | { 203 | if (isset($this->data[$anotherField])) { 204 | $anotherVal = $this->data[$anotherField]; 205 | 206 | // if (in_array($anotherVal, (array)$values, true)) { 207 | if (Helper::inArray($anotherVal, (array)$values)) { 208 | return $this->required($field, $fieldVal); 209 | } 210 | } 211 | 212 | return null; 213 | } 214 | 215 | /** 216 | * 如果指定的另一个字段( anotherField )值等于任何一个 value 时,此字段为 不必填(refer laravel) 217 | * 218 | * @param string $field 219 | * @param mixed $fieldVal 220 | * @param string $anotherField 221 | * @param array|string $values 222 | * 223 | * @return bool|null 224 | * @see requiredIf() 225 | */ 226 | public function requiredUnless(string $field, mixed $fieldVal, string $anotherField, array|string $values): ?bool 227 | { 228 | if (isset($this->data[$anotherField])) { 229 | $anotherVal = $this->data[$anotherField]; 230 | 231 | // if (in_array($anotherVal, (array)$values, true)) { 232 | if (Helper::inArray($anotherVal, (array)$values)) { 233 | return null; 234 | } 235 | } 236 | 237 | return $this->required($field, $fieldVal); 238 | } 239 | 240 | /** 241 | * 如果指定的其他字段中的 任意一个 有值且不为空,则此字段为 必填(refer laravel) 242 | * 243 | * @param string $field 244 | * @param mixed $fieldVal 245 | * @param array|string $fields 246 | * 247 | * @return bool|null 248 | * @see requiredIf() 249 | */ 250 | public function requiredWith(string $field, mixed $fieldVal, array|string $fields): ?bool 251 | { 252 | foreach ((array)$fields as $name) { 253 | if ($this->required($name)) { 254 | return $this->required($field, $fieldVal); 255 | } 256 | } 257 | 258 | return null; 259 | } 260 | 261 | /** 262 | * 如果指定的 所有字段 都有值且不为空,则此字段为 必填(refer laravel) 263 | * 264 | * @param string $field 265 | * @param mixed $fieldVal 266 | * @param array|string $fields 267 | * 268 | * @return bool|null 269 | * @see requiredIf() 270 | */ 271 | public function requiredWithAll(string $field, mixed $fieldVal, array|string $fields): ?bool 272 | { 273 | $allHasValue = true; 274 | 275 | foreach ((array)$fields as $name) { 276 | if (!$this->required($name)) { 277 | $allHasValue = false; 278 | break; 279 | } 280 | } 281 | 282 | return $allHasValue ? $this->required($field, $fieldVal) : null; 283 | } 284 | 285 | /** 286 | * 如果缺少 任意一个 指定的字段值,则此字段为 必填(refer laravel) 287 | * 288 | * @param string $field 289 | * @param mixed $fieldVal 290 | * @param array|string $fields 291 | * 292 | * @return bool|null 293 | * @see requiredIf() 294 | */ 295 | public function requiredWithout(string $field, mixed $fieldVal, array|string $fields): ?bool 296 | { 297 | $allHasValue = true; 298 | 299 | foreach ((array)$fields as $name) { 300 | if (!$this->required($name)) { 301 | $allHasValue = false; 302 | break; 303 | } 304 | } 305 | 306 | return $allHasValue ? null : $this->required($field, $fieldVal); 307 | } 308 | 309 | /** 310 | * 如果所有指定的字段 都没有 值,则此字段为 必填(refer laravel) 311 | * 312 | * @param string $field 313 | * @param mixed $fieldVal 314 | * @param array|string $fields 315 | * 316 | * @return bool|null 317 | * @see requiredIf() 318 | */ 319 | public function requiredWithoutAll(string $field, mixed $fieldVal, array|string $fields): ?bool 320 | { 321 | $allNoValue = true; 322 | 323 | foreach ((array)$fields as $name) { 324 | if ($this->required($name)) { 325 | $allNoValue = false; 326 | break; 327 | } 328 | } 329 | 330 | return $allNoValue ? $this->required($field, $fieldVal) : null; 331 | } 332 | 333 | /******************************************************************************* 334 | * Files validators(require context data) 335 | ******************************************************************************/ 336 | 337 | /** 338 | * 验证的字段必须是成功上传的文件 339 | * 340 | * @param string $field 341 | * @param array|string|null $suffixes e.g ['jpg', 'jpeg', 'png', 'gif', 'bmp'] 342 | * 343 | * @return bool 344 | */ 345 | public function fileValidator(string $field, array|string $suffixes = null): bool 346 | { 347 | if (!$file = $this->uploadedFiles[$field] ?? null) { 348 | return false; 349 | } 350 | 351 | if (!isset($file['error']) || ($file['error'] !== UPLOAD_ERR_OK)) { 352 | return false; 353 | } 354 | 355 | if (!$suffixes) { 356 | return true; 357 | } 358 | 359 | if (!$suffix = trim((string)strrchr($file['name'], '.'), '.')) { 360 | return false; 361 | } 362 | 363 | $suffixes = is_string($suffixes) ? Filters::explode($suffixes) : $suffixes; 364 | 365 | return in_array(strtolower($suffix), $suffixes, true); 366 | } 367 | 368 | /** 369 | * 验证的字段必须是成功上传的图片文件 370 | * 371 | * @param string $field 372 | * @param array|string|null $suffixes e.g ['jpg', 'jpeg', 'png', 'gif', 'bmp'] 373 | * 374 | * @return bool 375 | */ 376 | public function imageValidator(string $field, array|string $suffixes = null): bool 377 | { 378 | if (!$file = $this->uploadedFiles[$field] ?? null) { 379 | return false; 380 | } 381 | 382 | if (!isset($file['error']) || ($file['error'] !== UPLOAD_ERR_OK)) { 383 | return false; 384 | } 385 | 386 | if (!$tmpFile = $file['tmp_name'] ?? null) { 387 | return false; 388 | } 389 | 390 | // getimagesize() 判定某个文件是否为图片的有效手段, 常用在文件上传验证 391 | // Note: 本函数不需要 GD 图像库 392 | // list($width, $height, $type, $attr) = getimagesize("img/flag.jpg"); 393 | $imgInfo = @getimagesize($tmpFile); 394 | 395 | if ($imgInfo && $imgInfo[2]) { 396 | $mime = strtolower($imgInfo['mime']); // 支持不标准扩展名 397 | 398 | // 是否是图片 399 | if (!in_array($mime, Helper::$imgMimeTypes, true)) { 400 | return false; 401 | } 402 | 403 | if (!$suffixes) { 404 | return true; 405 | } 406 | 407 | $suffix = Helper::getImageExtByMime($mime); 408 | $suffixes = is_string($suffixes) ? Filters::explode($suffixes) : $suffixes; 409 | 410 | return in_array($suffix, $suffixes, true); 411 | } 412 | 413 | return false; 414 | } 415 | 416 | /** 417 | * 验证的文件必须与给定 MIME 类型之一匹配 418 | * ['video', 'mimeTypes', 'video/avi,video/mpeg,video/quicktime'] 419 | * 420 | * @param string $field 421 | * @param array|string $types 422 | * 423 | * @return bool 424 | */ 425 | public function mimeTypesValidator(string $field, array|string $types): bool 426 | { 427 | if (!$file = $this->uploadedFiles[$field] ?? null) { 428 | return false; 429 | } 430 | 431 | if (!isset($file['error']) || ($file['error'] !== UPLOAD_ERR_OK)) { 432 | return false; 433 | } 434 | 435 | if (!$tmpFile = $file['tmp_name'] ?? null) { 436 | return false; 437 | } 438 | 439 | $mime = Helper::getMimeType($tmpFile); 440 | $types = is_string($types) ? Filters::explode($types) : (array)$types; 441 | 442 | return in_array($mime, $types, true); 443 | } 444 | 445 | /** 446 | * 验证的文件必须具有与列出的其中一个扩展名相对应的 MIME 类型 447 | * ['photo', 'mimes', 'jpeg,bmp,png'] 448 | * 449 | * @param string $field 450 | * @param array|string|null $types 451 | * return bool 452 | * 453 | * @todo 454 | */ 455 | public function mimesValidator(string $field, array|string $types = null): void 456 | { 457 | } 458 | 459 | /******************************************************************************* 460 | * Field compare validators 461 | ******************************************************************************/ 462 | 463 | /** 464 | * 字段值比较:当前字段值是否与给定的字段值相同 465 | * 466 | * @param mixed $val 467 | * @param string $compareField 468 | * 469 | * @return bool 470 | */ 471 | public function compareValidator(mixed $val, string $compareField): bool 472 | { 473 | return $compareField && ($val === $this->getByPath($compareField)); 474 | } 475 | 476 | public function eqFieldValidator($val, string $compareField): bool 477 | { 478 | return $this->compareValidator($val, $compareField); 479 | } 480 | 481 | /** 482 | * 字段值比较:当前字段值是否与给定的字段值不相同 483 | * 484 | * @param mixed $val 485 | * @param string $compareField 486 | * 487 | * @return bool 488 | */ 489 | public function neqFieldValidator(mixed $val, string $compareField): bool 490 | { 491 | return $compareField && ($val !== $this->getByPath($compareField)); 492 | } 493 | 494 | /** 495 | * 字段值比较:当前字段值 要小于 给定字段的值 496 | * 497 | * @param int|string $val 498 | * @param string $compareField 499 | * 500 | * @return bool 501 | */ 502 | public function ltFieldValidator(int|string $val, string $compareField): bool 503 | { 504 | $maxVal = $this->getByPath($compareField); 505 | 506 | if ($maxVal === null) { 507 | return false; 508 | } 509 | 510 | return Validators::lt($val, $maxVal); 511 | } 512 | 513 | /** 514 | * 字段值比较:当前字段值 要小于等于 给定字段的值 515 | * 516 | * @param int|string $val 517 | * @param string $compareField 518 | * 519 | * @return bool 520 | */ 521 | public function lteFieldValidator(int|string $val, string $compareField): bool 522 | { 523 | $maxVal = $this->getByPath($compareField); 524 | 525 | if ($maxVal === null) { 526 | return false; 527 | } 528 | 529 | return Validators::lte($val, $maxVal); 530 | } 531 | 532 | /** 533 | * 字段值比较:当前字段值 要大于 给定字段的值 534 | * 535 | * @param int|string $val 536 | * @param string $compareField 537 | * 538 | * @return bool 539 | */ 540 | public function gtFieldValidator(int|string $val, string $compareField): bool 541 | { 542 | $minVal = $this->getByPath($compareField); 543 | 544 | if ($minVal === null) { 545 | return false; 546 | } 547 | 548 | return Validators::gt($val, $minVal); 549 | } 550 | 551 | /** 552 | * 字段值比较:当前字段值 要大于等于 给定字段的值 553 | * 554 | * @param int|string $val 555 | * @param string $compareField 556 | * 557 | * @return bool 558 | */ 559 | public function gteFieldValidator(int|string $val, string $compareField): bool 560 | { 561 | $minVal = $this->getByPath($compareField); 562 | 563 | if ($minVal === null) { 564 | return false; 565 | } 566 | 567 | return Validators::gte($val, $minVal); 568 | } 569 | 570 | /** 571 | * 验证的 字段值 必须存在于另一个字段(anotherField)的值中。 572 | * 573 | * @param mixed $val 574 | * @param string $anotherField 575 | * 576 | * @return bool 577 | */ 578 | public function inFieldValidator(mixed $val, string $anotherField): bool 579 | { 580 | if ($anotherField && $dict = $this->getByPath($anotherField)) { 581 | return Validators::in($val, $dict); 582 | } 583 | 584 | return false; 585 | } 586 | 587 | /** 588 | * 比较两个日期字段的 间隔天数 是否符合要求 589 | * 590 | * @param string $val 591 | * @param string $compareField 592 | * @param int $expected 593 | * @param string $op 594 | * 595 | * @todo 596 | */ 597 | public function intervalDayValidator(string $val, string $compareField, int $expected, string $op = '>='): void 598 | { 599 | } 600 | 601 | /** 602 | * 对数组中的每个值都应用给定的验证器,并且要全部通过 603 | * `['foo.*.id', 'each', 'number']` 604 | * `['goods.*', 'each', 'string']` 605 | * 606 | * @param array $values 607 | * @param mixed ...$args 608 | * - string|\Closure $validator 609 | * - ... args for $validator 610 | * 611 | * @return bool 612 | * @throws InvalidArgumentException 613 | */ 614 | public function eachValidator(array $values, ...$args): bool 615 | { 616 | if (!$validator = array_shift($args)) { 617 | throw new InvalidArgumentException('must setting a validator for \'each\' validate.'); 618 | } 619 | 620 | foreach ($values as $value) { 621 | $passed = false; 622 | 623 | if (is_object($validator) && method_exists($validator, '__invoke')) { 624 | $passed = $validator($value, ...$args); 625 | } elseif (is_string($validator)) { 626 | // special for required 627 | if ('required' === $validator) { 628 | $passed = !Validators::isEmpty($value); 629 | } elseif (isset($this->_validators[$validator])) { 630 | $callback = $this->_validators[$validator]; 631 | $passed = $callback($value, ...$args); 632 | } elseif (method_exists($this, $method = $validator . 'Validator')) { 633 | $passed = $this->$method($value, ...$args); 634 | } elseif (method_exists(Validators::class, $validator)) { 635 | $passed = Validators::$validator($value, ...$args); 636 | 637 | // it is function name 638 | } elseif (function_exists($validator)) { 639 | $passed = $validator($value, ...$args); 640 | } else { 641 | throw new InvalidArgumentException("The validator [$validator] don't exists!"); 642 | } 643 | } 644 | 645 | if (!$passed) { 646 | return false; 647 | } 648 | } 649 | 650 | return true; 651 | } 652 | 653 | /******************************************************************************* 654 | * getter/setter/helper 655 | ******************************************************************************/ 656 | 657 | /** 658 | * @param string $name 659 | * 660 | * @return bool 661 | */ 662 | public static function isCheckFile(string $name): bool 663 | { 664 | return str_contains(Helper::$fileValidators, '|' . $name . '|'); 665 | } 666 | 667 | /** 668 | * @param string $name 669 | * @param array $args 670 | * @return bool 671 | */ 672 | public static function isCheckRequired(string $name, array $args = []): bool 673 | { 674 | // the $arg.0 is validator name. 675 | $name = $name === 'each' ? ((string)($args[0] ?? '')) : $name; 676 | 677 | return str_starts_with($name, 'required'); 678 | } 679 | 680 | /** 681 | * @param string $field 682 | * 683 | * @return array|null 684 | */ 685 | public function getUploadedFile(string $field): ?array 686 | { 687 | return $this->uploadedFiles[$field] ?? null; 688 | } 689 | 690 | /** 691 | * @return array 692 | */ 693 | public function getUploadedFiles(): array 694 | { 695 | return $this->uploadedFiles; 696 | } 697 | 698 | /** 699 | * @param array $uploadedFiles 700 | * 701 | * @return static 702 | */ 703 | public function setUploadedFiles(array $uploadedFiles): static 704 | { 705 | $this->uploadedFiles = $uploadedFiles; 706 | return $this; 707 | } 708 | } 709 | -------------------------------------------------------------------------------- /src/Validation.php: -------------------------------------------------------------------------------- 1 | validate(); 23 | * 24 | * $vd->isFail();// bool 25 | * $vd->firstError(); // get first error message. 26 | * $vd->isOk();// bool 27 | */ 28 | class Validation extends AbstractValidation 29 | { 30 | /* examples: 31 | public function rules() 32 | { 33 | return [ 34 | ['fields', 'validator', arg0, arg1, something ...] 35 | ['tagId,userId,name,email,freeTime', 'required'], 36 | ['userId', 'number'], 37 | ]; 38 | } 39 | */ 40 | 41 | /** 42 | * @param string $key 43 | * @param mixed|null $value 44 | * 45 | * @return mixed 46 | */ 47 | public function get(string $key, $value = null): mixed 48 | { 49 | return $this->traitGet($key, $value); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ValidationInterface.php: -------------------------------------------------------------------------------- 1 | validate($value, $data); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Validator/GlobalMessage.php: -------------------------------------------------------------------------------- 1 | [ 19 | '{attr} must be an integer!', 20 | '{attr} must be an integer and minimum value is {min}', 21 | '{attr} must be an integer and value range {min} ~ {max}', 22 | ], 23 | // 'num' 24 | 'number' => [ 25 | '{attr} must be an integer greater than 0!', 26 | '{attr} must be an integer and minimum value is {min}', 27 | '{attr} must be an integer and in the range {min} ~ {max}', 28 | ], 29 | // 'bool', 'boolean', 30 | 'boolean' => '{attr} must be an boolean!', 31 | 'float' => '{attr} must be an float!', 32 | 'url' => '{attr} must be a URL address!', 33 | 'email' => '{attr} is not a email address!', 34 | 'date' => '{attr} is not a date format!', 35 | 'dateFormat' => '{attr} is not in a {value0} date format!', 36 | 'ip' => '{attr} is not IP address!', 37 | 'ipv4' => '{attr} is not a IPv4 address!', 38 | 'ipv6' => '{attr} is not a IPv6 address!', 39 | 'required' => 'parameter {attr} is required!', 40 | 'requiredIf' => 'parameter {attr} is required!', 41 | 'length' => [ 42 | '{attr} length validation is not through!', 43 | '{attr} must be an string/array and minimum length is {min}', 44 | '{attr} must be an string/array and length range {min} ~ {max}', 45 | ], 46 | // 'range', 'between' 47 | 'size' => [ 48 | '{attr} size validation is not through!', 49 | '{attr} must be an integer/string/array and minimum value/length is {min}', 50 | // '{attr} must be an integer/string/array and value/length range {min} ~ {max}', 51 | '{attr} must be in the range {min} ~ {max}', 52 | ], 53 | 54 | // 'lengthEq', 'sizeEq' 55 | 'fixedSize' => '{attr} length must is {value0}', 56 | 57 | 'eq' => '{attr} must be equals to {value0}', 58 | // 'different' 59 | 'ne' => '{attr} cannot be equals to {value0}', 60 | 'min' => '{attr} minimum boundary is {value0}', 61 | 'max' => '{attr} maximum boundary is {value0}', 62 | 'lt' => '{attr} value must be less than {value0}', 63 | 'lte' => '{attr} value must be less than or equals to {value0}', 64 | 'gt' => '{attr} value must be greater than {value0}', 65 | 'gte' => '{attr} value must be greater than or equals to {value0}', 66 | 67 | // field compare 68 | 'eqField' => '{attr} value must be equals to {value0}', 69 | 'neqField' => '{attr} value cannot be equals to {value0}', 70 | 'ltField' => '{attr} value must be less than {value0}', 71 | 'lteField' => '{attr} value must be less than or equals to {value0}', 72 | 'gtField' => '{attr} value must be greater than {value0}', 73 | 'gteField' => '{attr} value must be greater than or equals to {value0}', 74 | 'inField' => '{attr} value must be exists in {value0}', 75 | 76 | // 'in', 'enum', 77 | 'enum' => '{attr} must in ({value0})', 78 | 'notIn' => '{attr} cannot in ({value0})', 79 | 80 | 'string' => [ 81 | '{attr} must be a string', 82 | '{attr} must be a string and minimum length be {min}', 83 | '{attr} must be a string and length range must be {min} ~ {max}', 84 | ], 85 | 86 | // 'regex', 'regexp', 87 | 'regexp' => '{attr} does not match the {value0} conditions', 88 | 89 | 'mustBe' => '{attr} must be equals to {value0}', 90 | 'notBe' => '{attr} can not be equals to {value0}', 91 | 92 | 'compare' => '{attr} must be equals to {value0}', 93 | 'same' => '{attr} must be equals to {value0}', 94 | 95 | 'isArray' => '{attr} must be an array', 96 | 'isMap' => '{attr} must be an array and is key-value format', 97 | 'isList' => '{attr} must be an array of nature', 98 | 'intList' => '{attr} must be an array and value is all integers', 99 | 'numList' => '{attr} must be an array and value is all numbers', 100 | 'strList' => '{attr} must be an array and value is all strings', 101 | 'arrList' => '{attr} must be an array and value is all arrays', 102 | 103 | 'each' => '{attr} each value must be through the "{value0}" verify', 104 | 'hasKey' => '{attr} must be contains the key {value0}', 105 | 'distinct' => 'there should not be duplicate keys in the {attr}', 106 | 107 | 'json' => '{attr} must be an json string', 108 | 109 | 'file' => '{attr} must be an uploaded file', 110 | 'image' => '{attr} must be an uploaded image file', 111 | 112 | 'callback' => '{attr} don\'t pass the test and verify!', 113 | '__default' => '{attr} validation is not through!', 114 | ]; 115 | 116 | /** 117 | * @param string $key 118 | * 119 | * @return string|array 120 | */ 121 | public static function get(string $key): array|string 122 | { 123 | return self::$messages[$key] ?? ''; 124 | } 125 | 126 | /** 127 | * @param string $key 128 | * @param array|string $msg 129 | */ 130 | public static function set(string $key, array|string $msg): void 131 | { 132 | if ($key && $msg) { 133 | self::$messages[$key] = $msg; 134 | } 135 | } 136 | 137 | /** 138 | * @param string $key 139 | * 140 | * @return bool 141 | */ 142 | public static function has(string $key): bool 143 | { 144 | return isset(self::$messages[$key]); 145 | } 146 | 147 | /** 148 | * @return string 149 | */ 150 | public static function getDefault(): string 151 | { 152 | return self::$messages['__default']; 153 | } 154 | 155 | /** 156 | * @param array $messages 157 | */ 158 | public static function setMessages(array $messages): void 159 | { 160 | foreach ($messages as $key => $value) { 161 | self::set($key, $value); 162 | } 163 | } 164 | 165 | /** 166 | * @return array 167 | */ 168 | public static function getMessages(): array 169 | { 170 | return self::$messages; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/Validator/UserValidators.php: -------------------------------------------------------------------------------- 1 | $validator) { 73 | self::set($name, $validator); 74 | } 75 | } 76 | 77 | /** 78 | * @return array 79 | */ 80 | public static function getValidators(): array 81 | { 82 | return self::$validators; 83 | } 84 | 85 | /** 86 | * @param string $name 87 | */ 88 | public static function remove(string $name): void 89 | { 90 | if (isset(self::$validators[$name])) { 91 | unset(self::$validators[$name]); 92 | } 93 | } 94 | 95 | /** 96 | * clear all validators 97 | */ 98 | public static function removeAll(): void 99 | { 100 | self::$validators = []; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Validator/ValidatorInterface.php: -------------------------------------------------------------------------------- 1 | data = $data; 26 | 27 | return $this; 28 | } 29 | 30 | public function create(): bool|int 31 | { 32 | if ($this->validate()->isFail()) { 33 | return false; 34 | } 35 | 36 | // return $this->db->insert($this->getSafeData()); 37 | return 1; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/Example/FieldExample.php: -------------------------------------------------------------------------------- 1 | ['user', 'pwd', 'code'], 33 | 'update' => ['user', 'pwd'], 34 | ]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Example/PageRequest.php: -------------------------------------------------------------------------------- 1 | 4, 'max' => 567], // 4<= tagId <=567 23 | ['title', 'min', 'min' => 40], 24 | ['freeTime', 'number', 'msg' => '{attr} is require number!'], 25 | [ 26 | 'test', 27 | 'number', 28 | 'when' => function ($data) { 29 | return isset($data['status']) && $data['status'] > 2; 30 | } 31 | ], 32 | ['userId', 'number', 'on' => 'other'], 33 | // ['userId', function($value){ return false;}], 34 | ]; 35 | } 36 | 37 | public function translates(): array 38 | { 39 | return [ 40 | 'userId' => '用户Id', 41 | ]; 42 | } 43 | 44 | /** 45 | * custom validator message 46 | * 47 | * @return array 48 | */ 49 | public function messages(): array 50 | { 51 | return [ 52 | 'required' => '{attr} 是必填项。', 53 | ]; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/Example/RuleExample.php: -------------------------------------------------------------------------------- 1 | 234, 20 | 'userId' => 'is not an integer', 21 | 'tagId' => '234535', 22 | // 'freeTime' => '1456767657', // filed not exists 23 | 'note' => '', 24 | 'name' => 'Ajohn', 25 | 'status' => 2, 26 | 'existsField' => 'test', 27 | 'passwd' => 'password', 28 | 'repasswd' => 'repassword', 29 | 'insertTime' => '1456767657', 30 | 'goods' => [ 31 | 'apple' => 34, 32 | 'pear' => 50, 33 | ], 34 | ]; 35 | 36 | public function testRuleCollectError(): void 37 | { 38 | $rv = FieldValidation::make(['name' => 'inhere'], [ 39 | [] 40 | ]); 41 | try { 42 | $rv->validate(); 43 | } catch (Throwable $e) { 44 | $this->assertSame('Please setting the field(string) to wait validate! position: rule[0]', $e->getMessage()); 45 | } 46 | 47 | $rv = FieldValidation::make(['name' => 'inhere'], [ 48 | ['name'] 49 | ]); 50 | try { 51 | $rv->validate(); 52 | } catch (Throwable $e) { 53 | $this->assertSame( 54 | 'Please setting the validator(s) for validate field! position: rule[1]', 55 | $e->getMessage() 56 | ); 57 | } 58 | } 59 | 60 | public function testValidateField(): void 61 | { 62 | $rules = [ 63 | ['freeTime', 'required'], 64 | ['userId', 'required|int'], 65 | ['tagId', 'size:0,50'], 66 | ['status', 'enum:1,2'], 67 | ['goods.pear', 'max:30'], 68 | ]; 69 | 70 | $v = FieldValidation::make($this->data, $rules); 71 | $v->setMessages([ 72 | 'freeTime.required' => 'freeTime is required!!!!' 73 | ])->validate([], false); 74 | 75 | $this->assertFalse($v->isOk()); 76 | $this->assertTrue($v->failed()); 77 | 78 | $errors = $v->getErrors(); 79 | $this->assertNotEmpty($errors); 80 | 81 | $this->assertCount(4, $errors); 82 | $this->assertSame('freeTime is required!!!!', $v->getErrors('freeTime')[0]); 83 | 84 | $v = FieldValidation::check($this->data, [ 85 | ['goods.pear', 'required|int|min:30|max:60'] 86 | ]); 87 | $this->assertTrue($v->isOk()); 88 | 89 | $v = FieldValidation::check($this->data, [ 90 | ['userId', 'required|int'], 91 | ['userId', 'min:1'], 92 | ]); 93 | 94 | $this->assertFalse($v->isOk()); 95 | $errors = $v->getErrors(); 96 | $this->assertNotEmpty($errors); 97 | $this->assertCount(1, $errors); 98 | 99 | $v = FieldValidation::check([ 100 | 'title' => 'hello', 101 | ], [ 102 | ['title', 'required|string:2,8'] 103 | ]); 104 | $this->assertTrue($v->isOk()); 105 | 106 | $v = FieldValidation::check([ 107 | 'title' => 'hello', 108 | ], [ 109 | ['title', 'required|string:1,3'] 110 | ]); 111 | $this->assertTrue($v->isFail()); 112 | $this->assertSame('title must be a string and length range must be 1 ~ 3', $v->firstError()); 113 | } 114 | 115 | public function testOnScene(): void 116 | { 117 | $data = [ 118 | 'user' => 'inhere', 119 | 'pwd' => '123456', 120 | 'code' => '1234', 121 | ]; 122 | 123 | $v = FieldValidation::make($data, [ 124 | ['user', 'required|string', 'on' => 's1'], 125 | ['code', 'required|int', 'filter' => 'int', 'on' => 's2'], 126 | ]); 127 | 128 | $v->atScene('s1')->validate(); 129 | 130 | $this->assertCount(1, $v->getUsedRules()); 131 | } 132 | 133 | public function testScenarios(): void 134 | { 135 | $data = [ 136 | 'user' => 'inhere', 137 | 'pwd' => '123456', 138 | 'code' => '1234', 139 | ]; 140 | 141 | $v = FieldExample::quick($data, 'create')->validate(); 142 | $this->assertTrue($v->isOk()); 143 | $this->assertEmpty($v->getErrors()); 144 | 145 | $data = [ 146 | 'user' => 'inhere', 147 | 'pwd' => '123456', 148 | 'code' => '12345', 149 | ]; 150 | 151 | $v = FieldExample::quick($data, 'create')->validate(); 152 | $this->assertFalse($v->isOk()); 153 | $this->assertEquals('code length must is 4', $v->firstError()); 154 | 155 | $v = FieldExample::quick($data, 'update')->validate(); 156 | $this->assertTrue($v->isOk()); 157 | $this->assertEmpty($v->getErrors()); 158 | } 159 | 160 | /** 161 | * @link https://github.com/inhere/php-validate/issues/22 162 | */ 163 | public function testIssues22(): void 164 | { 165 | $rs = [ 166 | ['id', 'required'], 167 | ['name', 'required|string:5,10', 'msg' => '5~10位的字符串'], 168 | ['sex', 'required|enum:0,1'], 169 | ['age', 'requiredIf:sex,0|int'] 170 | ]; 171 | 172 | $v = FV::check([ 173 | 'id' => 1, 174 | 'name' => '12345', 175 | 'sex' => 0, 176 | 'age' => 25, 177 | ], $rs); 178 | 179 | $this->assertTrue($v->isOk()); 180 | 181 | $v = FV::check([ 182 | 'id' => 1, 183 | 'name' => '12345', 184 | 'sex' => 1, 185 | // 'age' => 25, 186 | ], $rs); 187 | 188 | $this->assertTrue($v->isOk()); 189 | 190 | $v = FV::check([ 191 | 'id' => 1, 192 | 'name' => '12345', 193 | 'sex' => 0, 194 | // 'age' => 25, 195 | // 'age' => 'string', 196 | ], $rs); 197 | 198 | $this->assertFalse($v->isOk()); 199 | $this->assertSame('parameter age is required!', $v->firstError()); 200 | 201 | $v = FV::check([ 202 | 'id' => 1, 203 | 'name' => '12345', 204 | 'sex' => 0, 205 | 'age' => 'string', 206 | ], $rs); 207 | 208 | $this->assertFalse($v->isOk()); 209 | $this->assertSame('age must be an integer!', $v->firstError()); 210 | } 211 | 212 | /** 213 | * @link https://github.com/inhere/php-validate/issues/36 214 | */ 215 | public function testIssues36(): void 216 | { 217 | $params = []; 218 | 219 | $v = FieldValidation::check($params, [ 220 | ['owner', 'required', 'msg' => ['owner' => 'owner 缺失']], 221 | ]); 222 | 223 | $this->assertTrue($v->isFail()); 224 | //$this->assertSame('parameter owner is required!', $v->firstError()); 225 | 226 | $v = FieldValidation::check($params, [ 227 | ['owner', 'required', 'msg' => ['required' => 'owner 缺失']], 228 | ]); 229 | 230 | $this->assertTrue($v->isFail()); 231 | $this->assertSame('owner 缺失', $v->firstError()); 232 | } 233 | 234 | /** 235 | * @link https://github.com/inhere/php-validate/issues/55 236 | */ 237 | public function testIssues55(): void 238 | { 239 | $data = ['title' => '', 'name' => '']; 240 | $msg = ['title' => '标题不能为空。', 'name' => '姓名不能为空。']; 241 | 242 | $validator = \Inhere\Validate\Validation::make($data, [ 243 | ['title,name', 'required', 'msg' => $msg], 244 | ])->validate([], false); 245 | 246 | $this->assertTrue($validator->isFail()); 247 | $this->assertSame( 248 | [[ 249 | 'name' => 'title', 250 | 'msg' => $msg['title'], 251 | ], [ 252 | 'name' => 'name', 253 | 'msg' => $msg['name'], 254 | ]], 255 | $validator->getErrors() 256 | ); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /test/Filter/FiltersTest.php: -------------------------------------------------------------------------------- 1 | true, 22 | 'yes' => true, 23 | 'Yes' => true, 24 | 'YEs' => true, 25 | 'true' => true, 26 | 'True' => true, 27 | '0' => false, 28 | 'no' => false, 29 | 'off' => false, 30 | 'false' => false, 31 | 'False' => false, 32 | null => false, 33 | ]; 34 | 35 | foreach ($samples as $sample => $expected) { 36 | $this->assertSame($expected, Filters::bool($sample)); 37 | } 38 | 39 | $this->assertFalse(Filters::bool([])); 40 | } 41 | 42 | public function testAliases(): void 43 | { 44 | $this->assertTrue(Filters::hasAlias('str2list')); 45 | $this->assertSame('explode', Filters::realName('str2list')); 46 | 47 | $this->assertFalse(Filters::hasAlias('not-exist')); 48 | $this->assertSame('not-exist', Filters::realName('not-exist')); 49 | 50 | $this->assertFalse(Filters::hasAlias('new-key')); 51 | Filters::setAliases([ 52 | 'new-key' => 'new-val', 53 | ]); 54 | $this->assertTrue(Filters::hasAlias('new-key')); 55 | $this->assertSame('new-val', Filters::realName('new-key')); 56 | } 57 | 58 | public function testInteger(): void 59 | { 60 | $this->assertSame(Filters::integer('456'), 456); 61 | $this->assertSame(Filters::integer('4df5dg6'), 456); 62 | 63 | $this->assertSame(Filters::integer(['34', '67gh']), [34, 67]); 64 | } 65 | 66 | public function testAbs(): void 67 | { 68 | $this->assertSame(Filters::abs('456'), 456); 69 | $this->assertSame(Filters::abs(-45), 45); 70 | } 71 | 72 | public function testFloat(): void 73 | { 74 | // $this->assertSame(FilterList::float('4.45'), 4.45); 75 | $this->assertSame(Filters::float(45.78), 45.78); 76 | $this->assertSame(Filters::float(-45.78), -45.78); 77 | 78 | $this->assertSame(Filters::float(45.78678, 2), 45.79); 79 | $this->assertSame(Filters::float(457, 2), 457.00); 80 | } 81 | 82 | public function testString(): void 83 | { 84 | self::assertSame('1', Filters::string(1)); 85 | self::assertSame('tom', Filters::string('tom')); 86 | self::assertSame('tom', Filters::stripped('tom')); 87 | self::assertSame('abc&', Filters::string('abc&')); 88 | self::assertSame(['abc&', '1'], Filters::string(['abc&', 1])); 89 | } 90 | 91 | public function testStringFilter(): void 92 | { 93 | // quotes 94 | $this->assertSame("O\'Reilly?", Filters::quotes("O'Reilly?")); 95 | 96 | // email 97 | $this->assertSame('', Filters::email('')); 98 | 99 | // url 100 | $this->assertSame('', Filters::url('')); 101 | $this->assertSame('abc/hi', Filters::url('abc/hi')); 102 | 103 | // unsafeRaw 104 | $this->assertSame('abc/hi', Filters::unsafeRaw('abc/hi')); 105 | 106 | // specialChars 107 | $this->assertSame('<a>link</a> hello, a & b', Filters::escape('link hello, a & b')); 108 | $this->assertSame('abc', Filters::specialChars('abc')); 109 | 110 | // fullSpecialChars 111 | $this->assertSame('hello, a & b', Filters::fullSpecialChars('hello, a & b')); 112 | } 113 | 114 | public function testNl2br(): void 115 | { 116 | $this->assertSame('a
b', Filters::nl2br("a\nb")); 117 | $this->assertSame('a
b', Filters::nl2br("a\r\nb")); 118 | } 119 | 120 | public function testStringCut(): void 121 | { 122 | $this->assertSame('cDE', Filters::stringCute('abcDEFgh', 2, 3)); 123 | $this->assertSame('abcDEFgh', Filters::cut('abcDEFgh')); 124 | $this->assertSame('cDEFgh', Filters::cut('abcDEFgh', 2)); 125 | $this->assertSame('abcDEFgh', Filters::cut('abcDEFgh', 0, 12)); 126 | } 127 | 128 | public function testClearXXX(): void 129 | { 130 | // clearSpace 131 | $samples = ['abc ', ' abc ', 'a bc', 'a b c', ' a b c']; 132 | 133 | foreach ($samples as $sample) { 134 | $this->assertSame('abc', Filters::clearSpace($sample)); 135 | } 136 | 137 | // clearNewline 138 | self::assertSame('ab', Filters::clearNewline("a\nb")); 139 | self::assertSame('ab', Filters::clearNewline("a\r\nb")); 140 | } 141 | 142 | public function testTrim(): void 143 | { 144 | $this->assertEquals(Filters::trim(' test '), 'test'); 145 | $this->assertEquals(Filters::trim([' test ', 'a ']), ['test', 'a']); 146 | } 147 | 148 | public function testChangeCase(): void 149 | { 150 | // lowercase 151 | $this->assertSame('test', Filters::lowercase('Test')); 152 | $this->assertSame('23', Filters::lowercase(23)); 153 | 154 | // uppercase 155 | $this->assertSame('23', Filters::uppercase(23)); 156 | $this->assertSame(Filters::upper('test'), 'TEST'); 157 | $this->assertSame(Filters::uppercase('Test'), 'TEST'); 158 | 159 | // ucfirst 160 | $this->assertSame('Abc', Filters::ucfirst('abc')); 161 | $this->assertSame('', Filters::ucfirst('')); 162 | 163 | // ucwords 164 | $this->assertSame('Hello World', Filters::ucwords('hello world')); 165 | $this->assertSame('', Filters::ucwords('')); 166 | 167 | // snake case 168 | $this->assertSame('hello_world', Filters::snake('HelloWorld')); 169 | $this->assertSame('hello-world', Filters::snake('HelloWorld', '-')); 170 | $this->assertSame('', Filters::snake('')); 171 | 172 | // camel case 173 | $this->assertSame('helloWorld', Filters::camel('hello_world')); 174 | $this->assertSame('HelloWorld', Filters::camel('hello_world', true)); 175 | $this->assertSame('', Filters::camel('')); 176 | } 177 | 178 | public function testTime(): void 179 | { 180 | $this->assertSame(1563811200, Filters::timestamp('2019-07-23')); 181 | $this->assertSame(0, Filters::timestamp('')); 182 | $this->assertSame(0, Filters::timestamp('invalid')); 183 | } 184 | 185 | public function testStr2list(): void 186 | { 187 | $samples = [ 188 | '0,23' => ['0', '23'], 189 | 'a,b,c,' => ['a', 'b', 'c'], 190 | 'a, b ,c,' => ['a', 'b', 'c'], 191 | ' a, b , c' => ['a', 'b', 'c'], 192 | ' a,, b ,, c' => ['a', 'b', 'c'], 193 | ]; 194 | 195 | foreach ($samples as $sample => $expected) { 196 | $this->assertSame($expected, Filters::str2array($sample)); 197 | $this->assertSame($expected, Filters::explode($sample)); 198 | } 199 | 200 | $this->assertSame([], Filters::explode('')); 201 | $this->assertSame([], Filters::str2list(' , ')); 202 | } 203 | 204 | public function testUnique(): void 205 | { 206 | $this->assertSame([1], Filters::unique(1)); 207 | $this->assertSame([1], Filters::unique([1, 1])); 208 | $this->assertSame(['a', 'b'], Filters::unique(['a', 'b', 'a'])); 209 | $this->assertSame(['a', 2 => 'b'], Filters::unique(['a', 'a', 'b', 'a'])); 210 | } 211 | 212 | public function testEncodeOrClearTag(): void 213 | { 214 | // clearTags 215 | $samples = [ 216 | '' => '', 217 | '

text

' => 'text', 218 | '

text' => 'text', 219 | '

text

' => 'text', 220 | ]; 221 | foreach ($samples as $sample => $expected) { 222 | $this->assertSame($expected, Filters::clearTags($sample)); 223 | } 224 | 225 | $this->assertSame('text', Filters::clearTags('

text

', '')); 226 | 227 | // encoded 228 | $this->assertSame('abc.com%3Fa%3D7%2B9', Filters::encoded('abc.com?a=7+9')); 229 | $this->assertSame('abc.com%3Fa%3D7%2B9%26b%3D', Filters::encoded('abc.com?a=7+9&b=你', FILTER_FLAG_STRIP_HIGH)); 230 | $this->assertSame('abc.com%3Fa%3D7%2B9%26b%3D%E4%BD%A0', Filters::encoded('abc.com?a=7+9&b=你')); 231 | $this->assertSame( 232 | 'abc.com%3Fa%3D7%2B9%26b%3D%E4%BD%A0', 233 | Filters::encoded('abc.com?a=7+9&b=你', FILTER_FLAG_ENCODE_LOW) 234 | ); 235 | $this->assertSame( 236 | 'abc.com%3Fa%3D7%2B9%26b%3D%E4%BD%A0', 237 | Filters::encoded('abc.com?a=7+9&b=你', FILTER_FLAG_ENCODE_HIGH) 238 | ); 239 | 240 | // url 241 | $this->assertSame('', Filters::url('')); 242 | $this->assertSame('abc.com?a=7+9', Filters::url('abc.com?a=7+9')); 243 | $this->assertSame('abc.com?a=7+9&b=', Filters::url('abc.com?a=7+9&b=你')); 244 | 245 | // email 246 | $this->assertSame('', Filters::email('')); 247 | $this->assertSame('abc@email.com', Filters::email('abc@email.com')); 248 | } 249 | 250 | public function testCallback(): void 251 | { 252 | $this->assertSame('abc', Filters::callback('ABC', 'strtolower')); 253 | $this->assertSame('abc', Filters::callback('ABC', [Filters::class, 'lower'])); 254 | $this->assertSame('abc', Filters::callback('ABC', Filters::class . '::' . 'lower')); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /test/Filter/FiltrationTest.php: -------------------------------------------------------------------------------- 1 | ' tom ', 25 | 'status' => ' 23 ', 26 | 'word' => 'word', 27 | 'toLower' => 'WORD', 28 | 'title' => 'helloWorld', 29 | ]; 30 | 31 | public function testBasic(): void 32 | { 33 | $fl = Filtration::make($this->data); 34 | 35 | $this->assertFalse($fl->has('age')); 36 | 37 | $fl->load([ 38 | 'age' => '34', 39 | ]); 40 | 41 | $this->assertTrue($fl->has('age')); 42 | $this->assertSame(34, $fl->get('age', 'intval')); 43 | $this->assertSame(23, $fl->get('status', 'trim|int')); 44 | $this->assertNull($fl->get('not-exists')); 45 | } 46 | 47 | public function testUserFilters(): void 48 | { 49 | $fl = Filtration::make($this->data); 50 | $fl->clearFilters(); 51 | $fl->addFilters([ 52 | 'name1' => static function () { 53 | }, 54 | 'newTrim' => function ($val) { 55 | return trim($val); 56 | }, 57 | '' => static function () { 58 | }, 59 | ]); 60 | 61 | $this->assertCount(2, $fl->getFilters()); 62 | 63 | $this->assertNotEmpty($fl->getFilter('newTrim')); 64 | $this->assertEmpty($fl->getFilter('name3')); 65 | 66 | $fl->addFilter('new1', static function () { 67 | }); 68 | $this->assertNotEmpty($fl->getFilter('new1')); 69 | 70 | // use user filter 71 | $filtered = $fl->filtering([ 72 | ['name', 'newTrim'] 73 | ]); 74 | $this->assertSame('tom', $filtered['name']); 75 | 76 | $fl->delFilter('name1'); 77 | $this->assertEmpty($fl->getFilter('name1')); 78 | 79 | $fl->clearFilters(); 80 | $this->assertCount(0, $fl->getFilters()); 81 | } 82 | 83 | public function testFiltering(): void 84 | { 85 | $rules = [ 86 | ['name', 'string|trim'], 87 | ['status', 'trim|int'], 88 | ['word', 'string|trim|upper'], 89 | ['toLower', 'lower'], 90 | [ 91 | 'title', 92 | [ 93 | 'string', 94 | 'snake' => ['-'], 95 | 'ucfirst', 96 | ] 97 | ], 98 | ]; 99 | 100 | $fl = Filtration::make($this->data); 101 | $fl->setRules($rules); 102 | 103 | // get cleaned data 104 | $cleaned = $fl->filtering(); 105 | $this->assertSame('tom', $cleaned['name']); 106 | $this->assertSame(' tom ', $fl->get('name')); 107 | $this->assertSame('default', $fl->get('not-exist', null, 'default')); 108 | $this->assertSame('TOM', $fl->get('name', 'trim|upper')); 109 | 110 | $fl->reset(true); 111 | 112 | $this->assertEmpty($fl->all()); 113 | $this->assertEmpty($fl->getData()); 114 | $this->assertEmpty($fl->getRules()); 115 | } 116 | 117 | public function testUseClosure(): void 118 | { 119 | $fl = Filtration::make($this->data); 120 | $fl->setRules([ 121 | [ 122 | 'name', 123 | function ($val) { 124 | $this->assertSame(' tom ', $val); 125 | return trim($val); 126 | } 127 | ] 128 | ]); 129 | 130 | $cleaned = $fl->filtering(); 131 | $this->assertSame('tom', $cleaned['name']); 132 | } 133 | 134 | public function testCallNotExist(): void 135 | { 136 | $fl = Filtration::make($this->data); 137 | $fl->setRules([ 138 | ['name', 'not-exist-filter'] 139 | ]); 140 | 141 | $this->expectException(InvalidArgumentException::class); 142 | $fl->filtering(); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /test/Filter/UserFiltersTest.php: -------------------------------------------------------------------------------- 1 | static function () { 26 | }, 27 | 'name2' => static function () { 28 | }, 29 | '' => static function () { 30 | }, 31 | ]); 32 | 33 | $this->assertCount(2, UserFilters::getFilters()); 34 | $this->assertTrue(UserFilters::has('name1')); 35 | $this->assertFalse(UserFilters::has('')); 36 | 37 | $this->assertNotEmpty(UserFilters::get('name2')); 38 | $this->assertEmpty(UserFilters::get('name3')); 39 | 40 | UserFilters::add('new1', static function () { 41 | }); 42 | $this->assertTrue(UserFilters::has('new1')); 43 | 44 | UserFilters::remove('name1'); 45 | $this->assertFalse(UserFilters::has('name1')); 46 | 47 | UserFilters::removeAll(); 48 | $this->assertCount(0, UserFilters::getFilters()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/FiltrationTest.php: -------------------------------------------------------------------------------- 1 | ' tom ', 25 | 'status' => ' 23 ', 26 | 'word' => 'word', 27 | 'toLower' => 'WORD', 28 | 'title' => 'helloWorld', 29 | ]; 30 | 31 | $rules = [ 32 | ['name', 'string|trim'], 33 | ['status', 'trim|int'], 34 | ['word', 'string|trim|upper'], 35 | ['toLower', 'lower'], 36 | [ 37 | 'title', 38 | [ 39 | 'string', 40 | 'snake' => ['-'], 41 | 'ucfirst', 42 | ] 43 | ], 44 | ]; 45 | 46 | $cleaned = Filtration::make($data, $rules)->filtering(); 47 | 48 | $this->assertSame($cleaned['name'], 'tom'); 49 | $this->assertSame($cleaned['status'], 23); 50 | $this->assertSame($cleaned['word'], 'WORD'); 51 | $this->assertSame($cleaned['toLower'], 'word'); 52 | $this->assertSame($cleaned['title'], 'Hello-world'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/HelperTest.php: -------------------------------------------------------------------------------- 1 | assertSame(3, Helper::strPos('string', 'i')); 26 | $this->assertSame(0, Helper::strPos('string', 's')); 27 | $this->assertFalse(Helper::strPos('string', 'o')); 28 | 29 | $this->assertSame(5, Helper::strrpos('string', 'g')); 30 | $this->assertFalse(Helper::strrpos('string', 'o')); 31 | } 32 | 33 | public function testMimeHelper(): void 34 | { 35 | $this->assertSame('image/jpeg', Helper::getImageMime('jpg')); 36 | $this->assertSame('', Helper::getImageMime('invalid')); 37 | 38 | $this->assertSame('jpeg', Helper::getImageExtByMime('image/jpeg')); 39 | $this->assertSame('png', Helper::getImageExtByMime('image/png')); 40 | $this->assertSame('', Helper::getImageExtByMime('invalid')); 41 | 42 | if (extension_loaded('fileinfo')) { 43 | $mime = Helper::getMimeType(__DIR__ . '/avatar.jpeg'); 44 | $this->assertSame('image/jpeg', $mime); 45 | $this->assertSame('', Helper::getMimeType('invalid')); 46 | } 47 | } 48 | 49 | public function testCompareSize(): void 50 | { 51 | $this->assertTrue(Helper::compareSize(5, '>', 3)); 52 | 53 | // $this->assertFalse(Helper::compareSize(true, '>', false)); 54 | $this->assertFalse(Helper::compareSize(5, 'invalid', 3)); 55 | } 56 | 57 | public function testRuleIsAvailable(): void 58 | { 59 | $this->assertTrue(Helper::ruleIsAvailable('', '')); 60 | $this->assertTrue(Helper::ruleIsAvailable('create', '')); 61 | $this->assertTrue(Helper::ruleIsAvailable('create', 'create')); 62 | $this->assertTrue(Helper::ruleIsAvailable('create', 'create, update')); 63 | $this->assertFalse(Helper::ruleIsAvailable('', 'create')); 64 | $this->assertFalse(Helper::ruleIsAvailable('delete', 'create, update')); 65 | } 66 | 67 | public function testGetValueOfArray(): void 68 | { 69 | $data = [ 70 | 'user' => [ 71 | 'name' => 'inhere', 72 | 'age' => 1, 73 | ] 74 | ]; 75 | 76 | $this->assertNull(Helper::getValueOfArray($data, 'not-exist')); 77 | $this->assertSame($data, Helper::getValueOfArray($data, null)); 78 | } 79 | 80 | public function testCall(): void 81 | { 82 | // function 83 | $this->assertSame(34, Helper::call('intval', '34')); 84 | 85 | // class:;method 86 | $this->assertSame(34, Helper::call(Filters::class . '::integer', '34')); 87 | 88 | $callabled = new class { 89 | public function __invoke($str): int 90 | { 91 | return (int)$str; 92 | } 93 | }; 94 | 95 | // callabled object 96 | $this->assertSame(34, Helper::call($callabled, '34')); 97 | 98 | // invalid 99 | $this->expectException(InvalidArgumentException::class); 100 | Helper::call('oo-invalid'); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/IssuesTest.php: -------------------------------------------------------------------------------- 1 | '1960-09-21']; 15 | 16 | $v = Validation::check($post, [ 17 | ['birthDay', 'required'], 18 | ['birthDay', 'date'], 19 | ]); 20 | 21 | $this->assertTrue($v->isOk()); 22 | 23 | // must > 0 24 | $v = Validation::check($post, [ 25 | ['birthDay', 'required'], 26 | ['birthDay', 'date', true], 27 | ]); 28 | 29 | $this->assertTrue($v->isFail()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/Simple/ValidDataTest.php: -------------------------------------------------------------------------------- 1 | 23, 17 | 'num' => '23', 18 | 'str' => 'abc', 19 | 'str1' => ' abc ', 20 | 'flt' => '2.33', 21 | 'arr' => ['ab', 'cd'], 22 | 'ints' => ['23', 25], 23 | 'strs' => ['23', 'cd', 25], 24 | ]; 25 | 26 | protected function setUp(): void 27 | { 28 | VData::load($this->testData); 29 | } 30 | 31 | protected function tearDown(): void 32 | { 33 | VData::reset(); 34 | } 35 | 36 | public function testBasicOK(): void 37 | { 38 | $data = $this->testData; 39 | 40 | $this->assertSame(23, VData::getInt('int')); 41 | $this->assertSame(2.33, VData::getFloat('flt')); 42 | $this->assertSame(23.0, VData::getFloat('int')); 43 | $this->assertSame('abc', VData::getString('str')); 44 | $this->assertSame('abc', VData::getString('str1')); 45 | $this->assertSame($data['arr'], VData::getArray('arr')); 46 | $this->assertSame($data['arr'], VData::getStrings('arr')); 47 | $this->assertSame([23, 25], VData::getInts('ints')); 48 | 49 | VData::reset(); 50 | $this->assertEmpty(VData::getData()); 51 | } 52 | 53 | public function testCheckFail_int(): void 54 | { 55 | $this->assertNotEmpty(VData::getData()); 56 | 57 | $e = $this->runAndGetException(function () { 58 | VData::getInt('str'); 59 | }); 60 | $this->assertSame(ValidateException::class, get_class($e)); 61 | $this->assertSame("'str' must be int value", $e->getMessage()); 62 | 63 | $e = $this->runAndGetException(function () { 64 | VData::getInt('str', 2); 65 | }); 66 | $this->assertSame(ValidateException::class, get_class($e)); 67 | $this->assertSame("'str' must be int value and must be greater or equal to 2", $e->getMessage()); 68 | 69 | $e = $this->runAndGetException(function () { 70 | VData::getInt('str', null, 20); 71 | }); 72 | $this->assertSame(ValidateException::class, get_class($e)); 73 | $this->assertSame("'str' must be int value and must be less than or equal to 20", $e->getMessage()); 74 | 75 | $e = $this->runAndGetException(function () { 76 | VData::getInt('str', 2, 20); 77 | }); 78 | $this->assertSame(ValidateException::class, get_class($e)); 79 | $this->assertSame("'str' must be int value and must be >= 2 and <= 20", $e->getMessage()); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/Traits/ErrorMessageTraitTest.php: -------------------------------------------------------------------------------- 1 | 234, 16 | 'userId' => 'is not an integer', 17 | 'tagId' => '234535', 18 | // 'freeTime' => '1456767657', // filed not exists 19 | 'note' => '', 20 | 'name' => 'Ajohn', 21 | 'status' => 2, 22 | 'existsField' => 'test', 23 | 'passwd' => 'password', 24 | 'repasswd' => 'repassword', 25 | 'insertTime' => '1456767657', 26 | 'goods' => [ 27 | 'apple' => 34, 28 | 'pear' => 50, 29 | ], 30 | ]; 31 | 32 | public function testErrorMessage(): void 33 | { 34 | // empty test 35 | $v = FieldValidation::make($this->sampleDate); 36 | 37 | $this->assertCount(0, $v->getErrors()); 38 | $this->assertCount(0, $v->getMessages()); 39 | 40 | $this->assertSame('', $v->firstError()); 41 | $this->assertSame([], $v->firstError(false)); 42 | 43 | $v = FieldValidation::check($this->sampleDate, [ 44 | ['userId', 'required|int'], 45 | ]); 46 | 47 | $this->assertTrue($v->isPrettifyName()); 48 | $this->assertNotEmpty($v->getErrors()); 49 | $this->assertNotEmpty($v->getErrors('userId')); 50 | 51 | // firstError 52 | $this->assertSame('user id must be an integer!', $v->firstError()); 53 | $this->assertNotEmpty($error = $v->firstError(false)); 54 | $this->assertSame('userId', $error['name']); 55 | $this->assertSame('user id must be an integer!', $error['msg']); 56 | 57 | // lastError 58 | $this->assertSame('user id must be an integer!', $v->lastError()); 59 | $this->assertNotEmpty($error = $v->lastError(false)); 60 | $this->assertSame('userId', $error['name']); 61 | $this->assertSame('user id must be an integer!', $error['msg']); 62 | 63 | // reset validation 64 | $v->resetValidation(); 65 | 66 | // prettifyName 67 | $v->setPrettifyName(false); 68 | $this->assertFalse($v->isPrettifyName()); 69 | 70 | // re-validate 71 | $v->validate(); 72 | 73 | // firstError 74 | $this->assertSame('userId must be an integer!', $v->firstError()); 75 | $this->assertNotEmpty($error = $v->firstError(false)); 76 | $this->assertSame('userId', $error['name']); 77 | $this->assertSame('userId must be an integer!', $error['msg']); 78 | 79 | // lastError 80 | $this->assertSame('userId must be an integer!', $v->lastError()); 81 | $this->assertNotEmpty($error = $v->lastError(false)); 82 | $this->assertSame('userId', $error['name']); 83 | $this->assertSame('userId must be an integer!', $error['msg']); 84 | } 85 | 86 | public function testFieldTranslate(): void 87 | { 88 | $v = FieldValidation::make([]); 89 | 90 | // getTranslates 91 | $this->assertEmpty($v->getTranslates()); 92 | 93 | $v->setTranslates([ 94 | 'userId' => 'User ID', 95 | ]); 96 | 97 | $this->assertNotEmpty($v->getTranslates()); 98 | // getTranslate 99 | $this->assertSame('User ID', $v->getTranslate('userId')); 100 | $this->assertSame('not exist', $v->getTranslate('notExist')); 101 | 102 | // clearTranslates 103 | $v->clearTranslates(); 104 | $this->assertEmpty($v->getTranslates()); 105 | } 106 | 107 | /** 108 | * for https://github.com/inhere/php-validate/issues/10 109 | */ 110 | public function testForIssues10(): void 111 | { 112 | $v = Validation::check([ 113 | 'page' => 0 114 | ], [ 115 | ['page', 'integer', 'min' => 1] 116 | ]); 117 | 118 | $this->assertTrue($v->isFail()); 119 | $this->assertSame('page must be an integer and minimum value is 1', $v->firstError()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /test/Traits/ScopedValidatorsTest.php: -------------------------------------------------------------------------------- 1 | clearValidators(); 27 | $v->addValidators([ 28 | 'name1' => static function () { 29 | }, 30 | 'name2' => static function () { 31 | }, 32 | '' => static function () { 33 | }, 34 | ]); 35 | 36 | $this->assertCount(2, $v->getValidators()); 37 | $this->assertTrue($v->hasValidator('name1')); 38 | $this->assertFalse($v->hasValidator('')); 39 | 40 | $this->assertNotEmpty($v->getValidator('name2')); 41 | $this->assertEmpty($v->getValidator('name3')); 42 | 43 | $v->addValidator('name4', static function () { 44 | }); 45 | $this->assertNotEmpty($v->getValidator('name4')); 46 | 47 | $v->delValidator('name1'); 48 | $this->assertFalse($v->hasValidator('name1')); 49 | 50 | $v->clearValidators(); 51 | $this->assertCount(0, $v->getValidators()); 52 | } 53 | 54 | /** 55 | * eq, neq, lt, lte, gt, gte field compare... 56 | */ 57 | public function testFiledCompare(): void 58 | { 59 | $v = Validation::make([ 60 | 'name' => 'tom', 61 | 'age' => 34, 62 | 'tags' => ['funny', 'smile'], 63 | ]); 64 | 65 | // eq 66 | $this->assertTrue($v->eqFieldValidator(34, 'age')); 67 | $this->assertFalse($v->eqFieldValidator(334, 'age')); 68 | $this->assertFalse($v->eqFieldValidator(334, '')); 69 | 70 | // neq 71 | $this->assertTrue($v->neqFieldValidator(334, 'age')); 72 | $this->assertFalse($v->neqFieldValidator(34, 'age')); 73 | $this->assertFalse($v->neqFieldValidator(34, '')); 74 | 75 | // lt 76 | $this->assertTrue($v->ltFieldValidator(23, 'age')); 77 | $this->assertFalse($v->ltFieldValidator('23', 'age')); 78 | $this->assertFalse($v->ltFieldValidator(34, 'age')); 79 | $this->assertFalse($v->ltFieldValidator(34, '')); 80 | 81 | // lte 82 | $this->assertTrue($v->lteFieldValidator(23, 'age')); 83 | $this->assertTrue($v->lteFieldValidator(34, 'age')); 84 | $this->assertFalse($v->lteFieldValidator('34', 'age')); 85 | $this->assertFalse($v->lteFieldValidator('34', '')); 86 | 87 | // gt 88 | $this->assertTrue($v->gtFieldValidator(45, 'age')); 89 | $this->assertFalse($v->gtFieldValidator('45', 'age')); 90 | $this->assertFalse($v->gtFieldValidator(23, 'age')); 91 | $this->assertFalse($v->gtFieldValidator(23, '')); 92 | 93 | // gte 94 | $this->assertTrue($v->gteFieldValidator(43, 'age')); 95 | $this->assertTrue($v->gteFieldValidator(34, 'age')); 96 | $this->assertFalse($v->gteFieldValidator('34', 'age')); 97 | $this->assertFalse($v->gteFieldValidator('34', '')); 98 | 99 | // in 100 | $this->assertTrue($v->inFieldValidator('funny', 'tags')); 101 | $this->assertFalse($v->inFieldValidator('book', 'tags')); 102 | } 103 | 104 | public function testRequired(): void 105 | { 106 | $v = Validation::make([ 107 | 'age' => 23, 108 | 'zero' => 0, 109 | 'false' => false, 110 | 'null' => null, 111 | 'emptyStr' => '', 112 | 'emptyArr' => [], 113 | 'emptyObj' => new stdClass(), 114 | ]); 115 | 116 | $samples = [ 117 | ['age', true], 118 | ['zero', true], 119 | ['false', true], 120 | ['null', false], 121 | ['emptyStr', false], 122 | ['emptyArr', false], 123 | ['emptyObj', false], 124 | ['notExist', false], 125 | ]; 126 | 127 | foreach ($samples as $item) { 128 | $this->assertSame($item[1], $v->required($item[0])); 129 | } 130 | } 131 | 132 | public function testRequiredXXX(): void 133 | { 134 | $v = Validation::make([ 135 | 'nick' => 'tom', 136 | ]); 137 | 138 | $vs1 = ['john', 'jac']; 139 | $vs2 = ['john', 'tom']; 140 | 141 | // 如果指定的另一个字段( anotherField )值等于任何一个 value 时,此字段为 必填 (refer laravel) 142 | $ok = $v->requiredIf('name', 'inhere', 'nick', $vs2); 143 | $this->assertTrue($ok); 144 | $ok = $v->requiredIf('name', '', 'nick', $vs2); 145 | $this->assertFalse($ok); 146 | $ok = $v->requiredIf('name', '', 'nick', $vs1); 147 | $this->assertNull($ok); 148 | 149 | // 如果指定的另一个字段( anotherField )值等于任何一个 value 时,此字段为 不必填(refer laravel) 150 | $ok = $v->requiredUnless('name', '', 'nick', $vs2); 151 | $this->assertNull($ok); 152 | $ok = $v->requiredUnless('name', 'inhere', 'nick', $vs1); 153 | $this->assertTrue($ok); 154 | } 155 | 156 | public function testUploadFile(): void 157 | { 158 | $v = Validation::make([]); 159 | $v->setUploadedFiles([ 160 | 'file1' => [ 161 | 'name' => 'some.jpg', 162 | 'tmp_name' => '/tmp/some.jpg', 163 | 'error' => UPLOAD_ERR_OK, 164 | ], 165 | 'err_file' => [ 166 | 'name' => 'some.jpg', 167 | 'tmp_name' => '/tmp/some.jpg', 168 | 'error' => UPLOAD_ERR_INI_SIZE, 169 | ], 170 | 'err_suffix' => [ 171 | 'name' => 'some-no-ext', 172 | 'tmp_name' => '/tmp/some.jpg', 173 | 'error' => UPLOAD_ERR_OK, 174 | ], 175 | ]); 176 | 177 | $this->assertNotEmpty($v->getUploadedFiles()); 178 | $this->assertNotEmpty($v->getUploadedFile('file1')); 179 | 180 | $this->assertTrue($v->fileValidator('file1')); 181 | $this->assertTrue($v->fileValidator('file1', ['jpg'])); 182 | $this->assertFalse($v->fileValidator('err_suffix', ['jpg'])); 183 | $this->assertFalse($v->fileValidator('err_file')); 184 | $this->assertFalse($v->fileValidator('not-exist')); 185 | 186 | $this->assertFalse($v->imageValidator('err_file')); 187 | $this->assertFalse($v->imageValidator('not-exist')); 188 | } 189 | 190 | public function testEachValidator(): void 191 | { 192 | $v = Validation::make([ 193 | 'tags' => [3, 4, 5], 194 | 'goods' => ['apple', 'pear'], 195 | 'users' => [ 196 | ['id' => 34, 'name' => 'tom'], 197 | ['id' => 89, 'name' => 'john'], 198 | ], 199 | ]); 200 | $v->addValidator('my-validator', function () { 201 | return true; 202 | }); 203 | 204 | $tags = $v->getByPath('tags.*'); 205 | $this->assertFalse($v->eachValidator($tags, 'lt', 4)); 206 | $this->assertTrue($v->eachValidator($tags, 'gt', 2)); 207 | $this->assertTrue($v->eachValidator($tags, 'is_int')); 208 | $this->assertTrue($v->eachValidator($tags, 'my-validator')); 209 | $this->assertTrue($v->eachValidator($tags, function () { 210 | return true; 211 | })); 212 | 213 | $this->expectException(InvalidArgumentException::class); 214 | $this->assertTrue($v->eachValidator([])); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /test/ValidationTraitTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidArgumentException::class); 25 | $v->validate(); 26 | } 27 | 28 | public function testGetByPath(): void 29 | { 30 | $v = Validation::make([ 31 | 'prod' => [ 32 | 'key0' => 'val0', 33 | [ 34 | 'attr' => [ 35 | 'wid' => 1 36 | ] 37 | ], 38 | [ 39 | 'attr' => [ 40 | 'wid' => 2, 41 | ] 42 | ], 43 | [ 44 | 'attr' => [ 45 | 'wid' => 3, 46 | ] 47 | ], 48 | ], 49 | 'users' => [ 50 | ['id' => 1,], 51 | ['id' => 2,] 52 | ] 53 | ]); 54 | 55 | $val = $v->getByPath('users.*.id'); 56 | $this->assertSame([1, 2], $val); 57 | 58 | $val = $v->getByPath('prod.key0'); 59 | $this->assertSame('val0', $val); 60 | 61 | $val = $v->getByPath('prod.0.attr'); 62 | $this->assertSame(['wid' => 1], $val); 63 | 64 | $val = $v->getByPath('prod.0.attr.wid'); 65 | $this->assertSame(1, $val); 66 | 67 | $val = $v->getByPath('prod.*.attr'); 68 | $this->assertSame([['wid' => 1], ['wid' => 2], ['wid' => 3],], $val); 69 | 70 | $val = $v->getByPath('prod.*.attr.wid'); 71 | $this->assertSame([1, 2, 3], $val); 72 | } 73 | 74 | // TODO key is must exists on data. 75 | // public function testIndexedArrayGetByPath(): void 76 | // { 77 | // $v = Validation::make([ 78 | // ['attr' => ['wid' => 1]], 79 | // ['attr' => ['wid' => 2]], 80 | // ['attr' => ['wid' => 3]], 81 | // ]); 82 | // 83 | // $val = $v->GetByPath('0.attr'); 84 | // $this->assertSame(['wid' => 1], $val); 85 | // 86 | // $val = $v->getByPath('0.attr.wid'); 87 | // $this->assertSame(1, $val); 88 | // } 89 | 90 | /** 91 | * @var \array[][] see PR https://github.com/inhere/php-validate/pull/19 92 | */ 93 | public array $deepData = [ 94 | 'companies' => [ 95 | [ 96 | 'name' => 'ms', 97 | 'departments' => [ 98 | [ 99 | 'name' => '111', 100 | 'employees' => [ 101 | [ 102 | 'name' => 'aaa', 103 | 'manage' => 1, 104 | ], 105 | [ 106 | 'name' => 'bbb', 107 | 'manage' => 2, 108 | ], 109 | ], 110 | ], 111 | [ 112 | 'name' => '222', 113 | 'employees' => [ 114 | [ 115 | 'name' => 'ccc', 116 | 'manage' => 3, 117 | ], 118 | [ 119 | 'name' => 'ddd', 120 | 'manage' => 4, 121 | ], 122 | ], 123 | ], 124 | [ 125 | 'name' => '333', 126 | 'employees' => [ 127 | [ 128 | 'name' => 'eee', 129 | 'manage' => 5, 130 | ], 131 | [ 132 | 'name' => 'fff', 133 | 'manage' => 6, 134 | ], 135 | ], 136 | ], 137 | ] 138 | ], 139 | [ 140 | 'name' => 'google', 141 | 'departments' => [ 142 | [ 143 | 'name' => '444', 144 | 'employees' => [ 145 | [ 146 | 'name' => 'xxx', 147 | 'manage' => 7, 148 | ], 149 | [ 150 | 'name' => 'yyy', 151 | 'manage' => 8, 152 | ], 153 | ], 154 | ], 155 | ] 156 | ], 157 | ], 158 | ]; 159 | 160 | public function testMultidimensionalArray(): void 161 | { 162 | $v = Validation::make($this->deepData); 163 | 164 | $val = $v->getByPath('companies.*.name'); 165 | $this->assertSame(['ms', 'google'], $val); 166 | } 167 | 168 | public function testMultidimensionalArray1(): void 169 | { 170 | $v = Validation::make($this->deepData); 171 | 172 | $val = $v->getByPath('companies.0.departments.*.employees.0.manage'); 173 | $this->assertSame([1, 3, 5], $val); 174 | } 175 | 176 | public function testMultidimensionalArray2(): void 177 | { 178 | $v = Validation::make($this->deepData); 179 | 180 | $val = $v->getByPath('companies.0.departments.*.employees.*.manage'); 181 | $this->assertSame([1, 2, 3, 4, 5, 6], $val); 182 | } 183 | 184 | public function testMultidimensionalArray3(): void 185 | { 186 | $v = Validation::make($this->deepData); 187 | 188 | $val = $v->getByPath('companies.*.departments.*.employees.*.name'); 189 | $this->assertSame(['aaa', 'bbb', 'ccc', 'ddd', 'eee', 'fff', 'xxx', 'yyy'], $val); 190 | } 191 | 192 | public function testBeforeAndAfter(): void 193 | { 194 | $v = Validation::make(['name' => 'inhere'], [ 195 | ['name', 'string', 'min' => 3, 'filter' => 'trim|upper'] 196 | ]); 197 | 198 | $v->onBeforeValidate(function (Validation $v) { 199 | $this->assertSame('inhere', $v->getRaw('name')); 200 | $this->assertNull($v->getSafe('name')); 201 | 202 | return true; 203 | }); 204 | 205 | $v->onAfterValidate(function (Validation $v) { 206 | $this->assertSame('INHERE', $v->getRaw('name')); 207 | $this->assertSame('INHERE', $v->getSafe('name')); 208 | }); 209 | 210 | $v->validate(); 211 | 212 | $this->assertTrue($v->isOk()); 213 | $this->assertTrue($v->isValidated()); 214 | 215 | $v->validate(); 216 | 217 | $this->assertTrue($v->isValidated()); 218 | } 219 | 220 | public function testRuleBeforeAndAfter(): void 221 | { 222 | $v = Validation::make(['name' => 'inhere'], [ 223 | [ 224 | 'name', 225 | 'string', 226 | 'min' => 3, 227 | 'before' => function ($value) { 228 | return $value === 'inhere'; 229 | }, 230 | 'after' => function ($value) { 231 | $this->assertSame('inhere', $value); 232 | return true; 233 | } 234 | ] 235 | ]); 236 | 237 | $v->validate(); 238 | $this->assertTrue($v->isOk()); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /test/Validator/AdemoValidator.php: -------------------------------------------------------------------------------- 1 | 'val1', 21 | 'key2' => 'val2', 22 | 'key3' => '', 23 | ]); 24 | 25 | $this->assertNotEmpty(GlobalMessage::getMessages()); 26 | $this->assertSame('val1', GlobalMessage::get('key1')); 27 | $this->assertTrue(GlobalMessage::has('key1')); 28 | $this->assertFalse(GlobalMessage::has('key3')); 29 | 30 | $needle = 'validation is not through!'; 31 | 32 | if (version_compare(Version::id(), '7.0.0', '<')) { 33 | $this->assertContains($needle, GlobalMessage::getDefault()); 34 | } else { 35 | $this->assertStringContainsString($needle, GlobalMessage::getDefault()); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/Validator/Rule.php: -------------------------------------------------------------------------------- 1 | field = $field; 94 | $this->value = $value; 95 | $this->validator = $validator; 96 | $this->params = $params; 97 | $this->message = $message; 98 | $this->default = $default; 99 | 100 | return $this; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /test/Validator/UserValidatorsTest.php: -------------------------------------------------------------------------------- 1 | static function () { 26 | }, 27 | 'name2' => static function () { 28 | }, 29 | '' => static function () { 30 | }, 31 | ]); 32 | 33 | $this->assertCount(2, UserValidators::getValidators()); 34 | $this->assertTrue(UserValidators::has('name1')); 35 | $this->assertFalse(UserValidators::has('')); 36 | 37 | $this->assertNotEmpty(UserValidators::get('name2')); 38 | $this->assertEmpty(UserValidators::get('name3')); 39 | 40 | UserValidators::remove('name1'); 41 | $this->assertFalse(UserValidators::has('name1')); 42 | 43 | UserValidators::removeAll(); 44 | $this->assertCount(0, UserValidators::getValidators()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/inhere/php-validate/7db74fec9cf4fa041544e8569ee874e8529e4ad1/test/avatar.jpeg -------------------------------------------------------------------------------- /test/bootstrap.php: -------------------------------------------------------------------------------- 1 | $libDir . '/src/', 14 | 'Inhere\\ValidateTest\\' => $libDir . '/test/', 15 | ]; 16 | 17 | spl_autoload_register(static function ($class) use ($npMap) { 18 | foreach ($npMap as $np => $dir) { 19 | $file = $dir . str_replace('\\', '/', substr($class, strlen($np))) . '.php'; 20 | 21 | if (file_exists($file)) { 22 | include $file; 23 | } 24 | } 25 | }); 26 | 27 | if (is_file(dirname(__DIR__, 3) . '/autoload.php')) { 28 | require dirname(__DIR__, 3) . '/autoload.php'; 29 | } elseif (is_file(dirname(__DIR__) . '/vendor/autoload.php')) { 30 | require dirname(__DIR__) . '/vendor/autoload.php'; 31 | } --------------------------------------------------------------------------------