├── .gitignore ├── NOTICE ├── phpunit.xml ├── src ├── ValidationException.php ├── ValidationErrorDumper.php ├── ValidationPair.php ├── ValidationRule.php ├── Validator.php ├── Adapter │ └── HyperfValidator.php └── ValidationRuleset.php ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── composer.json ├── tests ├── ValidatorIPTest.php └── HyperfValidatorTest.php ├── .php-cs-fixer.php ├── README.md ├── examples └── base.php └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | KK Validation 2 | Copyright 2021 KK INTERNATIONAL HONG KONG LIMITED (https://kkgroup.cn) 3 | 4 | This product includes software developed at 5 | KK INTERNATIONAL HONG KONG LIMITED (https://kkgroup.cn). -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | tests 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/ValidationException.php: -------------------------------------------------------------------------------- 1 | errors; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/ValidationErrorDumper.php: -------------------------------------------------------------------------------- 1 | $errorSet) { 19 | $messages[] = "Attribute '{$key}' violates the following rules: " . implode('|', $errorSet); 20 | } 21 | 22 | return implode("\n", $messages); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Release 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Create Release 17 | id: create_release 18 | uses: actions/create-release@v1 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | tag_name: ${{ github.ref }} 23 | release_name: Release ${{ github.ref }} 24 | draft: false 25 | prerelease: false 26 | -------------------------------------------------------------------------------- /src/ValidationPair.php: -------------------------------------------------------------------------------- 1 | patternParts = $patternParts; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/ValidationRule.php: -------------------------------------------------------------------------------- 1 | rule = $this->name; 31 | } 32 | 33 | public static function make(string $name, Closure $closure, array $args = []): static 34 | { 35 | try { 36 | $methodName = (new ReflectionFunction($closure))->getName(); 37 | } catch (ReflectionException $exception) { 38 | throw new InvalidArgumentException('Invalid validation attribute closure'); 39 | } 40 | $hash = $args === [] ? $methodName : $methodName . ':' . serialize($args); 41 | return static::$pool[$hash] ?? (static::$pool[$hash] = new static($name, $closure, $args)); 42 | } 43 | 44 | public function setRule(string $rule): static 45 | { 46 | $this->rule = $rule; 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kkgroup/validation", 3 | "type": "library", 4 | "license": "Apache-2.0", 5 | "keywords": [ 6 | "validation", 7 | "validator", 8 | "kkgroup", 9 | "kk" 10 | ], 11 | "description": "KK validation library", 12 | "require": { 13 | "php": ">=8.0", 14 | "ext-ctype": "*", 15 | "ext-mbstring": "*" 16 | }, 17 | "require-dev": { 18 | "hyperf/server": "^3.0", 19 | "hyperf/utils": "^3.0", 20 | "hyperf/validation": "^3.0", 21 | "jchook/phpunit-assert-throws": "^1.0", 22 | "mockery/mockery": "^1.4", 23 | "phpunit/phpunit": "^9.5", 24 | "friendsofphp/php-cs-fixer": "^3.0" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "KK\\Validation\\": "src/" 29 | } 30 | }, 31 | "autoload-dev": { 32 | "psr-4": { 33 | "KKTest\\Validation\\": "tests/" 34 | } 35 | }, 36 | "minimum-stability": "dev", 37 | "prefer-stable": true, 38 | "config": { 39 | "optimize-autoloader": true, 40 | "sort-packages": true 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "0.3-dev" 45 | } 46 | }, 47 | "scripts": { 48 | "test": "phpunit -c phpunit.xml --colors=always", 49 | "cs-fix": "@php vendor/bin/php-cs-fixer fix $1" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/ValidatorIPTest.php: -------------------------------------------------------------------------------- 1 | 'required|ip', 28 | ]); 29 | $this->assertSame($data = ['ip' => '127.0.0.1'], $validator->validate($data)); 30 | $this->assertThrows(ValidationException::class, function () use ($validator) { 31 | $validator->validate(['ip' => 'xxx']); 32 | }); 33 | } 34 | 35 | public function testIPV4() 36 | { 37 | $validator = new Validator([ 38 | 'ip' => 'required|ipv4', 39 | ]); 40 | $this->assertSame($data = ['ip' => '0.0.0.0'], $validator->validate($data)); 41 | $this->assertSame($data = ['ip' => '127.0.0.1'], $validator->validate($data)); 42 | $this->assertThrows(ValidationException::class, function () use ($validator) { 43 | $validator->validate(['ip' => '::']); 44 | }); 45 | } 46 | 47 | public function testIPV6() 48 | { 49 | $validator = new Validator([ 50 | 'ip' => 'required|ipv6', 51 | ]); 52 | $this->assertSame($data = ['ip' => '::'], $validator->validate($data)); 53 | $this->assertSame($data = ['ip' => '2001:0db8:86a3:08d3:1319:8a2e:0370:7344'], $validator->validate($data)); 54 | $this->assertThrows(ValidationException::class, function () use ($validator) { 55 | $validator->validate(['ip' => '0.0.0.0']); 56 | }); 57 | $this->assertThrows(ValidationException::class, function () use ($validator) { 58 | $validator->validate(['ip' => '127.0.0.1']); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: [ push, pull_request ] 4 | 5 | env: 6 | SWOOLE_VERSION: '4.8.0' 7 | SWOW_VERSION: 'develop' 8 | 9 | jobs: 10 | swow-ci: 11 | name: Test PHP ${{ matrix.php-version }} on Swow 12 | runs-on: "${{ matrix.os }}" 13 | strategy: 14 | matrix: 15 | os: [ ubuntu-latest ] 16 | php-version: [ '8.0', '8.1', '8.2', '8.3' ] 17 | max-parallel: 4 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php-version }} 25 | tools: phpize 26 | ini-values: opcache.enable_cli=1 27 | coverage: none 28 | - name: Setup Swow 29 | if: ${{ matrix.engine == 'swow' }} 30 | run: | 31 | wget https://github.com/swow/swow/archive/"${SWOW_VERSION}".tar.gz -O swow.tar.gz 32 | mkdir -p swow 33 | tar -xf swow.tar.gz -C swow --strip-components=1 34 | rm swow.tar.gz 35 | cd swow/ext || exit 36 | 37 | phpize 38 | ./configure --enable-debug 39 | make -j "$(nproc)" 40 | sudo make install 41 | sudo sh -c "echo extension=swow > /etc/php/${{ matrix.php-version }}/cli/conf.d/swow.ini" 42 | php --ri swow 43 | - name: Setup Redis Server 44 | run: docker run --name redis --restart always -p 6379:6379 -d redis 45 | - name: Setup Swow 46 | run: composer require hyperf/engine-swow 47 | - name: Setup Packages 48 | run: composer update -o 49 | - name: Run Test Cases 50 | run: | 51 | composer test 52 | swoole-ci: 53 | name: Test PHP ${{ matrix.php-version }} on Swoole 54 | runs-on: "${{ matrix.os }}" 55 | strategy: 56 | matrix: 57 | os: [ ubuntu-latest ] 58 | php-version: [ '8.0', '8.1', '8.2', '8.3' ] 59 | max-parallel: 4 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v2 63 | - name: Setup PHP 64 | uses: shivammathur/setup-php@v2 65 | with: 66 | php-version: ${{ matrix.php-version }} 67 | tools: phpize 68 | ini-values: opcache.enable_cli=1 69 | extensions: redis, pdo, pdo_mysql, bcmath, swoole 70 | coverage: none 71 | - name: Setup Redis Server 72 | run: docker run --name redis --restart always -p 6379:6379 -d redis 73 | - name: Setup Packages 74 | run: composer update -o 75 | - name: Run Test Cases 76 | run: | 77 | composer test 78 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | setRiskyAllowed(true) 18 | ->setRules([ 19 | '@PSR2' => true, 20 | '@Symfony' => true, 21 | '@DoctrineAnnotation' => true, 22 | '@PhpCsFixer' => true, 23 | 'header_comment' => [ 24 | 'comment_type' => 'PHPDoc', 25 | 'header' => $header, 26 | 'separate' => 'none', 27 | 'location' => 'after_declare_strict', 28 | ], 29 | 'array_syntax' => [ 30 | 'syntax' => 'short', 31 | ], 32 | 'list_syntax' => [ 33 | 'syntax' => 'short', 34 | ], 35 | 'concat_space' => [ 36 | 'spacing' => 'one', 37 | ], 38 | 'blank_line_before_statement' => [ 39 | 'statements' => [ 40 | 'declare', 41 | ], 42 | ], 43 | 'general_phpdoc_annotation_remove' => [ 44 | 'annotations' => [ 45 | 'author', 46 | ], 47 | ], 48 | 'ordered_imports' => [ 49 | 'imports_order' => [ 50 | 'class', 'function', 'const', 51 | ], 52 | 'sort_algorithm' => 'alpha', 53 | ], 54 | 'single_line_comment_style' => [ 55 | 'comment_types' => [ 56 | ], 57 | ], 58 | 'yoda_style' => [ 59 | 'always_move_variable' => false, 60 | 'equal' => false, 61 | 'identical' => false, 62 | ], 63 | 'phpdoc_align' => [ 64 | 'align' => 'left', 65 | ], 66 | 'multiline_whitespace_before_semicolons' => [ 67 | 'strategy' => 'no_multi_line', 68 | ], 69 | 'constant_case' => [ 70 | 'case' => 'lower', 71 | ], 72 | 'global_namespace_import' => [ 73 | 'import_classes' => true, 74 | 'import_constants' => true, 75 | 'import_functions' => true, 76 | ], 77 | 'class_attributes_separation' => true, 78 | 'combine_consecutive_unsets' => true, 79 | 'declare_strict_types' => true, 80 | 'linebreak_after_opening_tag' => true, 81 | 'lowercase_static_reference' => true, 82 | 'no_useless_else' => true, 83 | 'no_unused_imports' => true, 84 | 'not_operator_with_successor_space' => true, 85 | 'not_operator_with_space' => false, 86 | 'ordered_class_elements' => true, 87 | 'php_unit_strict' => false, 88 | 'phpdoc_separation' => false, 89 | 'single_quote' => true, 90 | 'standardize_not_equals' => true, 91 | 'multiline_comment_opening_closing' => true, 92 | 'single_line_empty_body' => false, 93 | ]) 94 | ->setFinder( 95 | Finder::create() 96 | ->exclude('public') 97 | ->exclude('runtime') 98 | ->exclude('vendor') 99 | ->in(__DIR__) 100 | ) 101 | ->setUsingCache(false); 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 验证器 2 | 3 | ## 简介 4 | 5 | - 兼容 Hyperf/Laravel Validation 规则 6 | - 部分场景可获得约 500 倍性能提升 7 | - 验证器可多次复用不同数据,无状态设计 8 | - 规则可全局复用 9 | - 智能合并验证规则 10 | 11 | ## 安装 12 | 13 | ### 环境要求 14 | 15 | - PHP >= 8.0 16 | - mbstring 扩展 17 | - ctype 扩展 18 | 19 | ### 安装命令 20 | 21 | ```bash 22 | composer require kkgroup/validation 23 | ``` 24 | 25 | ## 使用 26 | 27 | ### 如何在 Hyperf 框架中使用 28 | 29 | 因为并没有适配所有规则,所以大表单验证中,最好还是按需使用,不要全部替换。 30 | 31 | #### 局部替换 32 | 33 | 只需要在我们的 `FormRequest` 中添加对应的 `validator` 方法,即可使用。 34 | 35 | ```php 36 | 'required|max:64', 63 | 'summary' => 'required|max:512', 64 | 'type' => 'required|in:1,2,3,4', 65 | 'data' => 'array', 66 | 'data.*.name' => 'required', 67 | 'data.*.desc' => 'required', 68 | 'data.*.type' => 'required', 69 | ]; 70 | } 71 | 72 | public function validator(ValidatorFactory $factory) 73 | { 74 | return new HyperfValidator( 75 | $factory->getTranslator(), 76 | $this->validationData(), 77 | $this->getRules(), 78 | $this->messages(), 79 | $this->attributes() 80 | ); 81 | } 82 | } 83 | 84 | ``` 85 | 86 | #### 全部替换 87 | 88 | ```php 89 | container = $container; 111 | } 112 | 113 | public function listen(): array 114 | { 115 | return [ 116 | ValidatorFactoryResolved::class, 117 | ]; 118 | } 119 | 120 | public function process(object $event) 121 | { 122 | /** @var ValidatorFactoryInterface $validatorFactory */ 123 | $validatorFactory = $event->validatorFactory; 124 | $validatorFactory->resolver(static function ($translator, $data, $rules, $messages, $customAttributes) { 125 | return new HyperfValidator($translator, $data, $rules, $messages, $customAttributes); 126 | }); 127 | } 128 | } 129 | ``` 130 | 131 | ## 待办 132 | 133 | - 暂不支持转义 `.`, `*` 等关键符 (好做但是暂时还没需求) 134 | - 规则没有全部适配 135 | - 多语言支持 (或许该库只应该实现核心部分, 其它的可以在上层做) 136 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | $ruleString) { 32 | $ruleset = ValidationRuleset::make($ruleString); 33 | $validationPairs[] = new ValidationPair( 34 | $pattern, 35 | $ruleset 36 | ); 37 | } 38 | $this->validationPairs = $validationPairs; 39 | } 40 | 41 | public function valid(array $data): array 42 | { 43 | $this->errors = []; 44 | 45 | return $this->validRecursive($data, $this->validationPairs); 46 | } 47 | 48 | /** @throws ValidationException */ 49 | public function validate(array $data): array 50 | { 51 | $result = $this->valid($data); 52 | if (! empty($this->errors)) { 53 | throw new ValidationException($this->errors); 54 | } 55 | 56 | return $result; 57 | } 58 | 59 | /** 60 | * Get last errors. 61 | * @return string[][] 62 | */ 63 | public function errors(): array 64 | { 65 | return $this->errors; 66 | } 67 | 68 | /** 69 | * @return ValidationPair[] 70 | */ 71 | public function getValidationPairs(): array 72 | { 73 | return $this->validationPairs; 74 | } 75 | 76 | /** 77 | * @param ValidationPair[] $validationPairs 78 | * @param string[] $currentDir 79 | */ 80 | protected function validRecursive(array $data, array $validationPairs, array $currentDir = []): array 81 | { 82 | $this->currentDir = $currentDir; 83 | $currentLevel = count($currentDir); 84 | 85 | $deeperValidationPairsMap = []; 86 | $wildcardValidationPairs = []; 87 | $invalid = false; 88 | 89 | /* Filter out and verify the rules that match the current level */ 90 | foreach ($validationPairs as $validationPair) { 91 | $patternParts = $validationPair->patternParts; 92 | $validationLevel = count($patternParts); 93 | $diffLevel = $validationLevel - $currentLevel - 1; 94 | if ($diffLevel !== 0) { 95 | $deeperPatternPart = $patternParts[$currentLevel]; 96 | if ($deeperPatternPart !== '*') { 97 | $deeperValidationPairsMap[$deeperPatternPart][] = $validationPair; 98 | } else { 99 | $wildcardValidationPairs[] = $validationPair; 100 | } 101 | continue; 102 | } 103 | $ruleset = $validationPair->ruleset; 104 | $currentPatternPart = $patternParts[$validationLevel - 1]; 105 | if ($currentPatternPart === '*') { 106 | foreach ($data as $key => $value) { 107 | $errors = $ruleset->check($value); 108 | if ($errors) { 109 | $this->recordErrors($key, $errors); 110 | $invalid = true; 111 | } 112 | } 113 | } else { 114 | /* Check required fields */ 115 | if (! array_key_exists($currentPatternPart, $data)) { 116 | if ($ruleset->isDefinitelyRequired()) { 117 | $this->recordError($currentPatternPart, 'required'); 118 | $invalid = true; 119 | } 120 | 121 | if ($errors = $ruleset->check(null, $data, 'required_if')) { 122 | $this->recordErrors($currentPatternPart, $errors); 123 | $invalid = true; 124 | } 125 | 126 | continue; 127 | } 128 | $value = $data[$currentPatternPart]; 129 | $errors = $ruleset->check($value, $data); 130 | if ($errors) { 131 | $this->recordErrors($currentPatternPart, $errors); 132 | $invalid = true; 133 | } 134 | } 135 | } 136 | 137 | /* go deeper first, some invalid data will be removed */ 138 | foreach ($deeperValidationPairsMap as $deeperPatternPart => $deeperValidationPairs) { 139 | $value = $data[$deeperPatternPart] ?? null; 140 | if ($value === null) { 141 | /* required but not found | not definitely required | nullable */ 142 | continue; 143 | } 144 | if (! is_array($value)) { 145 | $this->recordError($deeperPatternPart, 'array'); 146 | $invalid = true; 147 | continue; 148 | } 149 | $nextDir = $currentDir; 150 | $nextDir[] = $deeperPatternPart; 151 | $ret = $this->validRecursive($value, $deeperValidationPairs, $nextDir); 152 | if ($ret !== []) { 153 | $data[$deeperPatternPart] = $ret; 154 | } else { 155 | unset($data[$deeperPatternPart]); 156 | } 157 | } 158 | 159 | /* Apply wildcard rule after deeper check, data was cleaned up */ 160 | if ($wildcardValidationPairs !== []) { 161 | foreach ($data as $key => $value) { 162 | $nextDir = $currentDir; 163 | $nextDir[] = $key; 164 | if (! is_array($value)) { 165 | $this->recordError($key, 'array'); 166 | $invalid = true; 167 | continue; 168 | } 169 | $ret = $this->validRecursive($value, $wildcardValidationPairs, $nextDir); 170 | if ($ret !== []) { 171 | $data[$key] = $ret; 172 | } else { 173 | unset($data[$key]); 174 | } 175 | } 176 | } 177 | 178 | if ($invalid) { 179 | return []; 180 | } 181 | 182 | return $data; 183 | } 184 | 185 | protected function recordError(string $key, string $error): void 186 | { 187 | $this->errors[static::generateFullPath($this->currentDir, $key)][] = $error; 188 | } 189 | 190 | protected function recordErrors(string $key, array $errors): void 191 | { 192 | $fullPath = static::generateFullPath($this->currentDir, $key); 193 | $this->errors[$fullPath] = array_merge($this->errors[$fullPath] ?? [], $errors); 194 | } 195 | 196 | protected static function generateFullPath(array $dir, string $key): string 197 | { 198 | $path = [...$dir, $key]; 199 | return implode('.', $path); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /examples/base.php: -------------------------------------------------------------------------------- 1 | 'sometimes|required', 16 | ]); 17 | try { 18 | $validator->validate(['type' => 'foo']); 19 | } catch (ValidationException) { 20 | echo "Never here\n"; 21 | } 22 | try { 23 | $validator->validate([]); 24 | } catch (ValidationException) { 25 | echo "Never here\n"; 26 | } 27 | try { 28 | $validator->validate(['type' => '']); 29 | } catch (ValidationException $e) { 30 | // Attribute 'type' violates the following rules: required 31 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 32 | } 33 | 34 | $validator = new Validator([ 35 | 'type' => 'required', 36 | ]); 37 | try { 38 | $validator->validate([]); 39 | } catch (ValidationException $e) { 40 | // Attribute 'type' violates the following rules: required 41 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 42 | } 43 | 44 | $validator = new Validator([ 45 | 'type' => 'required|numeric', 46 | 'foo.*.bar' => 'numeric|max:256', 47 | ]); 48 | try { 49 | $validator->validate([ 50 | 'type' => 'xxx', 51 | ]); 52 | } catch (ValidationException $e) { 53 | // Attribute 'type' violates the following rules: numeric 54 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 55 | } 56 | try { 57 | $validator->validate([ 58 | 'type' => 1, 59 | 'foo' => [['bar' => '1024']], 60 | ]); 61 | } catch (ValidationException $e) { 62 | // Attribute 'foo.0.bar' violates the following rules: max:256 63 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 64 | } 65 | 66 | $validator = new Validator([ 67 | 'a.*.b.*.c.*.d.*.e' => 'numeric', 68 | ]); 69 | try { 70 | $validator->validate([ 71 | 'a' => [['b' => [['c' => [['d' => [['e' => 'xxx']]]]]]]], 72 | ]); 73 | } catch (ValidationException $e) { 74 | // Attribute 'a.0.b.0.c.0.d.0.e' violates the following rules: numeric 75 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 76 | } 77 | 78 | $validator = new Validator([ 79 | '*' => 'numeric', 80 | ]); 81 | try { 82 | $validator->validate(['0', '1', '2', '3']); 83 | } catch (ValidationException) { 84 | echo "Never here\n"; 85 | } 86 | try { 87 | $validator->validate(['0', 'x', 'y', 'z']); 88 | } catch (ValidationException $e) { 89 | // Attribute '1' violates the following rules: numeric 90 | // Attribute '2' violates the following rules: numeric 91 | // Attribute '3' violates the following rules: numeric 92 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 93 | } 94 | 95 | $validator = new Validator([ 96 | 'foo.*' => 'integer', 97 | ]); 98 | try { 99 | $validator->validate(['foo' => ['0', '0.1', '0.2', '1']]); 100 | } catch (ValidationException $e) { 101 | // Attribute 'foo.1' violates the following rules: integer 102 | // Attribute 'foo.2' violates the following rules: integer 103 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 104 | } 105 | try { 106 | $validator->validate(['foo' => 'not array']); 107 | } catch (ValidationException $e) { 108 | // Attribute 'foo' violates the following rules: array 109 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 110 | } 111 | 112 | $validator = new Validator([ 113 | '*.*.*' => 'integer', 114 | ]); 115 | try { 116 | $validator->validate(['foo' => ['bar' => 'not array']]); 117 | } catch (ValidationException $e) { 118 | // Attribute 'foo.bar' violates the following rules: array 119 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 120 | } 121 | try { 122 | $validator->validate(['foo' => ['bar' => ['baz']]]); 123 | } catch (ValidationException $e) { 124 | // Attribute 'foo.bar.0' violates the following rules: integer 125 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 126 | } 127 | try { 128 | $validator->validate(['foo' => ['bar' => ['1']]]); 129 | } catch (ValidationException) { 130 | echo "Never here\n"; 131 | } 132 | 133 | $validator = new Validator([ 134 | 'foo' => 'string|max:255', 135 | ]); 136 | try { 137 | $validator->validate(['foo' => []]); 138 | } catch (ValidationException $e) { 139 | // Attribute 'foo' violates the following rules: string 140 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 141 | } 142 | try { 143 | $validator->validate(['foo' => null]); 144 | } catch (ValidationException $e) { 145 | // Attribute 'foo' violates the following rules: string 146 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 147 | } 148 | 149 | $validator = new Validator([ 150 | 'foo' => 'required|max:255', 151 | ]); 152 | try { 153 | $validator->validate(['foo' => null]); 154 | } catch (ValidationException $e) { 155 | // Attribute 'foo' violates the following rules: required 156 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 157 | } 158 | try { 159 | $validator->validate(['foo' => 256]); 160 | } catch (ValidationException $e) { 161 | // Attribute 'foo' violates the following rules: max:255 162 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 163 | } 164 | 165 | $validator = new Validator([ 166 | 'foo' => 'max:255', 167 | ]); 168 | try { 169 | $validator->validate(['foo' => null]); 170 | } catch (ValidationException $e) { 171 | echo "Never here\n"; 172 | } 173 | 174 | $validator = new Validator([ 175 | 'foo' => 'min:1|max:255', 176 | ]); 177 | try { 178 | $validator->validate(['foo' => null]); 179 | } catch (ValidationException $e) { 180 | // Attribute 'foo' violates the following rules: min:1 181 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 182 | } 183 | try { 184 | $validator->validate(['foo' => '256.00']); 185 | } catch (ValidationException $e) { 186 | // Attribute 'foo' violates the following rules: max:255 187 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 188 | } 189 | try { 190 | $validator->validate(['foo' => []]); 191 | } catch (ValidationException $e) { 192 | // Attribute 'foo' violates the following rules: min:1 193 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 194 | } 195 | 196 | $validator = new Validator([ 197 | 'foo' => 'in:1, 2, 3', 198 | ]); 199 | try { 200 | $validator->validate(['foo' => null]); 201 | } catch (ValidationException $e) { 202 | // Attribute 'foo' violates the following rules: in:1, 2, 3 203 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 204 | } 205 | try { 206 | $validator->validate(['foo' => 1]); 207 | } catch (ValidationException $e) { 208 | echo "Never here\n"; 209 | } 210 | try { 211 | $validator->validate(['foo' => []]); 212 | } catch (ValidationException $e) { 213 | echo "Never here\n"; 214 | } 215 | try { 216 | $validator->validate(['foo' => [123]]); 217 | } catch (ValidationException $e) { 218 | // Attribute 'foo' violates the following rules: in:1, 2, 3 219 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 220 | } 221 | try { 222 | $validator->validate(['foo' => [1, '3', 2]]); 223 | } catch (ValidationException $e) { 224 | echo "Never here\n"; 225 | } 226 | try { 227 | $validator->validate(['foo' => [2, 3, 4]]); 228 | } catch (ValidationException $e) { 229 | // Attribute 'foo' violates the following rules: in:1, 2, 3 230 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 231 | } 232 | 233 | $validator = new Validator([ 234 | 'foo' => 'required|array|in:9, 8, 7, 6, 5, 4, 3', // in map 235 | ]); 236 | try { 237 | $validator->validate(['foo' => []]); 238 | } catch (ValidationException $e) { 239 | // Attribute 'foo' violates the following rules: required 240 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 241 | } 242 | try { 243 | $validator->validate(['foo' => [0, 1, 2]]); 244 | } catch (ValidationException $e) { 245 | // Attribute 'foo' violates the following rules: in:9, 8, 7, 6, 5, 4, 3 246 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 247 | } 248 | 249 | $validator = new Validator([ 250 | 'foo' => 'alpha', 251 | ]); 252 | try { 253 | $validator->validate(['foo' => '1']); 254 | } catch (ValidationException $e) { 255 | // Attribute 'foo' violates the following rules: alpha 256 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 257 | } 258 | try { 259 | $validator->validate(['foo' => 'xyz']); 260 | } catch (ValidationException $e) { 261 | echo "Never here\n"; 262 | } 263 | 264 | $validator = new Validator([ 265 | 'foo' => 'alpha_num', 266 | ]); 267 | try { 268 | $validator->validate(['foo' => 'xyz123']); 269 | } catch (ValidationException $e) { 270 | echo "Never here\n"; 271 | } 272 | try { 273 | $validator->validate(['foo' => 'xyz_123-v4']); 274 | } catch (ValidationException $e) { 275 | // Attribute 'foo' violates the following rules: alpha_num 276 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 277 | } 278 | 279 | $validator = new Validator([ 280 | 'foo' => 'alpha_dash', 281 | ]); 282 | try { 283 | $validator->validate(['foo' => 'xyz_123-v4']); 284 | } catch (ValidationException $e) { 285 | echo "Never here\n"; 286 | } 287 | try { 288 | $validator->validate(['foo' => 'xyz 123 %']); 289 | } catch (ValidationException $e) { 290 | // Attribute 'foo' violates the following rules: alpha_dash 291 | echo ValidationErrorDumper::dump($e->errors()) . "\n"; 292 | } 293 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /src/Adapter/HyperfValidator.php: -------------------------------------------------------------------------------- 1 | translator = $translator; 176 | $this->customMessages = $messages; 177 | $this->data = $data; 178 | $this->customAttributes = $customAttributes; 179 | 180 | $this->validator = new Validator($this->rules = $rules); 181 | } 182 | 183 | /** 184 | * Determine if the data passes the validation rules. 185 | */ 186 | public function passes(): bool 187 | { 188 | $this->messages = new MessageBag(); 189 | 190 | [$this->distinctValues, $this->failedRules] = [[], []]; 191 | 192 | $this->validator->valid($this->data); 193 | foreach ($this->validator->errors() as $attribute => $errors) { 194 | foreach ($errors as $error) { 195 | [$rule, $parameters] = ValidationRuleParser::parse($error); 196 | $this->addFailure($attribute, $rule, $parameters); 197 | } 198 | } 199 | 200 | // Here we will spin through all of the "after" hooks on this validator and 201 | // fire them off. This gives the callbacks a chance to perform all kinds 202 | // of other validation that needs to get wrapped up in this operation. 203 | foreach ($this->after as $after) { 204 | call_user_func($after); 205 | } 206 | 207 | return $this->messages->isEmpty(); 208 | } 209 | 210 | /** 211 | * Set the IoC container instance. 212 | */ 213 | public function setContainer(ContainerInterface $container) 214 | { 215 | $this->container = $container; 216 | } 217 | 218 | public function getMessageBag(): MessageBagContract 219 | { 220 | return $this->messages(); 221 | } 222 | 223 | public function validate(): array 224 | { 225 | if ($this->fails()) { 226 | throw new ValidationException($this); 227 | } 228 | 229 | return $this->validated(); 230 | } 231 | 232 | public function validated(): array 233 | { 234 | if ($this->invalid()) { 235 | throw new ValidationException($this); 236 | } 237 | 238 | $results = []; 239 | $missingValue = Str::random(10); 240 | 241 | foreach ($this->getRules() as $key => $rules) { 242 | // 跳过包含通配符的规则键,避免创建虚拟的["*"]键 243 | // 这些规则只用于验证,不应该影响最终的数据结构 244 | if (is_string($key) && str_contains($key, '*')) { 245 | continue; 246 | } 247 | 248 | $value = data_get($this->getData(), $key, $missingValue); 249 | 250 | if ($value !== $missingValue) { 251 | Arr::set($results, $key, $value); 252 | } 253 | } 254 | 255 | return $results; 256 | } 257 | 258 | /** 259 | * Returns the data which was invalid. 260 | */ 261 | public function invalid(): array 262 | { 263 | if (! $this->messages) { 264 | $this->passes(); 265 | } 266 | 267 | return array_intersect_key( 268 | $this->data, 269 | $this->attributesThatHaveMessages() 270 | ); 271 | } 272 | 273 | public function fails(): bool 274 | { 275 | return ! $this->passes(); 276 | } 277 | 278 | public function failed(): array 279 | { 280 | return $this->failedRules; 281 | } 282 | 283 | /** 284 | * Get the message container for the validator. 285 | * 286 | * @return MessageBag 287 | */ 288 | public function messages() 289 | { 290 | if (! $this->messages) { 291 | $this->passes(); 292 | } 293 | 294 | return $this->messages; 295 | } 296 | 297 | public function sometimes($attribute, $rules, callable $callback) 298 | { 299 | $payload = new Fluent($this->getData()); 300 | 301 | if (call_user_func($callback, $payload)) { 302 | foreach ((array) $attribute as $key) { 303 | $this->addRules([$key => $rules]); 304 | } 305 | } 306 | 307 | return $this; 308 | } 309 | 310 | public function after($callback) 311 | { 312 | $this->after[] = function () use ($callback) { 313 | return call_user_func_array($callback, [$this]); 314 | }; 315 | 316 | return $this; 317 | } 318 | 319 | public function errors(): MessageBagContract 320 | { 321 | return $this->messages(); 322 | } 323 | 324 | /** 325 | * Add a failed rule and error message to the collection. 326 | */ 327 | public function addFailure(string $attribute, string $rule, array $parameters = []) 328 | { 329 | if (! $this->messages) { 330 | $this->passes(); 331 | } 332 | 333 | $this->messages->add($attribute, $this->makeReplacements( 334 | $this->getMessage($attribute, $rule), 335 | $attribute, 336 | $rule, 337 | $parameters 338 | )); 339 | 340 | $this->failedRules[$attribute][$rule] = $parameters; 341 | } 342 | 343 | /** 344 | * Set the Presence Verifier implementation. 345 | */ 346 | public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier) 347 | { 348 | $this->presenceVerifier = $presenceVerifier; 349 | } 350 | 351 | /** 352 | * Get the Presence Verifier implementation. 353 | * 354 | * @throws RuntimeException 355 | */ 356 | public function getPresenceVerifier(): PresenceVerifierInterface 357 | { 358 | if (! isset($this->presenceVerifier)) { 359 | throw new RuntimeException('Presence verifier has not been set.'); 360 | } 361 | 362 | return $this->presenceVerifier; 363 | } 364 | 365 | /** 366 | * Determine if the given attribute has a rule in the given set. 367 | * 368 | * @param array|string $rules 369 | */ 370 | public function hasRule(string $attribute, $rules): bool 371 | { 372 | return ! is_null($this->getRule($attribute, $rules)); 373 | } 374 | 375 | /** 376 | * Get the displayable name of the attribute. 377 | */ 378 | public function getDisplayableAttribute(string $attribute): string 379 | { 380 | return $attribute; 381 | } 382 | 383 | /** 384 | * Get the data under validation. 385 | */ 386 | public function getData(): array 387 | { 388 | return $this->data; 389 | } 390 | 391 | /** 392 | * Parse the given rules and merge them into current rules. 393 | */ 394 | public function addRules(array $rules) 395 | { 396 | $rules = array_merge_recursive( 397 | $this->rules, 398 | $rules 399 | ); 400 | 401 | $this->validator = new Validator($this->rules = $rules); 402 | } 403 | 404 | /** 405 | * Get the validation rules. 406 | */ 407 | public function getRules(): array 408 | { 409 | return $this->rules; 410 | } 411 | 412 | /** 413 | * FIXME: Bad performance 414 | * Get a rule and its parameters for a given attribute. 415 | * 416 | * @param array|string $rules 417 | */ 418 | protected function getRule(string $attribute, $rules): ?array 419 | { 420 | $pairs = $this->validator->getValidationPairs(); 421 | foreach ($pairs as $pair) { 422 | if (in_array($attribute, $pair->patternParts)) { 423 | foreach ($pair->ruleset->getRules() as $rule) { 424 | [$rule, $parameters] = ValidationRuleParser::parse($rule->name); 425 | 426 | if (in_array($rule, $rules)) { 427 | return [$rule, $parameters]; 428 | } 429 | } 430 | } 431 | } 432 | 433 | return null; 434 | } 435 | 436 | /** 437 | * Get the value of a given attribute. 438 | */ 439 | protected function getValue(string $attribute) 440 | { 441 | return Arr::get($this->data, $attribute); 442 | } 443 | 444 | /** 445 | * Generate an array of all attributes that have messages. 446 | */ 447 | protected function attributesThatHaveMessages(): array 448 | { 449 | return collect($this->messages()->toArray())->map(function ($message, $key) { 450 | return explode('.', $key)[0]; 451 | })->unique()->flip()->all(); 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /tests/HyperfValidatorTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Swow/Swoole extension is unavailable'); 32 | } 33 | } 34 | 35 | protected function tearDown(): void 36 | { 37 | Mockery::close(); 38 | } 39 | 40 | public function testFails() 41 | { 42 | $validator = $this->makeValidator(['id' => 256], ['id' => 'required|max:255']); 43 | $this->assertTrue($validator->fails()); 44 | 45 | $validator = $this->makeValidator([], ['id' => 'required|max:255']); 46 | $this->assertTrue($validator->fails()); 47 | 48 | $validator = $this->makeValidator(['id' => 1], ['id' => 'required|max:255']); 49 | $this->assertFalse($validator->fails()); 50 | } 51 | 52 | public function testRequiredIf() 53 | { 54 | $validator = $this->makeValidator(['type' => 1], ['id' => 'required_if:type,1', 'type' => 'required']); 55 | $this->assertTrue($validator->fails()); 56 | } 57 | 58 | public function testMax() 59 | { 60 | $validator = $this->makeValidator(['id' => 256], ['id' => 'max:255']); 61 | $this->assertTrue($validator->fails()); 62 | } 63 | 64 | public function testMaxArray() 65 | { 66 | $validator = $this->makeValidator(['id' => [1, 2, 3]], ['id' => 'max:2']); 67 | $this->assertTrue($validator->fails()); 68 | 69 | $validator = $this->makeValidator(['id' => [1, 2, 3]], ['id' => 'max:3']); 70 | $this->assertFalse($validator->fails()); 71 | } 72 | 73 | public function testRequiredIfArray() 74 | { 75 | $validator = $this->makeValidator(['id' => 3, 'type' => 1], ['id' => 'required_if:type,1,2,3', 'type' => 'required']); 76 | $this->assertFalse($validator->fails()); 77 | 78 | $validator = $this->makeValidator(['type' => 2], ['id' => 'required_if:type,1,2,3']); 79 | $this->assertTrue($validator->fails()); 80 | } 81 | 82 | public function testRequiredIfArray2() 83 | { 84 | $validator = $this->makeValidator(['type' => 1], ['id' => 'array|required_if:type,1,2,3']); 85 | $this->assertTrue($validator->fails()); 86 | 87 | $validator = $this->makeValidator(['type' => 1, 'id' => ['1']], ['id' => 'array|required_if:type,1,2,3']); 88 | $this->assertFalse($validator->fails()); 89 | 90 | $validator = $this->makeValidator(['type' => 1, 'id' => '1'], ['id' => 'array|required_if:type,1,2,3']); 91 | $this->assertTrue($validator->fails()); 92 | 93 | $validator = $this->makeValidator(['type' => 5], ['id' => 'array|required_if:type,1,2,3']); 94 | $this->assertFalse($validator->fails()); 95 | } 96 | 97 | public function testGetMessageBag() 98 | { 99 | $data = [['id' => 256], ['id' => 'required|integer|max:255']]; 100 | $validator = $this->makeValidator(...$data); 101 | $validator2 = (new ValidatorFactory($this->getTranslator(), $this->getContainer()))->make(...$data); 102 | 103 | $this->assertEquals($validator->getMessageBag(), $validator2->getMessageBag()); 104 | } 105 | 106 | public function testValidatedAndErrors() 107 | { 108 | $data = [['id' => 200, 'name' => 'kk', 'data' => ['gender' => 1]], ['id' => 'required|integer|max:255', 'data.gender' => 'integer']]; 109 | $validator = $this->makeValidator(...$data); 110 | $validator2 = (new ValidatorFactory($this->getTranslator(), $this->getContainer()))->make(...$data); 111 | 112 | $this->assertSame($validator->validated(), $validator2->validated()); 113 | 114 | $data = [['id' => 256, 'name' => 'kk', 'data' => ['gender' => 1]], ['id' => 'required|integer|max:255', 'data.gender' => 'integer']]; 115 | 116 | try { 117 | $validator = $this->makeValidator(...$data); 118 | $validator->validated(); 119 | } catch (ValidationException $exception) { 120 | $errors = $exception->validator->errors(); 121 | } 122 | 123 | try { 124 | $validator2 = (new ValidatorFactory($this->getTranslator(), $this->getContainer()))->make(...$data); 125 | $validator2->validated(); 126 | } catch (ValidationException $exception) { 127 | $errors2 = $exception->validator->errors(); 128 | } 129 | 130 | $this->assertEquals($errors, $errors2); 131 | } 132 | 133 | public function testBoolean() 134 | { 135 | // 测试普通 boolean 验证 - 应该通过的值 136 | $validator = $this->makeValidator(['flag' => true], ['flag' => 'boolean']); 137 | $this->assertFalse($validator->fails()); 138 | 139 | $validator = $this->makeValidator(['flag' => false], ['flag' => 'boolean']); 140 | $this->assertFalse($validator->fails()); 141 | 142 | $validator = $this->makeValidator(['flag' => 1], ['flag' => 'boolean']); 143 | $this->assertFalse($validator->fails()); 144 | 145 | $validator = $this->makeValidator(['flag' => 0], ['flag' => 'boolean']); 146 | $this->assertFalse($validator->fails()); 147 | 148 | $validator = $this->makeValidator(['flag' => '1'], ['flag' => 'boolean']); 149 | $this->assertFalse($validator->fails()); 150 | 151 | $validator = $this->makeValidator(['flag' => '0'], ['flag' => 'boolean']); 152 | $this->assertFalse($validator->fails()); 153 | 154 | $validator = $this->makeValidator(['flag' => 'true'], ['flag' => 'boolean']); 155 | $this->assertFalse($validator->fails()); 156 | 157 | $validator = $this->makeValidator(['flag' => 'false'], ['flag' => 'boolean']); 158 | $this->assertFalse($validator->fails()); 159 | 160 | $validator = $this->makeValidator(['flag' => 'TRUE'], ['flag' => 'boolean']); 161 | $this->assertFalse($validator->fails()); 162 | 163 | $validator = $this->makeValidator(['flag' => 'FALSE'], ['flag' => 'boolean']); 164 | $this->assertFalse($validator->fails()); 165 | 166 | // 测试普通 boolean 验证 - 应该失败的值 167 | $validator = $this->makeValidator(['flag' => 'yes'], ['flag' => 'boolean']); 168 | $this->assertTrue($validator->fails()); 169 | 170 | $validator = $this->makeValidator(['flag' => 'no'], ['flag' => 'boolean']); 171 | $this->assertTrue($validator->fails()); 172 | 173 | $validator = $this->makeValidator(['flag' => '2'], ['flag' => 'boolean']); 174 | $this->assertTrue($validator->fails()); 175 | 176 | $validator = $this->makeValidator(['flag' => 2], ['flag' => 'boolean']); 177 | $this->assertTrue($validator->fails()); 178 | 179 | $validator = $this->makeValidator(['flag' => []], ['flag' => 'boolean']); 180 | $this->assertTrue($validator->fails()); 181 | 182 | $validator = $this->makeValidator(['flag' => 'invalid'], ['flag' => 'boolean']); 183 | $this->assertTrue($validator->fails()); 184 | } 185 | 186 | public function testBooleanStrict() 187 | { 188 | // 测试严格 boolean 验证 - 应该通过的值 189 | $validator = $this->makeValidator(['flag' => true], ['flag' => 'boolean:strict']); 190 | $this->assertFalse($validator->fails()); 191 | 192 | $validator = $this->makeValidator(['flag' => false], ['flag' => 'boolean:strict']); 193 | $this->assertFalse($validator->fails()); 194 | 195 | // 测试严格 boolean 验证 - 应该失败的值 196 | $validator = $this->makeValidator(['flag' => 1], ['flag' => 'boolean:strict']); 197 | $this->assertTrue($validator->fails()); 198 | 199 | $validator = $this->makeValidator(['flag' => 0], ['flag' => 'boolean:strict']); 200 | $this->assertTrue($validator->fails()); 201 | 202 | $validator = $this->makeValidator(['flag' => '1'], ['flag' => 'boolean:strict']); 203 | $this->assertTrue($validator->fails()); 204 | 205 | $validator = $this->makeValidator(['flag' => '0'], ['flag' => 'boolean:strict']); 206 | $this->assertTrue($validator->fails()); 207 | 208 | $validator = $this->makeValidator(['flag' => 'true'], ['flag' => 'boolean:strict']); 209 | $this->assertTrue($validator->fails()); 210 | 211 | $validator = $this->makeValidator(['flag' => 'false'], ['flag' => 'boolean:strict']); 212 | $this->assertTrue($validator->fails()); 213 | } 214 | 215 | public function testRequiredBoolean() 216 | { 217 | // 测试 required boolean 组合 - 应该通过的值 218 | $validator = $this->makeValidator(['flag' => true], ['flag' => 'required|boolean']); 219 | $this->assertFalse($validator->fails()); 220 | 221 | $validator = $this->makeValidator(['flag' => false], ['flag' => 'required|boolean']); 222 | $this->assertFalse($validator->fails()); 223 | 224 | $validator = $this->makeValidator(['flag' => 1], ['flag' => 'required|boolean']); 225 | $this->assertFalse($validator->fails()); 226 | 227 | $validator = $this->makeValidator(['flag' => 0], ['flag' => 'required|boolean']); 228 | $this->assertFalse($validator->fails()); 229 | 230 | $validator = $this->makeValidator(['flag' => '1'], ['flag' => 'required|boolean']); 231 | $this->assertFalse($validator->fails()); 232 | 233 | $validator = $this->makeValidator(['flag' => '0'], ['flag' => 'required|boolean']); 234 | $this->assertFalse($validator->fails()); 235 | 236 | // 测试 required boolean 组合 - 应该失败的值 237 | $validator = $this->makeValidator([], ['flag' => 'required|boolean']); 238 | $this->assertTrue($validator->fails()); 239 | 240 | $validator = $this->makeValidator(['flag' => null], ['flag' => 'required|boolean']); 241 | $this->assertTrue($validator->fails()); 242 | 243 | $validator = $this->makeValidator(['flag' => ''], ['flag' => 'required|boolean']); 244 | $this->assertTrue($validator->fails()); 245 | 246 | $validator = $this->makeValidator(['flag' => 'invalid'], ['flag' => 'required|boolean']); 247 | $this->assertTrue($validator->fails()); 248 | } 249 | 250 | public function testRequiredBooleanStrict() 251 | { 252 | // 测试 required boolean:strict 组合 - 应该通过的值 253 | $validator = $this->makeValidator(['flag' => true], ['flag' => 'required|boolean:strict']); 254 | $this->assertFalse($validator->fails()); 255 | 256 | $validator = $this->makeValidator(['flag' => false], ['flag' => 'required|boolean:strict']); 257 | $this->assertFalse($validator->fails()); 258 | 259 | // 测试 required boolean:strict 组合 - 应该失败的值 260 | $validator = $this->makeValidator([], ['flag' => 'required|boolean:strict']); 261 | $this->assertTrue($validator->fails()); 262 | 263 | $validator = $this->makeValidator(['flag' => null], ['flag' => 'required|boolean:strict']); 264 | $this->assertTrue($validator->fails()); 265 | 266 | $validator = $this->makeValidator(['flag' => 1], ['flag' => 'required|boolean:strict']); 267 | $this->assertTrue($validator->fails()); 268 | 269 | $validator = $this->makeValidator(['flag' => 0], ['flag' => 'required|boolean:strict']); 270 | $this->assertTrue($validator->fails()); 271 | 272 | $validator = $this->makeValidator(['flag' => '1'], ['flag' => 'required|boolean:strict']); 273 | $this->assertTrue($validator->fails()); 274 | 275 | $validator = $this->makeValidator(['flag' => 'true'], ['flag' => 'required|boolean:strict']); 276 | $this->assertTrue($validator->fails()); 277 | } 278 | 279 | public function testNullableBoolean() 280 | { 281 | // 测试 nullable boolean 组合 - null 值应该通过 282 | $validator = $this->makeValidator(['flag' => null], ['flag' => 'nullable|boolean']); 283 | $this->assertFalse($validator->fails()); 284 | 285 | // 测试 nullable boolean 组合 - 有效的 boolean 值应该通过 286 | $validator = $this->makeValidator(['flag' => true], ['flag' => 'nullable|boolean']); 287 | $this->assertFalse($validator->fails()); 288 | 289 | $validator = $this->makeValidator(['flag' => false], ['flag' => 'nullable|boolean']); 290 | $this->assertFalse($validator->fails()); 291 | 292 | $validator = $this->makeValidator(['flag' => 1], ['flag' => 'nullable|boolean']); 293 | $this->assertFalse($validator->fails()); 294 | 295 | $validator = $this->makeValidator(['flag' => '0'], ['flag' => 'nullable|boolean']); 296 | $this->assertFalse($validator->fails()); 297 | 298 | // 测试 nullable boolean 组合 - 无效值应该失败 299 | $validator = $this->makeValidator(['flag' => 'invalid'], ['flag' => 'nullable|boolean']); 300 | $this->assertTrue($validator->fails()); 301 | 302 | $validator = $this->makeValidator(['flag' => []], ['flag' => 'nullable|boolean']); 303 | $this->assertTrue($validator->fails()); 304 | } 305 | 306 | public function testNullableBooleanStrict() 307 | { 308 | // 测试 nullable boolean:strict 组合 - null 值应该通过 309 | $validator = $this->makeValidator(['flag' => null], ['flag' => 'nullable|boolean:strict']); 310 | $this->assertFalse($validator->fails()); 311 | 312 | // 测试 nullable boolean:strict 组合 - 只有真正的 boolean 值应该通过 313 | $validator = $this->makeValidator(['flag' => true], ['flag' => 'nullable|boolean:strict']); 314 | $this->assertFalse($validator->fails()); 315 | 316 | $validator = $this->makeValidator(['flag' => false], ['flag' => 'nullable|boolean:strict']); 317 | $this->assertFalse($validator->fails()); 318 | 319 | // 测试 nullable boolean:strict 组合 - 非严格 boolean 值应该失败 320 | $validator = $this->makeValidator(['flag' => 1], ['flag' => 'nullable|boolean:strict']); 321 | $this->assertTrue($validator->fails()); 322 | 323 | $validator = $this->makeValidator(['flag' => '1'], ['flag' => 'nullable|boolean:strict']); 324 | $this->assertTrue($validator->fails()); 325 | 326 | $validator = $this->makeValidator(['flag' => 'true'], ['flag' => 'nullable|boolean:strict']); 327 | $this->assertTrue($validator->fails()); 328 | } 329 | 330 | public function testArray() 331 | { 332 | $rules = [ 333 | 'info' => 'array', 334 | 'info.*.id' => 'required|integer', 335 | 'info.*.name' => 'required', 336 | ]; 337 | $data = [ 338 | 'info' => [ 339 | [ 340 | 'id' => 1, 341 | 'name' => 'kk', 342 | ], 343 | [ 344 | 'id' => 2, 345 | 'name' => 'kk2', 346 | ], 347 | ], 348 | ]; 349 | $validator = $this->makeValidator($data, $rules); 350 | $this->assertFalse($validator->fails()); 351 | $this->assertEquals($data, $validator->validated()); 352 | } 353 | 354 | public function testArray2() 355 | { 356 | $rules = [ 357 | 'info' => 'array', 358 | 'info.id' => 'required|integer', 359 | 'info.name' => 'required|string', 360 | ]; 361 | 362 | $validator = $this->makeValidator([ 363 | 'info' => [ 364 | 'id' => 1, 365 | 'name' => 'kk', 366 | ], 367 | ], $rules); 368 | $this->assertFalse($validator->fails()); 369 | $this->assertEquals([ 370 | 'info' => [ 371 | 'id' => 1, 372 | 'name' => 'kk', 373 | ], 374 | ], $validator->validated()); 375 | } 376 | 377 | public function testArray3() 378 | { 379 | $rules = [ 380 | 'info' => 'array', 381 | 'info.*.class' => 'array', 382 | 'info.*.class.*.id' => 'required|integer', 383 | 'info.*.class.*.name' => 'required|string', 384 | ]; 385 | 386 | $data = [ 387 | 'info' => [ 388 | [ 389 | 'class' => [ 390 | [ 391 | 'id' => 1, 392 | 'name' => 'kk', 393 | ], 394 | ], 395 | ], 396 | ], 397 | ]; 398 | 399 | $validator = $this->makeValidator($data, $rules); 400 | $this->assertFalse($validator->fails()); 401 | $this->assertEquals($data, $validator->validated()); 402 | } 403 | 404 | protected function makeValidator(array $data, array $rules, array $messages = [], array $customAttributes = []) 405 | { 406 | $factory = new ValidatorFactory($this->getTranslator(), $this->getContainer()); 407 | $factory->resolver(static function ($translator, $data, $rules, $messages, $customAttributes) { 408 | return new HyperfValidator($translator, $data, $rules, $messages, $customAttributes); 409 | }); 410 | return $factory->make($data, $rules, $messages, $customAttributes); 411 | } 412 | 413 | protected function getTranslator() 414 | { 415 | return new Translator(new ArrayLoader(), 'en'); 416 | } 417 | 418 | protected function getContainer() 419 | { 420 | return Mockery::mock(ContainerInterface::class); 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/ValidationRuleset.php: -------------------------------------------------------------------------------- 1 | 50, 54 | 'required_if' => 50, 55 | 'numeric' => 100, 56 | 'integer' => 100, 57 | 'string' => 100, 58 | 'array' => 100, 59 | 'boolean' => 100, 60 | 'min' => 10, 61 | 'max' => 10, 62 | 'in' => 10, 63 | 'alpha' => 10, 64 | 'alpha_num' => 10, 65 | 'alpha_dash' => 10, 66 | 'ip' => 10, 67 | 'ipv4' => 5, 68 | 'ipv6' => 5, 69 | /* rule flags */ 70 | 'sometimes' => 0, 71 | 'nullable' => 0, 72 | 'bail' => 0, 73 | ]; 74 | 75 | protected const IMPLICIT_DEPENDENCY_RULESET_MAP = [ 76 | 'alpha' => 'string', 77 | 'alpha_num' => 'string', 78 | 'alpha_dash' => 'string', 79 | 'ip' => 'string', 80 | ]; 81 | 82 | protected const TYPED_RULES = [ 83 | /* note: integer should be in front of numeric, 84 | /* because it is more specific */ 85 | 'integer', 86 | 'numeric', 87 | 'string', 88 | 'array', 89 | 'boolean', 90 | ]; 91 | 92 | protected static array $stringToBoolMap = [ 93 | '1' => true, '0' => false, 94 | 'true' => true, 'false' => false, 95 | ]; 96 | 97 | /** @var int base flags */ 98 | protected int $flags; 99 | 100 | /** @var ValidationRule[] */ 101 | protected array $rules; 102 | 103 | /** @var static[] */ 104 | protected static array $pool = []; 105 | 106 | /** @var Closure[] */ 107 | protected static array $closureCache = []; 108 | 109 | protected function __construct(array $ruleMap) 110 | { 111 | $flags = 0; 112 | $rules = []; 113 | 114 | foreach ($ruleMap as $rule => $ruleArgs) { 115 | switch ($rule) { 116 | case 'sometimes': 117 | $flags |= static::FLAG_SOMETIMES; 118 | break; 119 | case 'required': 120 | $flags |= static::FLAG_REQUIRED; 121 | if (! isset($ruleMap['numeric']) && ! isset($ruleMap['integer'])) { 122 | $rules[] = ValidationRule::make('required', static::getClosure('validateRequired' . static::fetchTypedRule($ruleMap))); 123 | } 124 | break; 125 | case 'required_if': 126 | if (count($ruleArgs) < 2) { 127 | throw new InvalidArgumentException("Rule '{$rule}' must have at least 2 parameters"); 128 | } 129 | // 第一个参数是字段名,后续所有参数都是要匹配的值 130 | $fieldName = $ruleArgs[0]; 131 | $matchingValues = array_slice($ruleArgs, 1); // 获取所有要匹配的值 132 | $name = "{$rule}:{$fieldName}," . implode(',', $matchingValues); 133 | $closureArgs = [$fieldName, $matchingValues, 'validateRequired' . static::fetchTypedRule($ruleMap)]; 134 | $rules[] = ValidationRule::make($name, static::getClosure('validateRequiredIf'), $closureArgs)->setRule($rule); 135 | break; 136 | case 'nullable': 137 | $flags |= static::FLAG_NULLABLE; 138 | break; 139 | case 'numeric': 140 | if (isset($ruleMap['array'])) { 141 | throw new InvalidArgumentException("Rule 'numeric' conflicts with 'array'"); 142 | } 143 | $rules[] = ValidationRule::make('numeric', static::getClosure('validateNumeric')); 144 | break; 145 | case 'integer': 146 | $rules[] = ValidationRule::make('integer', static::getClosure('validateInteger')); 147 | break; 148 | case 'string': 149 | if (isset($ruleMap['array'])) { 150 | throw new InvalidArgumentException("Rule 'string' conflicts with 'array'"); 151 | } 152 | $rules[] = ValidationRule::make('string', static::getClosure('validateString')); 153 | break; 154 | case 'array': 155 | $rules[] = ValidationRule::make('array', static::getClosure('validateArray')); 156 | break; 157 | case 'min': 158 | case 'max': 159 | if (count($ruleArgs) !== 1) { 160 | throw new InvalidArgumentException("Rule '{$rule}' require 1 parameter"); 161 | } 162 | if (! is_numeric($ruleArgs[0])) { 163 | throw new InvalidArgumentException("Rule '{$rule}' require numeric parameters"); 164 | } 165 | $ruleArgs[0] += 0; 166 | $name = "{$rule}:{$ruleArgs[0]}"; 167 | $methodPart = $rule === 'min' ? 'Min' : 'Max'; 168 | $rules[] = ValidationRule::make($name, static::getClosure("validate{$methodPart}" . static::fetchTypedRule($ruleMap)), $ruleArgs); 169 | break; 170 | case 'in': 171 | if (count($ruleArgs) === 0) { 172 | throw new InvalidArgumentException("Rule '{$rule}' require 1 parameter at least"); 173 | } 174 | $name = static::implodeFullRuleName($rule, $ruleArgs); 175 | $suffix = isset($ruleMap['array']) ? 'Array' : ''; 176 | if (count($ruleArgs) <= 5) { 177 | $rules[] = ValidationRule::make($name, static::getClosure('validateInList' . $suffix), [$ruleArgs]); 178 | } else { 179 | $ruleArgsMap = []; 180 | foreach ($ruleArgs as $ruleArg) { 181 | $ruleArgsMap[$ruleArg] = true; 182 | } 183 | $rules[] = ValidationRule::make($name, static::getClosure('validateInMap' . $suffix), [$ruleArgsMap]); 184 | } 185 | break; 186 | case 'alpha': 187 | case 'alpha_num': 188 | case 'alpha_dash': 189 | $rules[] = ValidationRule::make($rule, static::getClosure('validate' . static::upperCamelize($rule))); 190 | break; 191 | case 'ip': 192 | case 'ipv4': 193 | case 'ipv6': 194 | $rules[] = ValidationRule::make($rule, static::getClosure('validate' . strtoupper($rule))); 195 | break; 196 | case 'boolean': 197 | if (count($ruleArgs) === 0) { 198 | $rules[] = ValidationRule::make('boolean', static::getClosure('validateBoolean')); 199 | } elseif (count($ruleArgs) === 1 && $ruleArgs[0] === 'strict') { 200 | $rules[] = ValidationRule::make('boolean:strict', static::getClosure('validateBooleanStrict')); 201 | } else { 202 | throw new InvalidArgumentException("Rule 'boolean' only supports 'strict' parameter"); 203 | } 204 | break; 205 | case 'bail': 206 | /* compatibility */ 207 | break; 208 | default: 209 | throw new InvalidArgumentException("Unknown rule '{$rule}'"); 210 | } 211 | } 212 | 213 | $this->flags = $flags; 214 | $this->rules = $rules; 215 | } 216 | 217 | public static function make(string $ruleString): static 218 | { 219 | $ruleMap = static::convertRuleStringToRuleMap($ruleString); 220 | $hash = static::getHashOfRuleMap($ruleMap); 221 | return static::$pool[$hash] ?? (static::$pool[$hash] = new static($ruleMap)); 222 | } 223 | 224 | public function isDefinitelyRequired(): bool 225 | { 226 | return ($this->flags & static::FLAG_REQUIRED) && ! ($this->flags & static::FLAG_SOMETIMES); 227 | } 228 | 229 | /** 230 | * @return ValidationRule[] 231 | */ 232 | public function getRules(): array 233 | { 234 | return $this->rules; 235 | } 236 | 237 | /** 238 | * @return string[] Error attribute names 239 | */ 240 | public function check(mixed $data, array $attributes = [], ?string $ruleName = null): array 241 | { 242 | if (($this->flags & static::FLAG_NULLABLE) && $data === null) { 243 | return []; 244 | } 245 | 246 | $errors = []; 247 | 248 | foreach ($this->rules as $rule) { 249 | if ($ruleName && $ruleName !== $rule->rule) { 250 | continue; 251 | } 252 | 253 | $closure = $rule->closure; 254 | $valid = $closure($data, $attributes, ...$rule->args); 255 | if (! $valid) { 256 | $errors[] = $rule->name; 257 | /* Always bail here, for example: 258 | * if we have a rule like `integer|max:255`, 259 | * then user input a string `x`, it's not even an integer, 260 | * continue to check its' length is meaningless, 261 | * in Laravel validation, it may violate `max:255` when 262 | * string length is longer than 255, how fool it is. */ 263 | break; 264 | } 265 | } 266 | 267 | return $errors; 268 | } 269 | 270 | public static function validateAlpha(string $value, array $attributes): bool 271 | { 272 | return preg_match('/^[\pL\pM]+$/u', $value); 273 | } 274 | 275 | public static function validateAlphaNum(string $value, array $attributes): bool 276 | { 277 | return preg_match('/^[\pL\pM\pN]+$/u', $value) > 0; 278 | } 279 | 280 | public static function validateAlphaDash(string $value, array $attributes): bool 281 | { 282 | return preg_match('/^[\pL\pM\pN_-]+$/u', $value) > 0; 283 | } 284 | 285 | public static function validateIP(string $value, array $attributes): bool 286 | { 287 | return filter_var($value, FILTER_VALIDATE_IP) !== false; 288 | } 289 | 290 | public static function validateIPV4(string $value, array $attributes): bool 291 | { 292 | return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false; 293 | } 294 | 295 | public static function validateIPV6(string $value, array $attributes): bool 296 | { 297 | return filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false; 298 | } 299 | 300 | protected static function getHashOfRuleMap(array $ruleMap): string 301 | { 302 | $hashSlots = []; 303 | ksort($ruleMap); 304 | foreach ($ruleMap as $rule => $ruleArgs) { 305 | if ($ruleArgs === []) { 306 | $hashSlots[] = $rule; 307 | } else { 308 | $hashSlots[] = sprintf('%s:%s', $rule, implode(', ', $ruleArgs)); 309 | } 310 | } 311 | return implode('|', $hashSlots); 312 | } 313 | 314 | protected static function convertRuleStringToRuleMap(string $ruleString, bool $solvePriority = true): array 315 | { 316 | $rules = array_map('trim', explode('|', $ruleString)); 317 | 318 | $tmpRuleMap = []; 319 | foreach ($rules as $rule) { 320 | $ruleParts = explode(':', $rule, 2); 321 | $rule = strtolower(trim($ruleParts[0])); 322 | if (! isset(static::PRIORITY_MAP[$rule])) { 323 | throw new InvalidArgumentException("Unknown rule '{$rule}'"); 324 | } 325 | if (($ruleParts[1] ?? '') !== '') { 326 | $ruleArgs = array_map('trim', explode(',', $ruleParts[1])); 327 | } else { 328 | $ruleArgs = []; 329 | } 330 | $tmpRuleMap[$rule] = $ruleArgs; 331 | } 332 | foreach ($tmpRuleMap as $rule => $_) { 333 | $implicitDependencyRuleset = static::IMPLICIT_DEPENDENCY_RULESET_MAP[$rule] ?? null; 334 | if ($implicitDependencyRuleset === null) { 335 | continue; 336 | } 337 | $extraRuleMap = static::convertRuleStringToRuleMap($implicitDependencyRuleset, false); 338 | foreach ($extraRuleMap as $extraRule => $extraRuleArgs) { 339 | if (! isset($tmpRuleMap[$extraRule])) { 340 | $tmpRuleMap[$extraRule] = $extraRuleArgs; 341 | } 342 | } 343 | } 344 | 345 | if (! $solvePriority) { 346 | return $tmpRuleMap; 347 | } 348 | 349 | $ruleQueue = new SplPriorityQueue(); 350 | foreach ($tmpRuleMap as $rule => $ruleArgs) { 351 | $ruleQueue->insert([$rule, $ruleArgs], static::PRIORITY_MAP[$rule]); 352 | } 353 | $ruleMap = []; 354 | while (! $ruleQueue->isEmpty()) { 355 | [$rule, $ruleArgs] = $ruleQueue->extract(); 356 | if (isset($ruleMap[$rule])) { 357 | throw new InvalidArgumentException("Duplicated rule '{$rule}' in ruleset '{$ruleString}'"); 358 | } 359 | $ruleMap[$rule] = $ruleArgs; 360 | } 361 | 362 | return $ruleMap; 363 | } 364 | 365 | protected static function upperCamelize(string $uncamelized_words, string $separator = '_'): string 366 | { 367 | $uncamelized_words = str_replace($separator, ' ', strtolower($uncamelized_words)); 368 | return ltrim(str_replace(' ', '', ucwords($uncamelized_words)), $separator); 369 | } 370 | 371 | protected static function fetchTypedRule(array $ruleMap): string 372 | { 373 | foreach (static::TYPED_RULES as $typedRule) { 374 | if (isset($ruleMap[$typedRule])) { 375 | return static::upperCamelize($typedRule); 376 | } 377 | } 378 | 379 | return ''; 380 | } 381 | 382 | protected static function implodeFullRuleName(string $rule, array $ruleArgs): string 383 | { 384 | if (count($ruleArgs) === 0) { 385 | return $rule; 386 | } 387 | return $rule . ':' . implode(',', $ruleArgs); 388 | } 389 | 390 | protected static function getClosure(string $method): Closure 391 | { 392 | return static::$closureCache[$method] 393 | ?? (static::$closureCache[$method] = Closure::fromCallable([static::class, $method])); 394 | } 395 | 396 | protected static function validateRequired(mixed $value, array $attributes): bool 397 | { 398 | if (is_null($value)) { 399 | return false; 400 | } 401 | if (is_string($value) && ($value === '' || ctype_space($value))) { 402 | return false; 403 | } 404 | if (is_countable($value) && count($value) === 0) { 405 | return false; 406 | } 407 | if ($value instanceof SplFileInfo) { 408 | return $value->getPath() !== ''; 409 | } 410 | 411 | return true; 412 | } 413 | 414 | protected static function validateRequiredIf(mixed $value, array $attributes, string $key, array $keyValues, string $validator): bool 415 | { 416 | if (array_key_exists($key, $attributes)) { 417 | // 检查字段值是否匹配任何一个指定的值 418 | foreach ($keyValues as $keyValue) { 419 | if ($attributes[$key] == $keyValue) { 420 | if ($value === null) { 421 | return false; 422 | } 423 | return self::$validator($value, $attributes); 424 | } 425 | } 426 | } 427 | 428 | return true; 429 | } 430 | 431 | protected static function validateRequiredString(mixed $value, array $attributes): bool 432 | { 433 | return $value !== '' && ! ctype_space($value); 434 | } 435 | 436 | protected static function validateRequiredArray(array $value, array $attributes): bool 437 | { 438 | return count($value) !== 0; 439 | } 440 | 441 | protected static function validateRequiredFile(SplFileInfo $value, array $attributes): bool 442 | { 443 | return $value->getPath() !== ''; 444 | } 445 | 446 | protected static function validateNumeric(mixed &$value, array $attributes): bool 447 | { 448 | if (! is_numeric($value)) { 449 | return false; 450 | } 451 | $value += 0; 452 | return true; 453 | } 454 | 455 | protected static function validateInteger(mixed &$value, array $attributes): bool 456 | { 457 | if (filter_var($value, FILTER_VALIDATE_INT) === false) { 458 | return false; 459 | } 460 | $value = (int) $value; 461 | return true; 462 | } 463 | 464 | protected static function validateString(mixed $value, array $attributes): bool 465 | { 466 | return is_string($value); 467 | } 468 | 469 | protected static function validateArray(mixed $value, array $attributes): bool 470 | { 471 | return is_array($value); 472 | } 473 | 474 | protected static function getLength(mixed $value, array $attributes): int 475 | { 476 | if (is_numeric($value)) { 477 | return $value + 0; 478 | } 479 | if (is_string($value)) { 480 | return mb_strlen($value); 481 | } 482 | if (is_array($value)) { 483 | return count($value); 484 | } 485 | if ($value === null) { 486 | return 0; 487 | } 488 | if ($value instanceof SplFileInfo) { 489 | return $value->getSize(); 490 | } 491 | 492 | return mb_strlen((string) $value); 493 | } 494 | 495 | protected static function validateMin(mixed $value, array $attributes, float|int $min): bool 496 | { 497 | // TODO: file min support b, kb, mb, gb ... 498 | return static::getLength($value, $attributes) >= $min; 499 | } 500 | 501 | protected static function validateMax(mixed $value, array $attributes, float|int $max): bool 502 | { 503 | // TODO: file max support b, kb, mb, gb ... 504 | return static::getLength($value, $attributes) <= $max; 505 | } 506 | 507 | protected static function validateMinInteger(int $value, array $attributes, float|int $min): bool 508 | { 509 | return $value >= $min; 510 | } 511 | 512 | protected static function validateMaxInteger(int $value, array $attributes, float|int $max): bool 513 | { 514 | return $value <= $max; 515 | } 516 | 517 | protected static function validateMinNumeric(float|int $value, array $attributes, float|int $min): bool 518 | { 519 | return $value >= $min; 520 | } 521 | 522 | protected static function validateMaxNumeric(float|int $value, array $attributes, float|int $max): bool 523 | { 524 | return $value <= $max; 525 | } 526 | 527 | protected static function validateMinString(string $value, array $attributes, float|int $min): bool 528 | { 529 | return mb_strlen($value) >= $min; 530 | } 531 | 532 | protected static function validateMaxString(string $value, array $attributes, float|int $max): bool 533 | { 534 | return mb_strlen($value) <= $max; 535 | } 536 | 537 | protected static function validateMinArray(array $value, array $attributes, float|int $min): bool 538 | { 539 | return count($value) >= $min; 540 | } 541 | 542 | protected static function validateMaxArray(array $value, array $attributes, float|int $max): bool 543 | { 544 | return count($value) <= $max; 545 | } 546 | 547 | #[Pure] 548 | protected static function validateInList(mixed $value, array $attributes, array $list): bool 549 | { 550 | if (! is_array($value)) { 551 | return in_array((string) $value, $list, true); 552 | } 553 | return static::validateInListArray($value, $list); 554 | } 555 | 556 | protected static function validateInListArray(array $value, array $attributes, array $list): bool 557 | { 558 | foreach ($value as $item) { 559 | if (is_array($item)) { 560 | return false; 561 | } 562 | if (! in_array((string) $item, $list, true)) { 563 | return false; 564 | } 565 | } 566 | return true; 567 | } 568 | 569 | #[Pure] 570 | protected static function validateInMap(mixed $value, array $attributes, array $map): bool 571 | { 572 | if (! is_array($value)) { 573 | return $map[(string) $value] ?? false; 574 | } 575 | return static::validateInMapArray($value, $map); 576 | } 577 | 578 | protected static function validateInMapArray(array $value, array $attributes, array $map): bool 579 | { 580 | foreach ($value as $item) { 581 | if (is_array($item)) { 582 | return false; 583 | } 584 | if (! isset($map[(string) $item])) { 585 | return false; 586 | } 587 | } 588 | return true; 589 | } 590 | 591 | protected static function validateBoolean(mixed $value, array $attributes): bool 592 | { 593 | return match (true) { 594 | is_bool($value), $value === 1, $value === 0 => true, 595 | is_string($value) => isset(static::$stringToBoolMap[strtolower($value)]), 596 | default => false, 597 | }; 598 | } 599 | 600 | protected static function validateBooleanStrict(mixed $value, array $attributes): bool 601 | { 602 | return is_bool($value); 603 | } 604 | 605 | protected static function validateRequiredBoolean(mixed $value, array $attributes): bool 606 | { 607 | return self::validateBoolean($value, $attributes); 608 | } 609 | } 610 | --------------------------------------------------------------------------------