.gitignore000064400000000230151665713370006545 0ustar00/.idea /.vscode /vendor /build *.log .env /tests/tmp /tests/.phpunit.result.cache .phpunit.result.cache syntax: glob !cfg/*.schema.php /.trae .DS_Store README.md000064400000024160151665713370006044 0ustar00# Awesome Validation 一个基于 PHP 8.1 的轻量级参数验证库,提供友好的错误消息和流畅的 API 设计。 ## 特性 - 🚀 基于 PHP 8.1+ 语法,使用现代 PHP 特性 - 📝 遵循 PSR-4 自动加载规范 - 🔗 流畅的链式调用 API - 💬 友好的中文错误消息 - 🎯 支持多种验证规则 - 🛡️ 支持自定义验证规则 - 📦 支持数组数据验证 - 🌳 支持嵌套字段验证 - ⚡ 支持异常抛出模式 ## 安装 ```bash composer require awesome/validation ``` ## 快速开始 ### 基础验证 ```php use Awesome\Validation\Validator; // 字符串验证 $result = Validator::string() ->minLength(5) ->maxLength(20) ->validate('hello world'); if ($result->isValid()) { echo "验证通过"; } else { print_r($result->errors()->all()); } // 邮箱验证 $result = Validator::email()->validate('test@example.com'); // 整数验证 $result = Validator::integer()->validate('25'); // 必填验证 $result = Validator::required()->validate('value'); ``` ### 数组数据验证 ```php $data = [ 'username' => 'john_doe', 'email' => 'john@example.com', 'age' => '25' ]; $rules = [ 'username' => 'required|min:5|max:20', 'email' => 'required|email', 'age' => 'required|integer' ]; $result = Validator::validate($data, $rules); if ($result->isValid()) { echo "验证通过"; } else { foreach ($result->errors()->all() as $field => $errors) { echo "{$field}: " . implode(', ', $errors) . "\n"; } } ``` ### 自定义错误消息 ```php $rules = [ 'username' => 'required|min:5,message:用户名至少需要5个字符', 'email' => 'required|email,message:请输入有效的邮箱地址' ]; $result = Validator::validate($data, $rules); ``` ### 异常抛出模式 ```php use Awesome\Validation\ValidationException; try { Validator::validateOrFail($data, $rules); echo "验证通过"; } catch (ValidationException $e) { echo "验证失败: " . $e->getMessage(); print_r($e->toArray()); } ``` ### 使用 key() 方法进行灵活验证 使用 `key()` 方法可以更灵活地进行数组验证,避免手写规则字符串的拼写错误,并支持 IDE 自动完成: ```php $data = [ 'username' => 'john_doe', 'email' => 'john@example.com', 'age' => '25' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string()->minLength(5), $builder->key('email')->required()->email(), $builder->key('age')->required()->integer() ]; }); if ($result->isValid()) { echo "验证通过"; } else { foreach ($result->errors()->all() as $field => $errors) { echo "{$field}: " . implode(', ', $errors) . "\n"; } } ``` #### 嵌套字段验证 ```php $data = [ 'user' => [ 'profile' => [ 'name' => 'John Doe', 'email' => 'john@example.com' ] ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('user.profile.name')->required()->string()->minLength(2), $builder->key('user.profile.email')->required()->email() ]; }); ``` #### 自定义错误消息 ```php $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string()->minLength(5, '用户名至少需要5个字符'), $builder->key('email')->required()->email('请输入有效的邮箱地址') ]; }); ``` #### 使用 Builder 的 key() 方法 ```php $builder = Validator::make($data); $result = $builder->key('username') ->required() ->string() ->minLength(3) ->validate(); ``` #### 使用 each() 方法验证数组元素 当某个字段是数组时,可以使用 `each()` 方法验证数组中的每个元素: ```php $data = [ 'emails' => [ 'john@example.com', 'jane@example.com', 'invalid-email' ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('emails')->each()->email() ]; }); if ($result->isValid()) { echo "所有邮箱验证通过"; } else { foreach ($result->errors()->all() as $field => $errors) { echo "{$field}: " . implode(', ', $errors) . "\n"; } } ``` #### 混合使用 key() 和 each() ```php $data = [ 'username' => 'john_doe', 'emails' => [ 'john@example.com', 'jane@example.com' ], 'ages' => ['25', '30', '35'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string()->minLength(5), $builder->key('emails')->each()->email(), $builder->key('ages')->each()->integer() ]; }); ``` ## 验证规则 ### 基础规则 | 规则 | 描述 | 示例 | |------|------|------| | `required` | 必填字段 | `required` | | `string` | 字符串类型 | `string` | | `integer` | 整数类型 | `integer` | | `numeric` | 数字类型(包括字符串形式的数字) | `numeric` | | `email` | 邮箱格式 | `email` | ### 长度规则 | 规则 | 描述 | 示例 | |------|------|------| | `min:n` | 最小长度 | `min:5` | | `max:n` | 最大长度 | `max:20` | ### 值规则 | 规则 | 描述 | 示例 | |------|------|------| | `in:a,b,c` | 值在指定列表中 | `in:active,inactive,pending` | | `regex:pattern` | 正则表达式匹配 | `regex:/^[A-Z]/` | ### 自定义规则 ```php $result = Validator::string() ->custom(function($value) { return preg_match('/[A-Z]/', $value) && preg_match('/[0-9]/', $value); }, '密码必须包含大写字母和数字') ->validate('Password123'); ``` ## 高级用法 ### 嵌套字段验证 ```php $data = [ 'user' => [ 'profile' => [ 'name' => 'John Doe', 'email' => 'john@example.com' ] ] ]; $rules = [ 'user.profile.name' => 'required|min:2', 'user.profile.email' => 'required|email' ]; $result = Validator::validate($data, $rules); ``` ### 数组项验证 ```php $data = [ ['name' => 'John', 'age' => '25'], ['name' => 'Jane', 'age' => '30'] ]; $rules = [ 'name' => 'required|min:2', 'age' => 'required|integer' ]; $result = Validator::validateNested($data, $rules); ``` ### 链式验证 ```php $result = Validator::string() ->minLength(8) ->custom(function($value) { return preg_match('/[A-Z]/', $value); }, '必须包含大写字母') ->validate('Password123'); ``` ## API 参考 ### Validator 门面类 #### 静态方法 - `string(?string $message = null): Builder` - 创建字符串验证器 - `integer(?string $message = null): Builder` - 创建整数验证器 - `required(?string $message = null): Builder` - 创建必填验证器 - `email(?string $message = null): Builder` - 创建邮箱验证器 - `array(?string $message = null): Builder` - 创建数组验证器 - `make(array $data): Builder` - 创建验证器构建器 - `validate(array $data, array $rules): ValidationResult` - 验证数组数据 - `validateOrFail(array $data, array $rules): ValidationResult` - 验证并抛出异常 - `validateNested(array $data, array $rules): ValidationResult` - 验证嵌套数组 - `validateKeys(array $data, callable $callback): ValidationResult` - 使用 key() 方法验证数组数据 ### ValidationResult 类 #### 方法 - `isValid(): bool` - 检查验证是否通过 - `isInvalid(): bool` - 检查验证是否失败 - `errors(): MessageBag` - 获取错误消息包 - `toArray(): array` - 转换为数组 - `toJson(): string` - 转换为 JSON ### ValidationException 类 #### 方法 - `errors(): MessageBag` - 获取错误消息包 - `toArray(): array` - 转换为数组 - `toJson(): string` - 转换为 JSON - `fromResult(ValidationResult $result, string $message = ''): self` - 从验证结果创建异常 ### EachValidator 类 EachValidator 用于验证数组中的每个元素,提供与 KeyValidator 相同的验证方法。 #### 方法 - `required(?string $message = null): self` - 添加必填验证 - `string(?string $message = null): self` - 添加字符串验证 - `integer(?string $message = null): self` - 添加整数验证 - `email(?string $message = null): self` - 添加邮箱验证 - `minLength(int $min, ?string $message = null): self` - 添加最小长度验证 - `maxLength(int $max, ?string $message = null): self` - 添加最大长度验证 - `in(array $values, ?string $message = null): self` - 添加枚举值验证 - `regex(string $pattern, ?string $message = null): self` - 添加正则验证 - `custom(callable $callback, ?string $message = null): self` - 添加自定义验证 - `validate(): ValidationResult` - 执行验证 ## 运行示例 ```bash # 基础验证示例 php examples/basic_validation.php # 数组验证示例 php examples/array_validation.php # Key 验证示例 php examples/key_validation.php # Each 验证示例 php examples/each_validation.php # Numeric 验证示例 php examples/numeric_validation.php ``` ## 运行测试 ```bash # 运行所有测试 ./vendor/bin/phpunit # 运行单元测试 ./vendor/bin/phpunit tests/Unit # 运行功能测试 ./vendor/bin/phpunit tests/Feature ``` ## 项目结构 ``` awesome-validation/ ├── src/ │ └── Awesome/ │ └── Validation/ │ ├── Validator.php │ ├── Builder.php │ ├── ValidationResult.php │ ├── ValidationException.php │ ├── Rules/ │ │ ├── AbstractRule.php │ │ ├── StringRule.php │ │ ├── IntegerRule.php │ │ ├── RequiredRule.php │ │ ├── EmailRule.php │ │ ├── MinLengthRule.php │ │ ├── MaxLengthRule.php │ │ ├── InRule.php │ │ ├── RegexRule.php │ │ └── CustomRule.php │ └── Messages/ │ └── MessageBag.php ├── tests/ │ ├── Unit/ │ └── Feature/ ├── examples/ │ ├── basic_validation.php │ └── array_validation.php └── composer.json ``` ## 许可证 MIT License ## 贡献 欢迎提交 Issue 和 Pull Request! RULES.md000064400000014664151665713370006011 0ustar00# Awesome Validation - 验证规则清单 本文档列出了 Awesome Validation 库中所有可用的验证规则。 ## 规则分类 ### 1. 类型验证规则 | 规则类 | 规则名 | 说明 | 使用示例 | |--------|--------|------|----------| | StringRule | string | 验证值是否为字符串 | `$v->string()` | | IntegerRule | integer | 验证值是否为整数(支持 0x, 0b 等进制) | `$v->integer()` | | StrictIntegerRule | strictInteger | 严格校验是否为 int 类型 | `$v->strictInteger()` | | ArrayRule | array | 验证值是否为数组 | `$v->array()` | | NumericRule | numeric | 验证值是否为数值(包括字符串数字) | `$v->numeric()` | ### 2. 长度/范围验证规则 | 规则类 | 规则名 | 说明 | 使用示例 | |--------|--------|------|----------| | MinLengthRule | minLength | 验证字符串/数组长度不小于指定值 | `$v->minLength(5)` | | MaxLengthRule | maxLength | 验证字符串/数组长度不大于指定值 | `$v->maxLength(20)` | | ChineseLengthRule | chineseLength | 验证中文字符长度(一个汉字算1个长度) | `$v->chineseLength(2, 10)` | | MinRule | min | 验证数值最小值 | `$v->min(10, true)` | | MaxRule | max | 验证数值最大值 | `$v->max(100, true)` | | BetweenRule | between | 验证数值区间 | `$v->between(1, 10, true, false)` | ### 3. 数学属性验证规则 | 规则类 | 规则名 | 说明 | 使用示例 | |--------|--------|------|----------| | EvenRule | even | 验证是否为偶数 | `$v->even()` | | OddRule | odd | 验证是否为奇数 | `$v->odd()` | | PositiveRule | positive | 验证是否为正数 | `$v->positive()` | | NegativeRule | negative | 验证是否为负数 | `$v->negative()` | | PrimeRule | prime | 验证是否为质数 | `$v->prime()` | | SafeIntegerRule | safeInteger | 验证是否在安全整数范围内 | `$v->safeInteger()` | ### 4. 格式验证规则 | 规则类 | 规则名 | 说明 | 使用示例 | |--------|--------|------|----------| | EmailRule | email | 验证邮箱格式 | `$v->email()` | | PhoneRule | phone | 验证手机号格式 | `$v->phone()` | | IpRule | ip | 验证 IP 地址格式 | `$v->ip()` | | DomainRule | domain | 验证域名格式 | `$v->domain()` | | UrlRule | url | 验证 URL 格式 | `$v->url()` 或 `$v->url(['https'])` | | DateRule | date | 验证日期格式 | `$v->date()` 或 `$v->date('Y-m-d')` | | JsonRule | json | 验证 JSON 格式 | `$v->json()` 或 `$v->json(10, JSON_OBJECT_AS_ARRAY)` | | RegexRule | regex | 使用正则表达式验证 | `$v->regex('/^[a-z]+$/')` | ### 5. 特殊验证规则 | 规则类 | 规则名 | 说明 | 使用示例 | |--------|--------|------|----------| | InRule | in | 验证值在指定范围内 | `$v->in(['active', 'inactive'])` | | EachRule | each | 验证数组中的每个元素 | `$v->each()->string()` | | CustomRule | custom | 使用自定义回调验证 | `$v->custom(fn($v) => $v > 0)` | | SanitizeRule | sanitize | 输入清理和过滤 | `$v->sanitize('special_chars')` | ### 6. 必填验证规则 | 规则类 | 规则名 | 说明 | 使用示例 | |--------|--------|------|----------| | RequiredRule | required | 验证字段存在且不为空 | `$v->key('name')->required()` | | NotEmptyRule | notEmpty | 验证值不为空 | `$v->notEmpty()` | ## 数据预处理 (Transformation) 您可以在验证之前对数据进行转换。转换后的值仅在当前验证流中有效。 ```php $v->key('email') ->transform('trim') ->transform('strtolower') ->email(); ``` ## 规则组合使用示例 ### 基础类型验证 ```php $result = Validator::validateKeys($data, function($v) { return [ $v->key('username')->string()->minLength(3)->maxLength(20), $v->key('age')->integer()->min(0)->max(150, true), $v->key('email')->transform('trim')->strtolower()->email(), ]; }); ``` ### 数值区间与属性校验 ```php $result = Validator::validateKeys($data, function($v) { return [ // 分数必须在 [0, 100] 之间 $v->key('score')->numeric()->between(0, 100), // 必须是正奇数 $v->key('count')->numeric()->positive()->odd(), // 质数校验 $v->key('id')->numeric()->prime(), ]; }); ``` ### 日期与 URL 校验 ```php $result = Validator::validateKeys($data, function($v) { return [ // 日期校验(自动格式) $v->key('birthday')->date(), // 日期校验(指定格式) $v->key('start_date')->date('Y-m-d'), // URL 校验(允许所有协议) $v->key('website')->url(), // URL 校验(仅允许 HTTPS) $v->key('api_endpoint')->url(['https']), ]; }); ``` ### JSON 数据校验 ```php $result = Validator::validateKeys($data, function($v) { return [ // JSON 校验(自动转换为数组) $v->key('config')->json(), // JSON 校验(限制深度) $v->key('data')->json(10), // JSON 校验(指定标志) $v->key('settings')->json(null, JSON_OBJECT_AS_ARRAY), ]; }); ``` ### 输入清理与安全校验 ```php $result = Validator::validateKeys($data, function($v) { return [ // 清理特殊字符(防止 XSS) $v->key('user_input')->sanitize('special_chars'), // 清理邮箱格式 $v->key('email')->sanitize('email')->email(), // 清理 URL 格式 $v->key('website')->sanitize('url')->url(), // 清理数字输入 $v->key('amount')->sanitize('number')->numeric(), ]; }); ``` ### 综合安全校验示例 ```php $result = Validator::validateKeys($data, function($v) { return [ // 用户名:字符串 + 长度限制 + 清理 $v->key('username') ->sanitize('special_chars') ->string() ->minLength(3) ->maxLength(20), // 邮箱:清理 + 格式验证 $v->key('email') ->sanitize('email') ->email(), // 网站:清理 + HTTPS 限制 $v->key('website') ->sanitize('url') ->url(['https']), // 配置:JSON 验证 + 深度限制 $v->key('config') ->json(5), // 创建日期:格式验证 $v->key('created_at') ->date('Y-m-d H:i:s'), ]; }); ``` --- **总计**: 30 个验证规则类,覆盖类型、范围、数学属性、格式、特殊、必填和安全七大类别。 composer.json000064400000001356151665713370007311 0ustar00{ "name": "awesome/validation", "description": "一个友好的 PHP 参数验证库", "type": "library", "license": "MIT", "repositories": [ { "packagist": false }, { "type": "composer", "url": "https://packagist.i-ktv.com/" } ], "require": { "php": "^8.1" }, "require-dev": { "phpunit/phpunit": "^10.0", "phpstan/phpstan": "^1.0" }, "autoload": { "psr-4": { "Awesome\\Validation\\": "src/Awesome/Validation/" } }, "autoload-dev": { "psr-4": { "Awesome\\Validation\\Tests\\": "tests/" } }, "scripts": { "test": "phpunit", "analyze": "phpstan analyse src" } }composer.lock000064400000157105151665713370007274 0ustar00{ "_readme": [ "This file locks the dependencies of your project to a known state", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], "content-hash": "71c15bb0aa4a339d3ffa937c70794a60", "packages": [], "packages-dev": [ { "name": "myclabs/deep-copy", "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/myclabs/deep-copy/myclabs-deep-copy-123267b2c49fbf30d78a7b2d333f6be754b94845-zip-d3d371.tar", "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "ca6d35760f2c915885e4aad2bc6f77fbfdd0515f" }, "require": { "php": "^7.1 || ^8.0" }, "conflict": { "doctrine/collections": "<1.6.8", "doctrine/common": "<2.13.3 || >=3 <3.2.2" }, "require-dev": { "doctrine/collections": "^1.6.8", "doctrine/common": "^2.13.3 || ^3.2.2", "phpspec/prophecy": "^1.10", "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" }, "type": "library", "autoload": { "files": [ "src/DeepCopy/deep_copy.php" ], "psr-4": { "DeepCopy\\": "src/DeepCopy/" } }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "MIT" ], "description": "Create deep copies (clones) of your objects", "keywords": [ "clone", "copy", "duplicate", "object", "object graph" ], "funding": [ { "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", "type": "tidelift" } ], "time": "2024-11-08T17:47:46+00:00" }, { "name": "nikic/php-parser", "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/nikic/php-parser/nikic-php-parser-447a020a1f875a434d62f2a401f53b82a396e494-zip-35835f.tar", "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "b528f9880dc133cb5ddf725e07f1f9cc2d18bdc5" }, "require": { "ext-ctype": "*", "ext-json": "*", "ext-tokenizer": "*", "php": ">=7.4" }, "require-dev": { "ircmaxell/php-yacc": "^0.0.7", "phpunit/phpunit": "^9.0" }, "bin": [ "bin/php-parse" ], "type": "library", "extra": { "branch-alias": { "dev-master": "5.0-dev" } }, "autoload": { "psr-4": { "PhpParser\\": "lib/PhpParser" } }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Nikita Popov" } ], "description": "A PHP parser written in PHP", "keywords": [ "parser", "php" ], "time": "2024-12-30T11:07:19+00:00" }, { "name": "phar-io/manifest", "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", "reference": "54750ef60c58e43759730615a392c31c80e23176" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/phar-io/manifest/phar-io-manifest-54750ef60c58e43759730615a392c31c80e23176-zip-828055.tar", "reference": "54750ef60c58e43759730615a392c31c80e23176", "shasum": "ff4b490eac87839069252112b75ccf8e8ae2f60f" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-phar": "*", "ext-xmlwriter": "*", "phar-io/version": "^3.0.1", "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { "dev-master": "2.0.x-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Arne Blankerts", "email": "arne@blankerts.de", "role": "Developer" }, { "name": "Sebastian Heuer", "email": "sebastian@phpeople.de", "role": "Developer" }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "Developer" } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", "funding": [ { "url": "https://github.com/theseer", "type": "github" } ], "time": "2024-03-03T12:33:53+00:00" }, { "name": "phar-io/version", "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/phar-io/version/phar-io-version-4f7fd7836c6f332bb2933569e566a0d6c4cbed74-zip-44d1a0.tar", "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", "shasum": "0704589b2e31b1fd3ac07305c84b97ba66d181ec" }, "require": { "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Arne Blankerts", "email": "arne@blankerts.de", "role": "Developer" }, { "name": "Sebastian Heuer", "email": "sebastian@phpeople.de", "role": "Developer" }, { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "Developer" } ], "description": "Library for handling version information and constraints", "time": "2022-02-21T01:04:05+00:00" }, { "name": "phpstan/phpstan", "version": "1.12.16", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", "reference": "e0bb5cb78545aae631220735aa706eac633a6be9" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/phpstan/phpstan/phpstan-phpstan-e0bb5cb78545aae631220735aa706eac633a6be9-zip-c6bc53.tar", "reference": "e0bb5cb78545aae631220735aa706eac633a6be9", "shasum": "83f9bcf6d04ab6e33888926029b5bdec3d1feba4" }, "require": { "php": "^7.2|^8.0" }, "conflict": { "phpstan/phpstan-shim": "*" }, "bin": [ "phpstan", "phpstan.phar" ], "type": "library", "autoload": { "files": [ "bootstrap.php" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", "keywords": [ "dev", "static analysis" ], "funding": [ { "url": "https://github.com/ondrejmirtes", "type": "github" }, { "url": "https://github.com/phpstan", "type": "github" } ], "time": "2025-01-21T14:50:05+00:00" }, { "name": "phpunit/php-code-coverage", "version": "10.1.16", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", "reference": "7e308268858ed6baedc8704a304727d20bc07c77" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/phpunit/php-code-coverage/phpunit-php-code-coverage-7e308268858ed6baedc8704a304727d20bc07c77-zip-385d17.tar", "reference": "7e308268858ed6baedc8704a304727d20bc07c77", "shasum": "082f909d83bb1436d143827bc343b7f7bd7f5069" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", "nikic/php-parser": "^4.19.1 || ^5.1.0", "php": ">=8.1", "phpunit/php-file-iterator": "^4.1.0", "phpunit/php-text-template": "^3.0.1", "sebastian/code-unit-reverse-lookup": "^3.0.0", "sebastian/complexity": "^3.2.0", "sebastian/environment": "^6.1.0", "sebastian/lines-of-code": "^2.0.2", "sebastian/version": "^4.0.1", "theseer/tokenizer": "^1.2.3" }, "require-dev": { "phpunit/phpunit": "^10.1" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" }, "type": "library", "extra": { "branch-alias": { "dev-main": "10.1.x-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", "homepage": "https://github.com/sebastianbergmann/php-code-coverage", "keywords": [ "coverage", "testing", "xunit" ], "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-08-22T04:31:57+00:00" }, { "name": "phpunit/php-file-iterator", "version": "4.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/phpunit/php-file-iterator/phpunit-php-file-iterator-a95037b6d9e608ba092da1b23931e537cadc3c3c-zip-bc4229.tar", "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", "shasum": "86767402534f67c71da6a94f44b8345dadcd9137" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "4.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "FilterIterator implementation that filters files based on a list of suffixes.", "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", "keywords": [ "filesystem", "iterator" ], "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-08-31T06:24:48+00:00" }, { "name": "phpunit/php-invoker", "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/phpunit/php-invoker/phpunit-php-invoker-f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7-zip-4167d0.tar", "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", "shasum": "bd0e4e5ac7ab471bd0837feca473eeb35f55705d" }, "require": { "php": ">=8.1" }, "require-dev": { "ext-pcntl": "*", "phpunit/phpunit": "^10.0" }, "suggest": { "ext-pcntl": "*" }, "type": "library", "extra": { "branch-alias": { "dev-main": "4.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Invoke callables with a timeout", "homepage": "https://github.com/sebastianbergmann/php-invoker/", "keywords": [ "process" ], "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T06:56:09+00:00" }, { "name": "phpunit/php-text-template", "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/phpunit/php-text-template/phpunit-php-text-template-0c7b06ff49e3d5072f057eb1fa59258bf287a748-zip-cc03ba.tar", "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", "shasum": "fae71c5616d45433f50e0b8ea19959f6d80545f7" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Simple template engine.", "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ "template" ], "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-08-31T14:07:24+00:00" }, { "name": "phpunit/php-timer", "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/phpunit/php-timer/phpunit-php-timer-e2a2d67966e740530f4a3343fe2e030ffdc1161d-zip-eb6b83.tar", "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", "shasum": "35587490a5453dcc78af419e3c72fd7735db046f" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "6.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Utility class for timing", "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ "timer" ], "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T06:57:52+00:00" }, { "name": "phpunit/phpunit", "version": "10.5.44", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", "reference": "1381c62769be4bb88fa4c5aec1366c7c66ca4f36" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/phpunit/phpunit/phpunit-phpunit-1381c62769be4bb88fa4c5aec1366c7c66ca4f36-zip-550947.tar", "reference": "1381c62769be4bb88fa4c5aec1366c7c66ca4f36", "shasum": "e8654d8a9671284872dd061b4934a6d17a4f0b30" }, "require": { "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.1", "phpunit/php-code-coverage": "^10.1.16", "phpunit/php-file-iterator": "^4.1.0", "phpunit/php-invoker": "^4.0.0", "phpunit/php-text-template": "^3.0.1", "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", "sebastian/comparator": "^5.0.3", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", "sebastian/exporter": "^5.1.2", "sebastian/global-state": "^6.0.2", "sebastian/object-enumerator": "^5.0.0", "sebastian/recursion-context": "^5.0.0", "sebastian/type": "^4.0.0", "sebastian/version": "^4.0.1" }, "suggest": { "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" ], "type": "library", "extra": { "branch-alias": { "dev-main": "10.5-dev" } }, "autoload": { "files": [ "src/Framework/Assert/Functions.php" ], "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "The PHP Unit Testing framework.", "homepage": "https://phpunit.de/", "keywords": [ "phpunit", "testing", "xunit" ], "funding": [ { "url": "https://phpunit.de/sponsors.html", "type": "custom" }, { "url": "https://github.com/sebastianbergmann", "type": "github" }, { "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "type": "tidelift" } ], "time": "2025-01-31T07:00:38+00:00" }, { "name": "sebastian/cli-parser", "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/cli-parser/sebastian-cli-parser-c34583b87e7b7a8055bf6c450c2c77ce32a24084-zip-580728.tar", "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", "shasum": "cf8e8cd3ca813a74689e5f81423f1e91fd4d10bc" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "2.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Library for parsing CLI options", "homepage": "https://github.com/sebastianbergmann/cli-parser", "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-03-02T07:12:49+00:00" }, { "name": "sebastian/code-unit", "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/code-unit/sebastian-code-unit-a81fee9eef0b7a76af11d121767abc44c104e503-zip-86e01f.tar", "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", "shasum": "b45db454110fc044e60fa62398453a2ddbeba598" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "2.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Collection of value objects that represent the PHP code units", "homepage": "https://github.com/sebastianbergmann/code-unit", "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T06:58:43+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/code-unit-reverse-lookup/sebastian-code-unit-reverse-lookup-5e3a687f7d8ae33fb362c5c0743794bbb2420a1d-zip-6c698c.tar", "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", "shasum": "0bad1d5f9453fd2629bd23111f9ea89537fdc8a8" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T06:59:15+00:00" }, { "name": "sebastian/comparator", "version": "5.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/comparator/sebastian-comparator-a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e-zip-a1c96f.tar", "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", "shasum": "decb876c06efa7f3bd8bfa78e7ee4cf081d190d5" }, "require": { "ext-dom": "*", "ext-mbstring": "*", "php": ">=8.1", "sebastian/diff": "^5.0", "sebastian/exporter": "^5.0" }, "require-dev": { "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { "branch-alias": { "dev-main": "5.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, { "name": "Volker Dusch", "email": "github@wallbash.com" }, { "name": "Bernhard Schussek", "email": "bschussek@2bepublished.at" } ], "description": "Provides the functionality to compare PHP values for equality", "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ "comparator", "compare", "equality" ], "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-10-18T14:56:07+00:00" }, { "name": "sebastian/complexity", "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", "reference": "68ff824baeae169ec9f2137158ee529584553799" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/complexity/sebastian-complexity-68ff824baeae169ec9f2137158ee529584553799-zip-99104e.tar", "reference": "68ff824baeae169ec9f2137158ee529584553799", "shasum": "3503ca493accd6d7ca1a0c497e24e8ea01728978" }, "require": { "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.2-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Library for calculating the complexity of PHP code units", "homepage": "https://github.com/sebastianbergmann/complexity", "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-12-21T08:37:17+00:00" }, { "name": "sebastian/diff", "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/diff/sebastian-diff-c41e007b4b62af48218231d6c2275e4c9b975b2e-zip-f415cf.tar", "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", "shasum": "80cbc53d49e13197d34210932c2cf055e857adc9" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0", "symfony/process": "^6.4" }, "type": "library", "extra": { "branch-alias": { "dev-main": "5.1-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Kore Nordmann", "email": "mail@kore-nordmann.de" } ], "description": "Diff implementation", "homepage": "https://github.com/sebastianbergmann/diff", "keywords": [ "diff", "udiff", "unidiff", "unified diff" ], "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-03-02T07:15:17+00:00" }, { "name": "sebastian/environment", "version": "6.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/environment/sebastian-environment-8074dbcd93529b357029f5cc5058fd3e43666984-zip-5b3490.tar", "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", "shasum": "301eab31fd519db24aa67e28247eb6d3f7db66ed" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "suggest": { "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { "dev-main": "6.1-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "description": "Provides functionality to handle HHVM/PHP environments", "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", "hhvm" ], "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-03-23T08:47:14+00:00" }, { "name": "sebastian/exporter", "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", "reference": "955288482d97c19a372d3f31006ab3f37da47adf" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/exporter/sebastian-exporter-955288482d97c19a372d3f31006ab3f37da47adf-zip-f40d11.tar", "reference": "955288482d97c19a372d3f31006ab3f37da47adf", "shasum": "547ec94a495e36fe0c8082418bf362c54365df27" }, "require": { "ext-mbstring": "*", "php": ">=8.1", "sebastian/recursion-context": "^5.0" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "5.1-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, { "name": "Volker Dusch", "email": "github@wallbash.com" }, { "name": "Adam Harvey", "email": "aharvey@php.net" }, { "name": "Bernhard Schussek", "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", "homepage": "https://www.github.com/sebastianbergmann/exporter", "keywords": [ "export", "exporter" ], "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-03-02T07:17:12+00:00" }, { "name": "sebastian/global-state", "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/global-state/sebastian-global-state-987bafff24ecc4c9ac418cab1145b96dd6e9cbd9-zip-a34a10.tar", "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", "shasum": "2001156a62e2843908056e2f097b97fdaa1c26df" }, "require": { "php": ">=8.1", "sebastian/object-reflector": "^3.0", "sebastian/recursion-context": "^5.0" }, "require-dev": { "ext-dom": "*", "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "6.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "description": "Snapshotting of global state", "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2024-03-02T07:19:19+00:00" }, { "name": "sebastian/lines-of-code", "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/lines-of-code/sebastian-lines-of-code-856e7f6a75a84e339195d48c556f23be2ebf75d0-zip-f0fa48.tar", "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", "shasum": "02dd5bf8a44f0d49940cd45c958d1db685b81ccd" }, "require": { "nikic/php-parser": "^4.18 || ^5.0", "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "2.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Library for counting the lines of code in PHP source code", "homepage": "https://github.com/sebastianbergmann/lines-of-code", "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-12-21T08:38:20+00:00" }, { "name": "sebastian/object-enumerator", "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/object-enumerator/sebastian-object-enumerator-202d0e344a580d7f7d04b3fafce6933e59dae906-zip-1726e9.tar", "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", "shasum": "2ba9f0673f17360388452106af69884db90c0500" }, "require": { "php": ">=8.1", "sebastian/object-reflector": "^3.0", "sebastian/recursion-context": "^5.0" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "5.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T07:08:32+00:00" }, { "name": "sebastian/object-reflector", "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/object-reflector/sebastian-object-reflector-24ed13d98130f0e7122df55d06c5c4942a577957-zip-38f027.tar", "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", "shasum": "6ac60650e1e10126838e8c0ee4e3d6073ed17022" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "3.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" } ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T07:06:18+00:00" }, { "name": "sebastian/recursion-context", "version": "5.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", "reference": "05909fb5bc7df4c52992396d0116aed689f93712" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/recursion-context/sebastian-recursion-context-05909fb5bc7df4c52992396d0116aed689f93712-zip-0d1366.tar", "reference": "05909fb5bc7df4c52992396d0116aed689f93712", "shasum": "7b52d9210ca5793a41ff9440aa39c24c4d130901" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "5.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de" }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" }, { "name": "Adam Harvey", "email": "aharvey@php.net" } ], "description": "Provides functionality to recursively process PHP variables", "homepage": "https://github.com/sebastianbergmann/recursion-context", "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T07:05:40+00:00" }, { "name": "sebastian/type", "version": "4.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/type/sebastian-type-462699a16464c3944eefc02ebdd77882bd3925bf-zip-c08c28.tar", "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", "shasum": "676c0745dd26083d1afdea9e20b9b185e5106e81" }, "require": { "php": ">=8.1" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "type": "library", "extra": { "branch-alias": { "dev-main": "4.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-03T07:10:45+00:00" }, { "name": "sebastian/version", "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/sebastian/version/sebastian-version-c51fa83a5d8f43f1402e3f32a005e6262244ef17-zip-d9a2c6.tar", "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", "shasum": "17cee86f8d711d798439dc165735a4d0ab17cb3e" }, "require": { "php": ">=8.1" }, "type": "library", "extra": { "branch-alias": { "dev-main": "4.0-dev" } }, "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Sebastian Bergmann", "email": "sebastian@phpunit.de", "role": "lead" } ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" } ], "time": "2023-02-07T11:34:05+00:00" }, { "name": "theseer/tokenizer", "version": "1.2.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" }, "dist": { "type": "tar", "url": "https://packagist.i-ktv.com//dist/theseer/tokenizer/theseer-tokenizer-737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2-zip-752359.tar", "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", "shasum": "1d8d5570aaf949ffb7d8b801abc092c73bef915d" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { "classmap": [ "src/" ] }, "notification-url": "https://packagist.int.fun360.cn/composer/download", "license": [ "BSD-3-Clause" ], "authors": [ { "name": "Arne Blankerts", "email": "arne@blankerts.de", "role": "Developer" } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "funding": [ { "url": "https://github.com/theseer", "type": "github" } ], "time": "2024-03-03T12:36:25+00:00" } ], "aliases": [], "minimum-stability": "stable", "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "^8.1" }, "platform-dev": [], "plugin-api-version": "1.1.0" } docs/plans/2026-04-10-awesome-validation-design.md000064400000023041151665713370015236 0ustar00# Awesome Validation 设计文档 ## 项目概述 Awesome Validation 是一个友好的 PHP 参数验证库,基于 PHP 8.1+ 语法开发,遵循 PSR-4 规范。该库解决了现有验证库(如 Respect Validation)错误信息提示不友好的问题,提供了灵活的消息定制和多种验证方式。 ## 核心需求 1. **友好的错误消息**:每个验证规则有默认中文消息模板,支持自定义覆盖 2. **灵活的消息设置**:支持单独设置、批量设置、使用默认消息三种方式 3. **多种验证方式**:支持链式调用、规则数组、Attribute 三种验证方式 4. **多格式输出**:支持字符串、数组、JSON 三种错误输出格式 5. **嵌套验证**:支持点号路径访问(如 `user.profile.name`) 6. **丰富的上下文信息**:错误消息包含字段名、期望值等上下文信息 ## 架构设计 ### 目录结构 ``` awesome-validation/ ├── src/ │ └── Awesome/ │ └── Validation/ │ ├── Validator.php # 门面类 │ ├── Builder.php # 验证器构建器 │ ├── ValidationResult.php # 验证结果 │ ├── Rules/ # 验证规则 │ │ ├── AbstractRule.php # 规则基类 │ │ ├── StringRule.php │ │ ├── IntegerRule.php │ │ ├── EmailRule.php │ │ ├── RequiredRule.php │ │ ├── MinLengthRule.php │ │ ├── MaxLengthRule.php │ │ ├── MinRule.php │ │ ├── MaxRule.php │ │ ├── InRule.php │ │ ├── RegexRule.php │ │ └── CustomRule.php │ ├── Exceptions/ # 异常类 │ │ └── ValidationException.php │ ├── Messages/ # 消息处理 │ │ ├── MessageBag.php │ │ └── MessageTemplate.php │ └── Support/ # 辅助类 │ ├── AttributeValidator.php │ └── RuleParser.php ├── tests/ │ ├── Unit/ │ │ ├── Rules/ │ │ └── Messages/ │ └── Feature/ │ └── ValidatorTest.php ├── composer.json ├── phpunit.xml └── README.md ``` ### 核心组件 #### 1. Validator 门面类 提供静态方法作为验证库的入口点。 ```php namespace Awesome\Validation; class Validator { public static function string(?string $message = null): Builder public static function integer(?string $message = null): Builder public static function email(?string $message = null): Builder public static function required(?string $message = null): Builder public static function array(?string $message = null): Builder public static function validate(array $data, array $rules): ValidationResult public static function make(array $data): Builder } ``` #### 2. Builder 验证器构建器 实现链式调用,管理验证规则和消息。 ```php namespace Awesome\Validation; class Builder { private array $rules = []; private array $messages = []; private ?string $field = null; public function __construct(private mixed $value = null) {} public function string(?string $message = null): self public function integer(?string $message = null): self public function minLength(int $min, ?string $message = null): self public function maxLength(int $max, ?string $message = null): self public function email(?string $message = null): self public function required(?string $message = null): self public function in(array $values, ?string $message = null): self public function regex(string $pattern, ?string $message = null): self public function custom(callable $callback, ?string $message = null): self public function message(string $message): self public function messages(array $messages): self public function validate(mixed $value = null): ValidationResult public function check(mixed $value = null): bool } ``` #### 3. ValidationResult 验证结果 封装验证结果,提供多种输出格式。 ```php namespace Awesome\Validation; class ValidationResult { private bool $valid; private MessageBag $errors; public function isValid(): bool public function isInvalid(): bool public function errors(): MessageBag public function firstError(): ?string public function toArray(): array public function toJson(): string public function __toString(): string } ``` #### 4. MessageBag 消息收集器 收集和管理验证错误消息。 ```php namespace Awesome\Validation; class MessageBag implements \JsonSerializable { private array $messages = []; public function add(string $field, string $message): void public function get(string $field): array public function all(): array public function first(string $field): ?string public function has(string $field): bool public function isEmpty(): bool public function toArray(): array public function toJson(): string public function __toString(): string } ``` #### 5. AbstractRule 规则基类 所有验证规则的基类,定义规则接口。 ```php namespace Awesome\Validation\Rules; abstract class AbstractRule { protected string $name; protected ?string $message = null; protected array $parameters = []; public function __construct(?string $message = null) { $this->message = $message; } abstract public function validate(mixed $value): bool; public function getName(): string public function setMessage(string $message): self public function getMessage(): string public function getParameters(): array public function getDefaultMessage(): string public function getFormattedMessage(array $context = []): string } ``` #### 6. ValidationException 异常类 验证失败时抛出的异常。 ```php namespace Awesome\Validation\Exceptions; class ValidationException extends \InvalidArgumentException { private MessageBag $errors; private string $field; public function __construct(string $message, MessageBag $errors, string $field = '') { parent::__construct($message); $this->errors = $errors; $this->field = $field; } public function getErrors(): MessageBag public function getField(): string public function toArray(): array public function toJson(): string } ``` ## 使用示例 ### 链式调用 ```php use Awesome\Validation\Validator; try { Validator::string()->message('必须是字符串') ->minLength(5)->message('至少5个字符') ->maxLength(20)->message('最多20个字符') ->validate($username); } catch (ValidationException $e) { echo $e->getMessage(); } $result = Validator::email()->validate($email); if ($result->isInvalid()) { echo $result->firstError(); } ``` ### 规则数组验证 ```php use Awesome\Validation\Validator; $result = Validator::validate($data, [ 'username' => 'required|string|min:5|max:20', 'email' => 'required|email', 'age' => 'integer|min:18|max:120', 'profile.bio' => 'string|max:500' ]); if ($result->isInvalid()) { print_r($result->errors()->toArray()); } ``` ### Attribute 验证 ```php use Awesome\Validation\Support\Validate; class UserRequest { #[Validate('required|string|min:5|max:20', message: '用户名必填')] public string $username; #[Validate('required|email', message: '邮箱格式错误')] public string $email; #[Validate('integer|min:18', message: '年龄必须大于18岁')] public int $age; } $validator = new AttributeValidator(); $validator->validate($userRequest); ``` ## 数据流设计 ### 验证流程 ``` 用户输入 ↓ Validator::validate() ↓ Builder (构建验证链) ↓ Rule 验证器 (逐个验证) ↓ MessageBag (收集错误) ↓ ValidationResult (返回结果) ↓ 用户处理结果 ``` ### 错误处理流程 ``` 验证失败 ↓ Rule 抛出异常 ↓ Builder 捕获并收集 ↓ MessageBag 存储错误 ↓ ValidationException (如果需要) ↓ 用户获取错误信息 ``` ## 技术规范 ### PHP 版本 - 最低要求:PHP 8.1+ - 推荐版本:PHP 8.2+ ### PSR 规范 - PSR-4:自动加载规范 - PSR-12:编码风格规范 ### 依赖 - 无外部依赖(纯 PHP 实现) - 开发依赖:PHPUnit、PHPStan ### 性能考虑 - 规则对象可缓存复用 - 适合常驻内存环境(如 Webman) - 避免不必要的对象创建 ## 扩展性设计 ### 添加新规则 1. 继承 `AbstractRule` 类 2. 实现 `validate()` 方法 3. 定义 `getDefaultMessage()` 方法 4. 在 `Builder` 中添加对应方法 ### 自定义消息模板 支持使用占位符动态生成消息: - `{field}`:字段名 - `{value}`:当前值 - `{min}`、`{max}`:规则参数 ### 国际化支持 预留接口,未来可扩展多语言支持。 ## 测试策略 ### 单元测试 - 每个规则类的独立测试 - MessageBag 功能测试 - MessageTemplate 测试 ### 集成测试 - 链式调用测试 - 规则数组验证测试 - Attribute 验证测试 ### 边界测试 - 空值处理 - 类型转换 - 嵌套验证 ## 后续优化 1. 支持更多验证规则(日期、URL、文件等) 2. 添加数据库验证规则(唯一性、存在性) 3. 性能优化和基准测试 4. 完善文档和示例 5. 添加更多语言支持 docs/plans/2026-04-10-awesome-validation-implementation.md000064400000131447151665713370017024 0ustar00# Awesome Validation Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 构建一个友好的 PHP 参数验证库,支持多种验证方式、灵活的错误消息定制和多格式输出 **Architecture:** 基于规则引擎的验证器架构,每个验证规则是独立的类,通过 Builder 实现链式调用,MessageBag 管理错误消息,支持链式调用、规则数组和 Attribute 三种验证方式 **Tech Stack:** PHP 8.1+, PHPUnit 10.0, PSR-4, PSR-12 --- ## Task 1: 初始化项目结构 **Files:** - Create: `composer.json` - Create: `phpunit.xml` - Create: `README.md` - Create: `src/Awesome/Validation/` - Create: `tests/` **Step 1: 创建 composer.json** ```json { "name": "awesome/validation", "description": "一个友好的 PHP 参数验证库", "type": "library", "license": "MIT", "require": { "php": "^8.1" }, "require-dev": { "phpunit/phpunit": "^10.0", "phpstan/phpstan": "^1.0" }, "autoload": { "psr-4": { "Awesome\\Validation\\": "src/Awesome/Validation/" } }, "autoload-dev": { "psr-4": { "Awesome\\Validation\\Tests\\": "tests/" } }, "scripts": { "test": "phpunit", "analyze": "phpstan analyse src" } } ``` **Step 2: 创建 phpunit.xml** ```xml tests src ``` **Step 3: 创建 README.md** ```markdown # Awesome Validation 一个友好的 PHP 参数验证库 ## 安装 ```bash composer require awesome/validation ``` ## 使用示例 ```php use Awesome\Validation\Validator; Validator::string()->minLength(5)->validate($username); ``` ## 运行测试 ```bash composer test ``` ``` **Step 4: 创建目录结构** Run: `mkdir -p src/Awesome/Validation/{Rules,Exceptions,Messages,Support} tests/{Unit,Feature}` **Step 5: 初始化 Composer** Run: `composer install` **Step 6: Commit** ```bash git add composer.json phpunit.xml README.md git commit -m "feat: initialize project structure" ``` --- ## Task 2: 实现 MessageBag 消息收集器 **Files:** - Create: `src/Awesome/Validation/Messages/MessageBag.php` - Create: `tests/Unit/Messages/MessageBagTest.php` **Step 1: 编写 MessageBag 测试** ```php add('username', '用户名必填'); $this->assertEquals(['用户名必填'], $bag->get('username')); } public function testGetAllMessages() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $bag->add('email', '邮箱格式错误'); $expected = [ 'username' => ['用户名必填'], 'email' => ['邮箱格式错误'] ]; $this->assertEquals($expected, $bag->all()); } public function testFirstMessage() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $bag->add('username', '用户名太短'); $this->assertEquals('用户名必填', $bag->first('username')); } public function testHasField() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $this->assertTrue($bag->has('username')); $this->assertFalse($bag->has('email')); } public function testIsEmpty() { $bag = new MessageBag(); $this->assertTrue($bag->isEmpty()); $bag->add('username', '用户名必填'); $this->assertFalse($bag->isEmpty()); } public function testToArray() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $this->assertEquals(['username' => ['用户名必填']], $bag->toArray()); } public function testToJson() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $expected = json_encode(['username' => ['用户名必填']]); $this->assertEquals($expected, $bag->toJson()); } public function testToString() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $this->assertEquals('用户名必填', (string)$bag); } } ``` **Step 2: 运行测试验证失败** Run: `composer test` Expected: FAIL with "Class Awesome\Validation\Messages\MessageBag not found" **Step 3: 实现 MessageBag 类** ```php messages[$field])) { $this->messages[$field] = []; } $this->messages[$field][] = $message; } public function get(string $field): array { return $this->messages[$field] ?? []; } public function all(): array { return $this->messages; } public function first(string $field): ?string { $messages = $this->get($field); return $messages[0] ?? null; } public function has(string $field): bool { return isset($this->messages[$field]) && !empty($this->messages[$field]); } public function isEmpty(): bool { return empty($this->messages); } public function toArray(): array { return $this->messages; } public function toJson(): string { return json_encode($this->messages, JSON_UNESCAPED_UNICODE); } public function jsonSerialize(): array { return $this->messages; } public function __toString(): string { $messages = []; foreach ($this->messages as $field => $fieldMessages) { foreach ($fieldMessages as $message) { $messages[] = $message; } } return implode("\n", $messages); } } ``` **Step 4: 运行测试验证通过** Run: `composer test` Expected: PASS **Step 5: Commit** ```bash git add src/Awesome/Validation/Messages/MessageBag.php tests/Unit/Messages/MessageBagTest.php git commit -m "feat: implement MessageBag message collector" ``` --- ## Task 3: 实现 AbstractRule 规则基类 **Files:** - Create: `src/Awesome/Validation/Rules/AbstractRule.php` - Create: `tests/Unit/Rules/AbstractRuleTest.php` **Step 1: 编写 AbstractRule 测试** ```php assertEquals('test', $rule->getName()); } public function testSetAndGetMessage() { $rule = new TestRule('自定义消息'); $this->assertEquals('自定义消息', $rule->getMessage()); $rule->setMessage('新消息'); $this->assertEquals('新消息', $rule->getMessage()); } public function testGetParameters() { $rule = new TestRuleWithParams(5, 10); $this->assertEquals(['min' => 5, 'max' => 10], $rule->getParameters()); } public function testGetDefaultMessage() { $rule = new TestRule(); $this->assertEquals('默认消息', $rule->getDefaultMessage()); } public function testGetFormattedMessage() { $rule = new TestRuleWithParams(5, 10); $message = $rule->getFormattedMessage(['field' => 'username']); $this->assertStringContainsString('username', $message); $this->assertStringContainsString('5', $message); $this->assertStringContainsString('10', $message); } } class TestRule extends AbstractRule { protected string $name = 'test'; public function validate(mixed $value): bool { return true; } public function getDefaultMessage(): string { return '默认消息'; } } class TestRuleWithParams extends AbstractRule { protected string $name = 'test'; public function __construct(int $min, int $max, ?string $message = null) { parent::__construct($message); $this->parameters['min'] = $min; $this->parameters['max'] = $max; } public function validate(mixed $value): bool { return true; } public function getDefaultMessage(): string { return '{field} 必须在 {min} 到 {max} 之间'; } } ``` **Step 2: 运行测试验证失败** Run: `composer test` Expected: FAIL with "Class Awesome\Validation\Rules\AbstractRule not found" **Step 3: 实现 AbstractRule 类** ```php message = $message; } abstract public function validate(mixed $value): bool; public function getName(): string { return $this->name; } public function setMessage(string $message): self { $this->message = $message; return $this; } public function getMessage(): string { return $this->message ?? $this->getDefaultMessage(); } public function getParameters(): array { return $this->parameters; } abstract public function getDefaultMessage(): string; public function getFormattedMessage(array $context = []): string { $message = $this->getMessage(); $replacements = array_merge($this->parameters, $context); foreach ($replacements as $key => $value) { $message = str_replace('{' . $key . '}', (string)$value, $message); } return $message; } } ``` **Step 4: 运行测试验证通过** Run: `composer test` Expected: PASS **Step 5: Commit** ```bash git add src/Awesome/Validation/Rules/AbstractRule.php tests/Unit/Rules/AbstractRuleTest.php git commit -m "feat: implement AbstractRule base class" ``` --- ## Task 4: 实现基础验证规则 **Files:** - Create: `src/Awesome/Validation/Rules/StringRule.php` - Create: `src/Awesome/Validation/Rules/IntegerRule.php` - Create: `src/Awesome/Validation/Rules/RequiredRule.php` - Create: `src/Awesome/Validation/Rules/EmailRule.php` - Create: `tests/Unit/Rules/StringRuleTest.php` - Create: `tests/Unit/Rules/IntegerRuleTest.php` - Create: `tests/Unit/Rules/RequiredRuleTest.php` - Create: `tests/Unit/Rules/EmailRuleTest.php` **Step 1: 实现 StringRule** ```php assertTrue($rule->validate('hello')); } public function testInvalidString() { $rule = new StringRule(); $this->assertFalse($rule->validate(123)); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate([])); } public function testCustomMessage() { $rule = new StringRule('必须是字符串'); $this->assertEquals('必须是字符串', $rule->getMessage()); } public function testDefaultMessage() { $rule = new StringRule(); $this->assertEquals('{field} 必须是字符串', $rule->getDefaultMessage()); } } ``` **Step 6: 编写 IntegerRule 测试** ```php assertTrue($rule->validate(123)); $this->assertTrue($rule->validate(0)); $this->assertTrue($rule->validate(-456)); } public function testInvalidInteger() { $rule = new IntegerRule(); $this->assertFalse($rule->validate('123')); $this->assertFalse($rule->validate(12.34)); $this->assertFalse($rule->validate(null)); } } ``` **Step 7: 编写 RequiredRule 测试** ```php assertTrue($rule->validate('hello')); $this->assertTrue($rule->validate(123)); $this->assertTrue($rule->validate([1, 2, 3])); } public function testInvalidRequired() { $rule = new RequiredRule(); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate('')); $this->assertFalse($rule->validate(' ')); $this->assertFalse($rule->validate([])); } } ``` **Step 8: 编写 EmailRule 测试** ```php assertTrue($rule->validate('test@example.com')); $this->assertTrue($rule->validate('user.name+tag@domain.co.uk')); } public function testInvalidEmail() { $rule = new EmailRule(); $this->assertFalse($rule->validate('invalid')); $this->assertFalse($rule->validate('invalid@')); $this->assertFalse($rule->validate('@example.com')); $this->assertFalse($rule->validate(123)); } } ``` **Step 9: 运行测试验证通过** Run: `composer test` Expected: PASS **Step 10: Commit** ```bash git add src/Awesome/Validation/Rules/*.php tests/Unit/Rules/*.php git commit -m "feat: implement basic validation rules" ``` --- ## Task 5: 实现 ValidationResult 验证结果类 **Files:** - Create: `src/Awesome/Validation/ValidationResult.php` - Create: `tests/Unit/ValidationResultTest.php` **Step 1: 编写 ValidationResult 测试** ```php assertTrue($result->isValid()); $this->assertFalse($result->isInvalid()); } public function testInvalidResult() { $errors = new MessageBag(); $errors->add('username', '用户名必填'); $result = new ValidationResult(false, $errors); $this->assertFalse($result->isValid()); $this->assertTrue($result->isInvalid()); } public function testErrors() { $errors = new MessageBag(); $errors->add('username', '用户名必填'); $result = new ValidationResult(false, $errors); $this->assertEquals($errors, $result->errors()); } public function testFirstError() { $errors = new MessageBag(); $errors->add('username', '用户名必填'); $errors->add('email', '邮箱格式错误'); $result = new ValidationResult(false, $errors); $this->assertEquals('用户名必填', $result->firstError()); } public function testToArray() { $errors = new MessageBag(); $errors->add('username', '用户名必填'); $result = new ValidationResult(false, $errors); $this->assertEquals(['username' => ['用户名必填']], $result->toArray()); } public function testToJson() { $errors = new MessageBag(); $errors->add('username', '用户名必填'); $result = new ValidationResult(false, $errors); $expected = json_encode(['username' => ['用户名必填']]); $this->assertEquals($expected, $result->toJson()); } public function testToString() { $errors = new MessageBag(); $errors->add('username', '用户名必填'); $result = new ValidationResult(false, $errors); $this->assertEquals('用户名必填', (string)$result); } } ``` **Step 2: 运行测试验证失败** Run: `composer test` Expected: FAIL with "Class Awesome\Validation\ValidationResult not found" **Step 3: 实现 ValidationResult 类** ```php valid = $valid; $this->errors = $errors; } public function isValid(): bool { return $this->valid; } public function isInvalid(): bool { return !$this->valid; } public function errors(): MessageBag { return $this->errors; } public function firstError(): ?string { $all = $this->errors->all(); if (empty($all)) { return null; } $firstField = array_key_first($all); return $this->errors->first($firstField); } public function toArray(): array { return $this->errors->toArray(); } public function toJson(): string { return $this->errors->toJson(); } public function __toString(): string { return (string)$this->errors; } } ``` **Step 4: 运行测试验证通过** Run: `composer test` Expected: PASS **Step 5: Commit** ```bash git add src/Awesome/Validation/ValidationResult.php tests/Unit/ValidationResultTest.php git commit -m "feat: implement ValidationResult class" ``` --- ## Task 6: 实现 Builder 验证器构建器 **Files:** - Create: `src/Awesome/Validation/Builder.php` - Create: `tests/Unit/BuilderTest.php` **Step 1: 编写 Builder 测试** ```php string(); $result = $builder->validate('hello'); $this->assertTrue($result->isValid()); } public function testStringRuleWithInvalidValue() { $builder = new Builder(); $builder->string(); $result = $builder->validate(123); $this->assertTrue($result->isInvalid()); } public function testChainedRules() { $builder = new Builder(); $builder->string()->minLength(5)->maxLength(10); $result = $builder->validate('hello'); $this->assertTrue($result->isValid()); } public function testChainedRulesWithInvalidValue() { $builder = new Builder(); $builder->string()->minLength(5); $result = $builder->validate('hi'); $this->assertTrue($result->isInvalid()); } public function testCustomMessage() { $builder = new Builder(); $builder->string('必须是字符串'); $result = $builder->validate(123); $this->assertEquals('必须是字符串', $result->firstError()); } public function testMessagesArray() { $builder = new Builder(); $builder->string()->minLength(5)->messages([ 'string' => '必须是字符串', 'minLength' => '至少5个字符' ]); $result = $builder->validate(123); $this->assertEquals('必须是字符串', $result->firstError()); } public function testCheckMethod() { $builder = new Builder(); $builder->string(); $this->assertTrue($builder->check('hello')); $this->assertFalse($builder->check(123)); } } ``` **Step 2: 运行测试验证失败** Run: `composer test` Expected: FAIL with "Class Awesome\Validation\Builder not found" **Step 3: 实现 Builder 类** ```php addRule($rule); return $this; } public function integer(?string $message = null): self { $rule = new IntegerRule($message); $this->addRule($rule); return $this; } public function required(?string $message = null): self { $rule = new RequiredRule($message); $this->addRule($rule); return $this; } public function email(?string $message = null): self { $rule = new EmailRule($message); $this->addRule($rule); return $this; } public function minLength(int $min, ?string $message = null): self { $rule = new MinLengthRule($min, $message); $this->addRule($rule); return $this; } public function maxLength(int $max, ?string $message = null): self { $rule = new MaxLengthRule($max, $message); $this->addRule($rule); return $this; } public function in(array $values, ?string $message = null): self { $rule = new InRule($values, $message); $this->addRule($rule); return $this; } public function regex(string $pattern, ?string $message = null): self { $rule = new RegexRule($pattern, $message); $this->addRule($rule); return $this; } public function custom(callable $callback, ?string $message = null): self { $rule = new CustomRule($callback, $message); $this->addRule($rule); return $this; } public function message(string $message): self { if (!empty($this->rules)) { $lastRule = end($this->rules); $lastRule->setMessage($message); } return $this; } public function messages(array $messages): self { $this->messages = array_merge($this->messages, $messages); return $this; } public function validate(mixed $value = null): ValidationResult { $value = $value ?? $this->value; $errors = new MessageBag(); foreach ($this->rules as $rule) { if (!$rule->validate($value)) { $message = $this->getRuleMessage($rule); $field = $this->field ?? 'value'; $errors->add($field, $message); } } return new ValidationResult($errors->isEmpty(), $errors); } public function check(mixed $value = null): bool { return $this->validate($value)->isValid(); } public function setField(string $field): self { $this->field = $field; return $this; } private function addRule(AbstractRule $rule): void { $this->rules[] = $rule; } private function getRuleMessage(AbstractRule $rule): string { $ruleName = $rule->getName(); if (isset($this->messages[$ruleName])) { return $rule->getFormattedMessage(['field' => $this->field ?? 'value']); } return $rule->getFormattedMessage(['field' => $this->field ?? 'value']); } } ``` **Step 4: 运行测试验证失败(缺少规则类)** Run: `composer test` Expected: FAIL with missing rule classes **Step 5: 实现缺失的规则类** ```php parameters['min'] = $min; } public function validate(mixed $value): bool { if (!is_string($value) && !is_array($value)) { return false; } return strlen($value) >= $this->parameters['min']; } public function getDefaultMessage(): string { return '{field} 长度不能小于 {min}'; } } ``` ```php parameters['max'] = $max; } public function validate(mixed $value): bool { if (!is_string($value) && !is_array($value)) { return false; } return strlen($value) <= $this->parameters['max']; } public function getDefaultMessage(): string { return '{field} 长度不能大于 {max}'; } } ``` ```php parameters['values'] = $values; } public function validate(mixed $value): bool { return in_array($value, $this->parameters['values'], true); } public function getDefaultMessage(): string { return '{field} 必须是以下值之一'; } } ``` ```php parameters['pattern'] = $pattern; } public function validate(mixed $value): bool { if (!is_string($value)) { return false; } return preg_match($this->parameters['pattern'], $value) === 1; } public function getDefaultMessage(): string { return '{field} 格式不正确'; } } ``` ```php callback = $callback; } public function validate(mixed $value): bool { return ($this->callback)($value); } public function getDefaultMessage(): string { return '{field} 验证失败'; } } ``` **Step 6: 运行测试验证通过** Run: `composer test` Expected: PASS **Step 7: Commit** ```bash git add src/Awesome/Validation/Builder.php src/Awesome/Validation/Rules/*.php tests/Unit/BuilderTest.php git commit -m "feat: implement Builder validator" ``` --- ## Task 7: 实现 Validator 门面类 **Files:** - Create: `src/Awesome/Validation/Validator.php` - Create: `tests/Feature/ValidatorTest.php` **Step 1: 编写 Validator 测试** ```php validate('hello'); $this->assertTrue($result->isValid()); } public function testIntegerValidation() { $result = Validator::integer()->validate(123); $this->assertTrue($result->isValid()); } public function testEmailValidation() { $result = Validator::email()->validate('test@example.com'); $this->assertTrue($result->isValid()); } public function testChainedValidation() { $result = Validator::string() ->minLength(5) ->maxLength(10) ->validate('hello'); $this->assertTrue($result->isValid()); } public function testCustomMessage() { $result = Validator::string('必须是字符串')->validate(123); $this->assertEquals('必须是字符串', $result->firstError()); } } ``` **Step 2: 运行测试验证失败** Run: `composer test` Expected: FAIL with "Class Awesome\Validation\Validator not found" **Step 3: 实现 Validator 类** ```php string($message); } public static function integer(?string $message = null): Builder { return (new Builder())->integer($message); } public static function required(?string $message = null): Builder { return (new Builder())->required($message); } public static function email(?string $message = null): Builder { return (new Builder())->email($message); } public static function array(?string $message = null): Builder { return (new Builder())->custom(fn($v) => is_array($v), $message); } public static function make(array $data): Builder { return new Builder($data); } public static function validate(array $data, array $rules): ValidationResult { $errors = new Messages\MessageBag(); foreach ($rules as $field => $ruleString) { $value = self::getNestedValue($data, $field); $builder = new Builder($value); $builder->setField($field); $ruleList = explode('|', $ruleString); foreach ($ruleList as $rule) { self::applyRule($builder, $rule); } $result = $builder->validate(); if ($result->isInvalid()) { foreach ($result->errors()->all() as $fieldErrors) { foreach ($fieldErrors as $error) { $errors->add($field, $error); } } } } return new ValidationResult($errors->isEmpty(), $errors); } private static function getNestedValue(array $data, string $field): mixed { $keys = explode('.', $field); $value = $data; foreach ($keys as $key) { if (!is_array($value) || !array_key_exists($key, $value)) { return null; } $value = $value[$key]; } return $value; } private static function applyRule(Builder $builder, string $rule): void { if (str_contains($rule, ':')) { [$ruleName, $parameters] = explode(':', $rule, 2); $paramArray = explode(',', $parameters); match ($ruleName) { 'min' => $builder->minLength((int)$paramArray[0]), 'max' => $builder->maxLength((int)$paramArray[0]), 'in' => $builder->in($paramArray), 'regex' => $builder->regex($paramArray[0]), default => null }; } else { match ($rule) { 'required' => $builder->required(), 'string' => $builder->string(), 'integer' => $builder->integer(), 'email' => $builder->email(), default => null }; } } } ``` **Step 4: 运行测试验证通过** Run: `composer test` Expected: PASS **Step 5: Commit** ```bash git add src/Awesome/Validation/Validator.php tests/Feature/ValidatorTest.php git commit -m "feat: implement Validator facade" ``` --- ## Task 8: 实现 ValidationException 异常类 **Files:** - Create: `src/Awesome/Validation/Exceptions/ValidationException.php` - Create: `tests/Unit/Exceptions/ValidationExceptionTest.php` **Step 1: 编写 ValidationException 测试** ```php add('username', '用户名必填'); $exception = new ValidationException('验证失败', $errors, 'username'); $this->assertEquals('验证失败', $exception->getMessage()); $this->assertEquals($errors, $exception->getErrors()); $this->assertEquals('username', $exception->getField()); } public function testToArray() { $errors = new MessageBag(); $errors->add('username', '用户名必填'); $exception = new ValidationException('验证失败', $errors); $this->assertEquals(['username' => ['用户名必填']], $exception->toArray()); } public function testToJson() { $errors = new MessageBag(); $errors->add('username', '用户名必填'); $exception = new ValidationException('验证失败', $errors); $expected = json_encode(['username' => ['用户名必填']]); $this->assertEquals($expected, $exception->toJson()); } } ``` **Step 2: 运行测试验证失败** Run: `composer test` Expected: FAIL with "Class Awesome\Validation\Exceptions\ValidationException not found" **Step 3: 实现 ValidationException 类** ```php errors = $errors; $this->field = $field; } public function getErrors(): MessageBag { return $this->errors; } public function getField(): string { return $this->field; } public function toArray(): array { return $this->errors->toArray(); } public function toJson(): string { return $this->errors->toJson(); } } ``` **Step 4: 运行测试验证通过** Run: `composer test` Expected: PASS **Step 5: Commit** ```bash git add src/Awesome/Validation/Exceptions/ValidationException.php tests/Unit/Exceptions/ValidationExceptionTest.php git commit -m "feat: implement ValidationException" ``` --- ## Task 9: 实现规则数组验证功能 **Files:** - Modify: `src/Awesome/Validation/Validator.php` - Create: `tests/Feature/ArrayValidationTest.php` **Step 1: 编写规则数组验证测试** ```php 'john', 'email' => 'john@example.com', 'age' => 25 ]; $result = Validator::validate($data, [ 'username' => 'required|string|min:3', 'email' => 'required|email', 'age' => 'integer|min:18' ]); $this->assertTrue($result->isValid()); } public function testArrayValidationWithErrors() { $data = [ 'username' => 'jo', 'email' => 'invalid-email', 'age' => 15 ]; $result = Validator::validate($data, [ 'username' => 'required|string|min:3', 'email' => 'required|email', 'age' => 'integer|min:18' ]); $this->assertTrue($result->isInvalid()); $this->assertNotEmpty($result->toArray()); } public function testNestedValidation() { $data = [ 'user' => [ 'profile' => [ 'name' => 'John Doe', 'age' => 30 ] ] ]; $result = Validator::validate($data, [ 'user.profile.name' => 'required|string', 'user.profile.age' => 'required|integer|min:18' ]); $this->assertTrue($result->isValid()); } } ``` **Step 2: 运行测试验证通过** Run: `composer test` Expected: PASS **Step 3: Commit** ```bash git add tests/Feature/ArrayValidationTest.php git commit -m "feat: add array validation tests" ``` --- ## Task 10: 实现完整的使用示例和文档 **Files:** - Modify: `README.md` - Create: `examples/basic.php` - Create: `examples/array-validation.php` - Create: `examples/chained-validation.php` **Step 1: 更新 README.md** ```markdown # Awesome Validation 一个友好的 PHP 参数验证库,基于 PHP 8.1+ 语法开发,遵循 PSR-4 规范。 ## 特性 - 多种验证方式:链式调用、规则数组、Attribute - 友好的错误消息:默认中文消息,支持自定义 - 灵活的消息设置:单独设置、批量设置、使用默认消息 - 多格式输出:字符串、数组、JSON - 嵌套验证:支持点号路径访问 - 丰富的上下文信息:字段名、期望值等 ## 安装 ```bash composer require awesome/validation ``` ## 使用示例 ### 链式调用 ```php use Awesome\Validation\Validator; try { Validator::string()->message('必须是字符串') ->minLength(5)->message('至少5个字符') ->maxLength(20)->message('最多20个字符') ->validate($username); } catch (ValidationException $e) { echo $e->getMessage(); } $result = Validator::email()->validate($email); if ($result->isInvalid()) { echo $result->firstError(); } ``` ### 规则数组验证 ```php use Awesome\Validation\Validator; $result = Validator::validate($data, [ 'username' => 'required|string|min:5|max:20', 'email' => 'required|email', 'age' => 'integer|min:18|max:120', 'profile.bio' => 'string|max:500' ]); if ($result->isInvalid()) { print_r($result->errors()->toArray()); } ``` ### 批量设置消息 ```php Validator::string()->minLength(5)->maxLength(20) ->messages([ 'string' => '必须是字符串', 'minLength' => '至少5个字符', 'maxLength' => '最多20个字符' ]) ->validate($username); ``` ## 运行测试 ```bash composer test ``` ## 代码分析 ```bash composer analyze ``` ``` **Step 2: 创建基础示例** ```php validate('hello'); echo "字符串验证: " . ($stringValidation->isValid() ? '通过' : '失败') . "\n"; $emailValidation = Validator::email()->validate('test@example.com'); echo "邮箱验证: " . ($emailValidation->isValid() ? '通过' : '失败') . "\n"; $integerValidation = Validator::integer()->validate(123); echo "整数验证: " . ($integerValidation->isValid() ? '通过' : '失败') . "\n"; ``` **Step 3: 创建链式验证示例** ```php minLength(5) ->maxLength(20) ->validate('hello world'); echo "验证结果: " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->firstError() . "\n"; } $result2 = Validator::string() ->minLength(5)->message('至少5个字符') ->maxLength(20)->message('最多20个字符') ->validate('hi'); echo "\n验证结果: " . ($result2->isValid() ? '通过' : '失败') . "\n"; if ($result2->isInvalid()) { echo "错误信息: " . $result2->firstError() . "\n"; } ``` **Step 4: 创建数组验证示例** ```php 'john', 'email' => 'john@example.com', 'age' => 25, 'profile' => [ 'bio' => 'A developer' ] ]; $result = Validator::validate($data, [ 'username' => 'required|string|min:3', 'email' => 'required|email', 'age' => 'integer|min:18', 'profile.bio' => 'string|max:100' ]); echo "验证结果: " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息:\n"; print_r($result->toArray()); } ``` **Step 5: 运行示例验证** Run: `php examples/basic.php` Expected: 显示基础验证结果 Run: `php examples/chained-validation.php` Expected: 显示链式验证结果 Run: `php examples/array-validation.php` Expected: 显示数组验证结果 **Step 6: Commit** ```bash git add README.md examples/ git commit -m "docs: add usage examples and documentation" ``` --- ## Task 11: 运行完整测试套件 **Files:** - None (verification task) **Step 1: 运行所有测试** Run: `composer test` Expected: All tests PASS **Step 2: 运行代码分析** Run: `composer analyze` Expected: No errors **Step 3: 验证自动加载** Run: `composer dump-autoload` Expected: No errors **Step 4: 最终验证** Run: `php -v && composer --version` Expected: Show PHP and Composer versions **Step 5: Commit** ```bash git add . git commit -m "test: verify complete implementation" ``` --- ## 总结 实现计划已完成,包含以下主要组件: 1. ✅ 项目结构初始化 2. ✅ MessageBag 消息收集器 3. ✅ AbstractRule 规则基类 4. ✅ 基础验证规则(String, Integer, Required, Email等) 5. ✅ ValidationResult 验证结果类 6. ✅ Builder 验证器构建器 7. ✅ Validator 门面类 8. ✅ ValidationException 异常类 9. ✅ 规则数组验证功能 10. ✅ 使用示例和文档 11. ✅ 完整测试套件 该实现满足所有需求: - 支持多种验证方式(链式调用、规则数组) - 友好的错误消息(默认中文,支持自定义) - 灵活的消息设置(单独、批量、默认) - 多格式输出(字符串、数组、JSON) - 嵌套验证(点号路径) - 遵循 PSR-4 规范 - 基于 PHP 8.1+ 语法 docs/plans/2026-04-11-numeric-and-preprocess-design.md000064400000004705151665713370016042 0ustar00# Awesome Validation 增强功能设计方案 (2026-04-11) ## 1. 目标 扩展 Awesome Validation 库,增加更强大的数值校验(最小值、区间)、严格类型校验、大数处理、预处理管道以及数学特征校验。 ## 2. 详细设计 ### 2.1 数值验证增强 (Min/Between) - **MinRule**: 扩展现有规则,支持 `inclusive` 参数。 - API: `$v->min(float $min)->inclusive(bool $inclusive = true)` - **BetweenRule**: 支持数值范围检查。 - API: `$v->between(float $min, float $max)->inclusive(bool $minInclusive = true, bool $maxInclusive = true)` - **实现细节**: - 内部统一将输入转换为 `float` 进行比较。 - 提供清晰的错误消息,如 "value 必须在 [1, 10) 之间"。 ### 2.2 字符串预处理框架 (Preprocessing) - **机制**: 在 `Builder` 中引入预处理管道。 - **Builder 变更**: - 增加 `private mixed $currentValue` 属性,初始值为原始 `$value`。 - 增加 `public function transform(callable $callback): self`。 - 所有后续规则的 `validate()` 方法将作用于 `$currentValue`。 - **缓存**: 在单次验证链中,`transform` 的结果将被缓存。 - **示例**: `$v->transform('trim')->transform('strtolower')->email()`。 ### 2.3 严格数字类型与大数支持 - **StrictIntegerRule**: 严格检查类型是否为 `int`。 - **SafeIntegerRule**: 检查是否在 `PHP_INT_MIN` 到 `PHP_INT_MAX` 之间。 - **进制支持**: - `IntegerRule` 增加进制自动检测逻辑。 - 使用 `filter_var($value, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_OCTAL | FILTER_FLAG_ALLOW_HEX)`。 ### 2.4 数字属性验证库 - **新增规则**: - `EvenRule`: 偶数校验 (`$value % 2 === 0`)。 - `OddRule`: 奇数校验 (`$value % 2 !== 0`)。 - `PositiveRule`: 正数校验 (`$value > 0`)。 - `NegativeRule`: 负数校验 (`$value < 0`)。 - `PrimeRule`: 质数校验。 - **PrimeRule 性能优化**: - 使用试除法 (Trial Division) 结合 $\sqrt{n}$ 优化。 - 增加大数安全检查。 ## 3. 架构影响 - `Builder` 将不再是无状态的(在单次验证链中),它需要跟踪 `$currentValue`。 - `AbstractRule` 可能需要微调以更好地支持 `field` 名称的动态获取(尤其是在预处理后)。 ## 4. 测试策略 - 为每个新规则编写 Unit Tests。 - 编写 Feature Tests 验证 `transform` 管道的流转正确性。 - 增加大数、浮点数精度等边缘情况的测试用例。 docs/plans/2026-04-11-numeric-preprocess-implementation.md000064400000012410151665713370017046 0ustar00# Numeric and Preprocess Enhancements Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Extend validation rules for numeric range, strict types, mathematical properties, and implement a transformation pipeline. **Architecture:** Add a `transform` pipeline to `Builder` to allow pre-validation data modification. Implement specialized numeric rules (Min, Between, StrictInteger, Prime, etc.) using PHP's native filters and optimized algorithms. **Tech Stack:** PHP 8.1+, filter_var, BCMath (optional for precision). --- ### Task 1: Implement Enhanced MinRule and BetweenRule **Files:** - Create: `src/Awesome/Validation/Rules/MinRule.php` - Create: `src/Awesome/Validation/Rules/BetweenRule.php` - Modify: `src/Awesome/Validation/Builder.php` - Test: `tests/Unit/Rules/MinRuleTest.php`, `tests/Unit/Rules/BetweenRuleTest.php` **Step 1: Create MinRule with inclusive support** ```php namespace Awesome\Validation\Rules; class MinRule extends AbstractRule { protected string $name = 'min'; public function __construct(protected float $min, protected bool $inclusive = true, ?string $message = null) { parent::__construct($message); } public function validate(mixed $value): bool { if (!is_numeric($value)) return false; return $this->inclusive ? $value >= $this->min : $value > $this->min; } public function getDefaultMessage(): string { return $this->inclusive ? "{field} 必须大于或等于 {$this->min}" : "{field} 必须大于 {$this->min}"; } } ``` **Step 2: Create BetweenRule with flexible boundaries** ```php namespace Awesome\Validation\Rules; class BetweenRule extends AbstractRule { protected string $name = 'between'; public function __construct(protected float $min, protected float $max, protected bool $minInclusive = true, protected bool $maxInclusive = true, ?string $message = null) { parent::__construct($message); } public function validate(mixed $value): bool { if (!is_numeric($value)) return false; $low = $this->minInclusive ? $value >= $this->min : $value > $this->min; $high = $this->maxInclusive ? $value <= $this->max : $value < $this->max; return $low && $high; } } ``` **Step 3: Add methods to Builder** ```php public function min(float $min, bool $inclusive = true, ?string $message = null): self { return $this->addRule(new Rules\MinRule($min, $inclusive, $message)); } public function between(float $min, float $max, bool $minInc = true, bool $maxInc = true, ?string $message = null): self { return $this->addRule(new Rules\BetweenRule($min, $max, $minInc, $maxInc, $message)); } ``` --- ### Task 2: Build Transformation (Preprocessing) Framework **Files:** - Modify: `src/Awesome/Validation/Builder.php` - Test: `tests/Feature/TransformTest.php` **Step 1: Add transformation state to Builder** ```php private mixed $currentValue; private array $transformations = []; // Update constructor public function __construct(private mixed $value = null) { $this->currentValue = $value; } public function transform(callable $callback): self { $this->transformations[] = $callback; $this->currentValue = $callback($this->currentValue); return $this; } ``` **Step 2: Update validate methods to use currentValue** ```php public function validate(mixed $value = null): ValidationResult { $val = $value !== null ? $this->applyTransformations($value) : $this->currentValue; // ... loop rules using $val } private function applyTransformations(mixed $value): mixed { foreach ($this->transformations as $callback) { $value = $callback($value); } return $value; } ``` --- ### Task 3: Implement Strict Integer and Base Detection **Files:** - Create: `src/Awesome/Validation/Rules/StrictIntegerRule.php` - Modify: `src/Awesome/Validation/Rules/IntegerRule.php` - Test: `tests/Unit/Rules/IntegerTypeTest.php` **Step 1: Strict Integer (type check)** ```php public function validate(mixed $value): bool { return is_int($value); } ``` **Step 2: Update IntegerRule for base detection** ```php public function validate(mixed $value): bool { return filter_var($value, FILTER_VALIDATE_INT, FILTER_FLAG_ALLOW_OCTAL | FILTER_FLAG_ALLOW_HEX) !== false; } ``` --- ### Task 4: Implement Mathematical Property Rules (Even, Odd, Prime) **Files:** - Create: `src/Awesome/Validation/Rules/EvenRule.php` - Create: `src/Awesome/Validation/Rules/OddRule.php` - Create: `src/Awesome/Validation/Rules/PrimeRule.php` **Step 1: Even/Odd Rules** ```php // Even: $value % 2 === 0 // Odd: $value % 2 !== 0 ``` **Step 2: Optimized PrimeRule** ```php public function validate(mixed $value): bool { if (!is_numeric($value) || $value < 2) return false; $n = (int)$value; if ($n === 2 || $n === 3) return true; if ($n % 2 === 0 || $n % 3 === 0) return false; for ($i = 5; $i * $i <= $n; $i += 6) { if ($n % $i === 0 || $n % ($i + 2) === 0) return false; } return true; } ``` --- ### Task 5: Final Integration and Documentation **Files:** - Modify: `RULES.md` - Create: `examples/advanced_numeric.php` **Step 1: Update documentation with new rules and transform example.** **Step 2: Create a complex example showing transform -> numeric -> between -> prime pipeline.** examples/advanced_numeric.php000064400000004154151665713370012404 0ustar00 ' 101 ', // 需要 trim 且是质数 'score' => '85.5', // 需要在 [0, 100] 之间 'age' => 26, // 修正为偶数 'hex_val' => '0x1F', // 进制检测支持 'email' => ' Admin@Example.Com ', // 预处理转换 ]; $result = Validator::validateKeys($data, function($v) { return [ // 1. 预处理 + 质数校验 $v->key('user_id')->transform('trim')->numeric()->prime(), // 2. 数值区间校验 (闭区间) $v->key('score')->numeric()->between(0, 100), // 3. 正数 + 偶数校验 $v->key('age')->numeric()->positive()->even(), // 4. 自动进制检测 $v->key('hex_val')->integer()->min(0), // 5. 链式预处理 + 格式校验 $v->key('email')->transform('trim')->transform('strtolower')->email(), ]; }); if ($result->isValid()) { echo "✅ 所有高级校验均通过!\n"; echo "验证日志:\n"; foreach ($result->getLogs() as $log) { echo " - [{$log['status']}] {$log['field']}: {$log['message']}\n"; } } else { echo "❌ 校验失败:\n"; print_r($result->errors()->all()); } echo "\n=== 边界情况演示 ===\n"; $failData = [ 'odd_num' => 4, // 期望奇数 'range' => 10, // 期望 (0, 10) 开区间 'large' => '9223372036854775808', // 超出 PHP 64位整数范围 ]; $result2 = Validator::validateKeys($failData, function($v) { return [ $v->key('odd_num')->numeric()->odd(), $v->key('range')->numeric()->between(0, 10, false, false), $v->key('large')->safeInteger(), ]; }); if ($result2->isInvalid()) { echo "捕获到预期错误:\n"; foreach ($result2->errors()->all() as $field => $errors) { $msg = is_array($errors) ? implode(', ', $errors) : $errors; echo " - {$field}: {$msg}\n"; } } echo "\n=== 示例结束 ===\n"; examples/array_validation.php000064400000011174151665713370012445 0ustar00 'john_doe', 'email' => 'john@example.com', 'age' => '25', 'status' => 'active' ]; $rules = [ 'username' => 'required|min:5|max:20', 'email' => 'required|email', 'age' => 'required|integer', 'status' => 'required|in:active,inactive,pending' ]; $result = Validator::validate($data, $rules); if ($result->isValid()) { echo "✓ 所有字段验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example2 = function() { echo "示例2: 带自定义错误消息的验证\n"; $data = [ 'username' => 'ab', 'email' => 'invalid-email', 'password' => '123' ]; $rules = [ 'username' => 'required|min:5,message:用户名至少需要5个字符', 'email' => 'required|email,message:请输入有效的邮箱地址', 'password' => 'required|min:8,message:密码至少需要8个字符' ]; $result = Validator::validate($data, $rules); if ($result->isValid()) { echo "✓ 验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example3 = function() { echo "示例3: 嵌套字段验证\n"; $data = [ 'user' => [ 'profile' => [ 'name' => 'John Doe', 'email' => 'john@example.com', 'age' => '30' ] ] ]; $rules = [ 'user.profile.name' => 'required|min:2', 'user.profile.email' => 'required|email', 'user.profile.age' => 'required|integer' ]; $result = Validator::validate($data, $rules); if ($result->isValid()) { echo "✓ 嵌套字段验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example4 = function() { echo "示例4: 数组项验证\n"; $data = [ ['name' => 'John', 'age' => '25'], ['name' => 'Jane', 'age' => '30'], ['name' => 'Bob', 'age' => 'not-a-number'] ]; $rules = [ 'name' => 'required|min:2', 'age' => 'required|integer' ]; $result = Validator::validateNested($data, $rules); if ($result->isValid()) { echo "✓ 所有数组项验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example5 = function() { echo "示例5: 使用 validateOrFail 抛出异常\n"; $data = [ 'username' => 'ab', 'email' => 'invalid' ]; $rules = [ 'username' => 'required|min:5', 'email' => 'required|email' ]; try { Validator::validateOrFail($data, $rules); echo "✓ 验证通过\n"; } catch (\Awesome\Validation\ValidationException $e) { echo "✗ 验证失败,抛出异常:\n"; echo "异常消息: " . $e->getMessage() . "\n"; echo "错误详情:\n"; foreach ($e->toArray() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example6 = function() { echo "示例6: 正则表达式验证\n"; $data = [ 'phone' => '13800138000', 'id_card' => '110101199001011234' ]; $rules = [ 'phone' => 'required|regex:/^1[3-9]\d{9}$/', 'id_card' => 'required|regex:/^\d{17}[\dXx]$/' ]; $result = Validator::validate($data, $rules); if ($result->isValid()) { echo "✓ 正则验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example1(); $example2(); $example3(); $example4(); $example5(); $example6(); echo "=== 数组验证示例完成 ===\n"; examples/basic_validation.php000064400000004354151665713370012412 0ustar00minLength(5) ->maxLength(20) ->validate('hello world'); if ($result->isValid()) { echo "✓ 验证通过\n"; } else { echo "✗ 验证失败\n"; print_r($result->errors()->all()); } echo "\n"; }; $example2 = function() { echo "示例2: 邮箱验证\n验证邮箱: test@example.com\n"; $result = Validator::email()->validate('test@example.com'); if ($result->isValid()) { echo "✓ 邮箱格式正确\n"; } else { echo "✗ 邮箱格式错误\n"; print_r($result->errors()->all()); } echo "\n"; }; $example3 = function() { echo "示例3: 整数验证\n验证年龄: 25\n"; $result = Validator::integer() ->minLength(1) ->maxLength(3) ->validate('25'); if ($result->isValid()) { echo "✓ 年龄验证通过\n"; } else { echo "✗ 年龄验证失败\n"; print_r($result->errors()->all()); } echo "\n"; }; $example4 = function() { echo "示例4: 必填字段验证\n验证空值\n"; $result = Validator::required()->validate(''); if ($result->isValid()) { echo "✓ 验证通过\n"; } else { echo "✗ 验证失败\n"; print_r($result->errors()->all()); } echo "\n"; }; $example5 = function() { echo "示例5: 自定义验证规则\n验证密码强度\n"; $result = Validator::string() ->minLength(8) ->custom(function($value) { return preg_match('/[A-Z]/', $value) && preg_match('/[0-9]/', $value); }, '密码必须包含大写字母和数字') ->validate('Password123'); if ($result->isValid()) { echo "✓ 密码强度验证通过\n"; } else { echo "✗ 密码强度验证失败\n"; print_r($result->errors()->all()); } echo "\n"; }; $example1(); $example2(); $example3(); $example4(); $example5(); echo "=== 基础验证示例完成 ===\n"; examples/common_validation.php000064400000010265151665713370012617 0ustar00validate('你好世界'); echo "验证 '你好世界' (2-10个字符): " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::chineseLength(2, 10)->validate('你'); echo "验证 '你' (2-10个字符): " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->errors()->first('value') . "\n"; } echo "\n2. 手机号验证\n"; $result = Validator::phone()->validate('13800138000'); echo "验证 '13800138000': " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::phone()->validate('12800138000'); echo "验证 '12800138000': " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->errors()->first('value') . "\n"; } echo "\n3. IP地址验证 (IPv4)\n"; $result = Validator::ip('v4')->validate('192.168.1.1'); echo "验证 '192.168.1.1': " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::ip('v4')->validate('256.168.1.1'); echo "验证 '256.168.1.1': " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->errors()->first('value') . "\n"; } echo "\n4. IP地址验证 (IPv6)\n"; $result = Validator::ip('v6')->validate('2001:0db8:85a3:0000:0000:8a2e:0370:7334'); echo "验证 '2001:0db8:85a3:0000:0000:8a2e:0370:7334': " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::ip('v6')->validate('192.168.1.1'); echo "验证 '192.168.1.1': " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->errors()->first('value') . "\n"; } echo "\n5. 域名验证\n"; $result = Validator::domain()->validate('example.com'); echo "验证 'example.com': " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::domain()->validate('example'); echo "验证 'example': " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->errors()->first('value') . "\n"; } echo "\n6. 组合验证 (用户注册表单)\n"; $data = [ 'username' => '张三', 'phone' => '13800138000', 'ip' => '192.168.1.1', 'domain' => 'example.com' ]; $result = Validator::validateKeys($data, function($validator) use ($data) { return [ $validator->key('username')->required()->chineseLength(2, 10), $validator->key('phone')->required()->phone(), $validator->key('ip')->required()->ip('v4'), $validator->key('domain')->required()->domain() ]; }); if ($result->isValid()) { echo "所有字段验证通过!\n"; } else { echo "验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { foreach ($errors as $error) { echo " {$field}: {$error}\n"; } } } echo "\n7. 数组元素验证\n"; $phones = ['13800138000', '13912345678', '12800138000']; $result = Validator::validateKeys($phones, function($validator) { return [ $validator->key('0')->phone(), $validator->key('1')->phone(), $validator->key('2')->phone() ]; }); if ($result->isValid()) { echo "所有手机号验证通过!\n"; } else { echo "验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { foreach ($errors as $error) { echo " {$field}: {$error}\n"; } } } echo "\n8. 使用 each() 验证数组中的每个元素\n"; $domains = ['example.com', 'test.org', 'invalid']; $result = Validator::validateKeys($domains, function($validator) { return [ $validator->key('0')->domain(), $validator->key('1')->domain(), $validator->key('2')->domain() ]; }); if ($result->isValid()) { echo "所有域名验证通过!\n"; } else { echo "验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { foreach ($errors as $error) { echo " {$field}: {$error}\n"; } } } echo "\n=== 示例完成 ===\n"; examples/date_validation.php000064400000007132151665713370012243 0ustar00validate('2024-01-01'); echo "验证 '2024-01-01': " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::date()->validate('2024-12-31'); echo "验证 '2024-12-31': " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::date()->validate('2024-01-15 10:30:00'); echo "验证 '2024-01-15 10:30:00': " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::date()->validate('not-a-date'); echo "验证 'not-a-date': " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->errors()->first('value') . "\n"; } echo "\n2. 指定格式日期验证 (Y-m-d)\n"; $result = Validator::date('Y-m-d')->validate('2024-01-01'); echo "验证 '2024-01-01': " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::date('Y-m-d')->validate('2024/01/01'); echo "验证 '2024/01/01': " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->errors()->first('value') . "\n"; } echo "\n3. 中文日期格式验证 (Y年m月d日)\n"; $result = Validator::date('Y年m月d日')->validate('2024年01月01日'); echo "验证 '2024年01月01日': " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::date('Y年m月d日')->validate('2024-01-01'); echo "验证 '2024-01-01': " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->errors()->first('value') . "\n"; } echo "\n4. 时间格式验证 (H:i:s)\n"; $result = Validator::date('H:i:s')->validate('10:30:00'); echo "验证 '10:30:00': " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = Validator::date('H:i:s')->validate('25:00:00'); echo "验证 '25:00:00': " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->errors()->first('value') . "\n"; } echo "\n5. 日期值转换\n"; $rule = new \Awesome\Validation\Rules\DateRule('Y-m-d'); $converted = $rule->getConvertedValue('2024-01-01'); echo "转换 '2024-01-01' 为 DateTime 对象: " . ($converted instanceof \DateTime ? '成功' : '失败') . "\n"; if ($converted instanceof \DateTime) { echo "格式化后的日期: " . $converted->format('Y-m-d H:i:s') . "\n"; } echo "\n6. 组合验证(用户注册表单)\n"; $data = [ 'birthday' => '1990-01-01', 'start_date' => '2024-01-01', 'end_date' => '2024-12-31', 'appointment' => '2024-06-15 14:30' ]; $result = Validator::validateKeys($data, function($validator) { return [ $validator->key('birthday')->required()->date('Y-m-d'), $validator->key('start_date')->required()->date('Y-m-d'), $validator->key('end_date')->required()->date('Y-m-d'), $validator->key('appointment')->required()->date('Y-m-d H:i') ]; }); if ($result->isValid()) { echo "所有日期字段验证通过!\n"; } else { echo "验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { foreach ($errors as $error) { echo " {$field}: {$error}\n"; } } } echo "\n7. 自定义错误消息\n"; $result = Validator::date('Y-m-d', '请输入有效的日期格式(YYYY-MM-DD)')->validate('invalid-date'); if ($result->isInvalid()) { echo "自定义错误消息: " . $result->errors()->first('value') . "\n"; } echo "\n=== 示例完成 ===\n";examples/each_validation.php000064400000013112151665713370012221 0ustar00 ['1', '2', '3', '4', '5'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('numbers')->each()->integer() ]; }); if ($result->isValid()) { echo "✓ 所有数字验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example2 = function() { echo "示例2: 验证邮箱数组\n"; $data = [ 'emails' => [ 'john@example.com', 'jane@example.com', 'invalid-email' ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('emails')->each()->email() ]; }); if ($result->isValid()) { echo "✓ 所有邮箱验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example3 = function() { echo "示例3: 验证字符串数组(最小长度)\n"; $data = [ 'names' => ['John', 'Jane', 'A', 'Bob'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('names')->each()->string()->minLength(2) ]; }); if ($result->isValid()) { echo "✓ 所有名称验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example4 = function() { echo "示例4: 验证状态数组(in 规则)\n"; $data = [ 'statuses' => ['active', 'inactive', 'invalid'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('statuses')->each()->in(['active', 'inactive', 'pending']) ]; }); if ($result->isValid()) { echo "✓ 所有状态验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example5 = function() { echo "示例5: 验证年龄数组(自定义错误消息)\n"; $data = [ 'ages' => ['25', '30', 'not-a-number', '35'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('ages')->each()->integer('年龄必须是整数') ]; }); if ($result->isValid()) { echo "✓ 所有年龄验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example6 = function() { echo "示例6: 验证空数组\n"; $data = [ 'items' => [] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('items')->each()->string() ]; }); if ($result->isValid()) { echo "✓ 空数组验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example7 = function() { echo "示例7: 混合验证(普通字段 + 数组字段)\n"; $data = [ 'username' => 'john_doe', 'emails' => [ 'john@example.com', 'jane@example.com', 'invalid-email' ], 'ages' => ['25', '30', '35'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string()->minLength(5), $builder->key('emails')->each()->email(), $builder->key('ages')->each()->integer() ]; }); if ($result->isValid()) { echo "✓ 混合验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example8 = function() { echo "示例8: 嵌套数组验证\n"; $data = [ 'data' => [ 'users' => [ ['name' => 'John', 'age' => '25'], ['name' => 'Jane', 'age' => '30'] ] ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('data.users')->each()->string()->integer() ]; }); if ($result->isValid()) { echo "✓ 嵌套数组验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example1(); $example2(); $example3(); $example4(); $example5(); $example6(); $example7(); $example8(); echo "=== Each 验证示例完成 ===\n"; examples/json_validation.php000064400000010221151665713370012270 0ustar00validate('{"name": "John", "age": 30}'); echo "验证简单 JSON 对象: " . ($result ? '通过' : '失败') . "\n"; $result = $rule->validate('["apple", "banana", "orange"]'); echo "验证 JSON 数组: " . ($result ? '通过' : '失败') . "\n"; $result = $rule->validate('not-json'); echo "验证无效 JSON: " . ($result ? '通过' : '失败') . "\n"; echo "\n2. 验证嵌套 JSON\n"; $nestedJson = '{ "user": { "name": "John", "address": { "city": "New York", "zip": "10001" } } }'; $result = $rule->validate($nestedJson); echo "验证嵌套 JSON 对象: " . ($result ? '通过' : '失败') . "\n"; echo "\n3. 验证带特殊字符的 JSON\n"; $specialJson = '{"message": "Hello\\nWorld!", "emoji": "😀"}'; $result = $rule->validate($specialJson); echo "验证带特殊字符的 JSON: " . ($result ? '通过' : '失败') . "\n"; echo "\n4. 验证空 JSON\n"; $result = $rule->validate('{}'); echo "验证空对象: " . ($result ? '通过' : '失败') . "\n"; $result = $rule->validate('[]'); echo "验证空数组: " . ($result ? '通过' : '失败') . "\n"; echo "\n5. 设置最大深度限制\n"; $deepJson = '{"level1": {"level2": {"level3": "value"}}}'; $rule = new \Awesome\Validation\Rules\JsonRule(5); $result = $rule->validate($deepJson); echo "验证深度为 3 的 JSON (限制 5): " . ($result ? '通过' : '失败') . "\n"; $rule = new \Awesome\Validation\Rules\JsonRule(2); $result = $rule->validate($deepJson); echo "验证深度为 3 的 JSON (限制 2): " . ($result ? '通过' : '失败') . "\n"; echo "\n6. JSON 值转换\n"; $rule = new \Awesome\Validation\Rules\JsonRule(); $converted = $rule->getConvertedValue('{"name": "John", "age": 30}'); echo "转换 JSON 为 PHP 数组: " . (is_array($converted) ? '成功' : '失败') . "\n"; if (is_array($converted)) { echo "转换结果: " . json_encode($converted, JSON_UNESCAPED_UNICODE) . "\n"; } echo "\n7. 使用 JSON_OBJECT_AS_ARRAY 标志\n"; $rule = new \Awesome\Validation\Rules\JsonRule(10, JSON_OBJECT_AS_ARRAY); $converted = $rule->getConvertedValue('{"name": "John", "age": 30}'); echo "转换结果类型: " . (is_array($converted) ? 'array' : 'object') . "\n"; echo "\n8. 组合验证(API 配置)\n"; $data = [ 'config' => '{"timeout": 30, "retries": 3}', 'metadata' => '{"version": "1.0.0", "author": "John"}', 'settings' => '{"theme": "dark", "language": "zh-CN"}' ]; $result = Validator::validateKeys($data, function($validator) { return [ $validator->key('config')->required()->json(), $validator->key('metadata')->required()->json(), $validator->key('settings')->required()->json() ]; }); if ($result->isValid()) { echo "所有 JSON 字段验证通过!\n"; } else { echo "验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { foreach ($errors as $error) { echo " {$field}: {$error}\n"; } } } echo "\n9. 自定义错误消息\n"; $result = Validator::json(10, 0, '请输入有效的 JSON 格式')->validate('invalid-json'); if ($result->isInvalid()) { echo "自定义错误消息: " . $result->errors()->first('value') . "\n"; } echo "\n10. 验证大型 JSON 数据\n"; $largeArray = json_encode(range(1, 1000)); $rule = new \Awesome\Validation\Rules\JsonRule(); $result = $rule->validate($largeArray); echo "验证包含 1000 个元素的数组: " . ($result ? '通过' : '失败') . "\n"; echo "\n11. 验证带布尔值的 JSON\n"; $boolJson = '{"active": true, "deleted": false, "nullable": null}'; $result = $rule->validate($boolJson); echo "验证带布尔值和 null 的 JSON: " . ($result ? '通过' : '失败') . "\n"; echo "\n12. 验证带数字的 JSON\n"; $numberJson = '{"integer": 42, "float": 3.14, "negative": -10, "scientific": 1.5e3}'; $result = $rule->validate($numberJson); echo "验证带各种数字类型的 JSON: " . ($result ? '通过' : '失败') . "\n"; echo "\n=== 示例完成 ===\n"; examples/key_validation.php000064400000014446151665713370012124 0ustar00 'john_doe', 'email' => 'john@example.com', 'age' => '25' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string()->minLength(5), $builder->key('email')->required()->email(), $builder->key('age')->required()->integer() ]; }); if ($result->isValid()) { echo "✓ 所有字段验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example2 = function() { echo "示例2: 带自定义错误消息的 key 验证\n"; $data = [ 'username' => 'ab', 'email' => 'invalid-email', 'password' => '123' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string()->minLength(5, '用户名至少需要5个字符'), $builder->key('email')->required()->email('请输入有效的邮箱地址'), $builder->key('password')->required()->string()->minLength(8, '密码至少需要8个字符') ]; }); if ($result->isValid()) { echo "✓ 验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example3 = function() { echo "示例3: 嵌套字段 key 验证\n"; $data = [ 'user' => [ 'profile' => [ 'name' => 'John Doe', 'email' => 'john@example.com', 'age' => '30' ] ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('user.profile.name')->required()->string()->minLength(2), $builder->key('user.profile.email')->required()->email(), $builder->key('user.profile.age')->required()->integer() ]; }); if ($result->isValid()) { echo "✓ 嵌套字段验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example4 = function() { echo "示例4: 使用 in 和 regex 规则\n"; $data = [ 'status' => 'active', 'priority' => 'high', 'phone' => '13800138000' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('status')->required()->in(['active', 'inactive', 'pending']), $builder->key('priority')->required()->in(['low', 'medium', 'high']), $builder->key('phone')->required()->regex('/^1[3-9]\d{9}$/') ]; }); if ($result->isValid()) { echo "✓ 验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example5 = function() { echo "示例5: 自定义验证规则\n"; $data = [ 'password' => 'Password123' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('password') ->required() ->string() ->minLength(8) ->custom(function($value) { return preg_match('/[A-Z]/', $value) && preg_match('/[0-9]/', $value); }, '密码必须包含大写字母和数字') ]; }); if ($result->isValid()) { echo "✓ 密码强度验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example6 = function() { echo "示例6: 使用 Builder 的 key() 方法\n"; $data = [ 'username' => 'john', 'email' => 'john@example.com' ]; $builder = Validator::make($data); $result = $builder->key('username') ->required() ->string() ->minLength(3) ->validate(); if ($result->isValid()) { echo "✓ 用户名验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example7 = function() { echo "示例7: 复杂数据结构验证\n"; $data = [ 'username' => 'shenzhao', 'balance' => 67.89, 'brand_id' => 37, 'user' => [ 'profile' => [ 'name' => 'John', 'email' => 'john@example.com' ] ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string()->minLength(3), $builder->key('balance')->required()->custom(function($value) { return is_numeric($value) && $value >= 0; }, '余额必须是非负数'), $builder->key('brand_id')->required()->integer(), $builder->key('user.profile.name')->required()->string()->minLength(2), $builder->key('user.profile.email')->required()->email() ]; }); if ($result->isValid()) { echo "✓ 复杂数据结构验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example1(); $example2(); $example3(); $example4(); $example5(); $example6(); $example7(); echo "=== Key 验证示例完成 ===\n"; examples/max_validation.php000064400000005705151665713370012117 0ustar00numeric()->max(100, false)->validate(50); echo "值 50 验证结果: " . ($result->isValid() ? '通过' : '失败') . "\n"; $result = $builder->numeric()->max(100, false)->validate(100); echo "值 100 验证结果: " . ($result->isValid() ? '通过' : '失败') . " (exclusive: 等于100应该失败)\n"; $result = $builder->numeric()->max(100, false)->validate(150); echo "值 150 验证结果: " . ($result->isValid() ? '通过' : '失败') . "\n\n"; // 示例 2: 使用 Builder 进行 inclusive 最大值验证(<=) echo "示例 2: Inclusive Max (<= 100)\n"; $builder2 = new Builder(); $result = $builder2->numeric()->max(100, true)->validate(100); echo "值 100 验证结果: " . ($result->isValid() ? '通过' : '失败') . " (inclusive: 等于100应该通过)\n"; $result = $builder2->numeric()->max(100, true)->validate(99); echo "值 99 验证结果: " . ($result->isValid() ? '通过' : '失败') . "\n\n"; // 示例 3: 数组验证场景 echo "示例 3: 在数组验证中使用 max()\n"; $data = [ 'age' => 25, 'score' => 95, 'temperature' => 36.5, ]; $result = Validator::validateKeys($data, function($v) { return [ $v->key('age')->numeric()->max(150, true), // 年龄 <= 150 $v->key('score')->numeric()->max(100, true), // 分数 <= 100 $v->key('temperature')->numeric()->max(42, false), // 体温 < 42 ]; }); echo "验证结果: " . ($result->isValid() ? '全部通过' : '有失败项') . "\n\n"; // 示例 4: 验证失败的情况 echo "示例 4: 验证失败的场景\n"; $failData = [ 'age' => 200, // 超过了150 'percentage' => 100, // exclusive 模式下等于100会失败 ]; $result = Validator::validateKeys($failData, function($v) { return [ $v->key('age')->numeric()->max(150, true), $v->key('percentage')->numeric()->max(100, false), // exclusive 模式 ]; }); echo "验证结果: " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . (is_array($errors) ? implode(', ', $errors) : $errors) . "\n"; } } echo "\n"; // 示例 5: 自定义错误消息 echo "示例 5: 使用自定义错误消息\n"; $builder5 = new Builder(); $result = $builder5->numeric()->max(100, true, '分数不能超过100分')->validate(150); echo "验证结果: " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { // 使用 firstError() 获取第一个错误 $errorMessage = $result->firstError(); echo "错误信息: {$errorMessage}\n"; } echo "\n=== 示例结束 ===\n"; examples/numeric_validation.php000064400000011346151665713370012772 0ustar00 '99.99', 'quantity' => '10', 'discount' => '0.5' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('price')->numeric(), $builder->key('quantity')->numeric(), $builder->key('discount')->numeric() ]; }); if ($result->isValid()) { echo "✓ 所有数字验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example2 = function() { echo "示例2: 验证无效数字\n"; $data = [ 'price' => 'not-a-number', 'quantity' => 'abc', 'discount' => null ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('price')->numeric(), $builder->key('quantity')->numeric(), $builder->key('discount')->numeric() ]; }); if ($result->isValid()) { echo "✓ 验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example3 = function() { echo "示例3: 使用 Builder 的 numeric() 方法\n"; $result = Validator::numeric()->validate('123.45'); if ($result->isValid()) { echo "✓ 数字验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example4 = function() { echo "示例4: 验证数字数组\n"; $data = [ 'prices' => ['99.99', '10.50', 'abc', '25.00'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('prices')->each()->numeric() ]; }); if ($result->isValid()) { echo "✓ 所有数字验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example5 = function() { echo "示例5: 自定义错误消息\n"; $data = [ 'amount' => 'invalid' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('amount')->numeric('金额必须是数字') ]; }); if ($result->isValid()) { echo "✓ 验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example6 = function() { echo "示例6: 混合验证(numeric + integer)\n"; $data = [ 'price' => '99.99', 'quantity' => '10', 'total' => '999.99' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('price')->numeric(), $builder->key('quantity')->integer(), $builder->key('total')->numeric() ]; }); if ($result->isValid()) { echo "✓ 混合验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example7 = function() { echo "示例7: 负数和零值验证\n"; $data = [ 'balance' => '-100.50', 'discount' => '0', 'profit' => '250.75' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('balance')->numeric(), $builder->key('discount')->numeric(), $builder->key('profit')->numeric() ]; }); if ($result->isValid()) { echo "✓ 负数和零值验证通过\n"; } else { echo "✗ 验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { echo " - {$field}: " . implode(', ', $errors) . "\n"; } } echo "\n"; }; $example1(); $example2(); $example3(); $example4(); $example5(); $example6(); $example7(); echo "=== Numeric 验证示例完成 ===\n"; examples/sanitize_validation.php000064400000007774151665713370013170 0ustar00validate(''); echo "验证 '': " . ($result ? '通过' : '失败') . "\n"; $cleaned = $rule->getConvertedValue(''); echo "清理后的值: " . $cleaned . "\n"; echo "\n2. URL 编码\n"; $rule = new \Awesome\Validation\Rules\SanitizeRule('url'); $result = $rule->validate('hello world & test'); echo "验证 'hello world & test': " . ($result ? '通过' : '失败') . "\n"; $encoded = $rule->getConvertedValue('hello world & test'); echo "URL 编码后的值: " . $encoded . "\n"; echo "\n3. 邮箱地址清理\n"; $rule = new \Awesome\Validation\Rules\SanitizeRule('email'); $result = $rule->validate(' USER@EXAMPLE.COM '); echo "验证 ' USER@EXAMPLE.COM ': " . ($result ? '通过' : '失败') . "\n"; $cleanedEmail = $rule->getConvertedValue(' USER@EXAMPLE.COM '); echo "清理后的邮箱: " . $cleanedEmail . "\n"; echo "\n4. 数字清理\n"; $rule = new \Awesome\Validation\Rules\SanitizeRule('number'); $result = $rule->validate('123abc456'); echo "验证 '123abc456': " . ($result ? '通过' : '失败') . "\n"; $cleanedNumber = $rule->getConvertedValue('123abc456'); echo "清理后的数字: " . $cleanedNumber . "\n"; echo "\n5. 浮点数清理\n"; $rule = new \Awesome\Validation\Rules\SanitizeRule('float'); $result = $rule->validate('3.14abc'); echo "验证 '3.14abc': " . ($result ? '通过' : '失败') . "\n"; $cleanedFloat = $rule->getConvertedValue('3.14abc'); echo "清理后的浮点数: " . $cleanedFloat . "\n"; echo "\n6. 使用自定义选项\n"; $rule = new \Awesome\Validation\Rules\SanitizeRule('special_chars', ['flags' => FILTER_FLAG_STRIP_LOW]); $result = $rule->validate('test' . chr(1) . 'string'); echo "验证带控制字符的字符串: " . ($result ? '通过' : '失败') . "\n"; $cleaned = $rule->getConvertedValue('test' . chr(1) . 'string'); echo "清理后的值: " . bin2hex($cleaned) . "\n"; echo "\n7. 组合验证(用户输入清理)\n"; $data = [ 'username' => ' john_doe ', 'comment' => ' Nice post!', 'website' => 'https://example.com/path with spaces', 'age' => '25 years old' ]; $result = Validator::validateKeys($data, function($validator) { return [ $validator->key('username')->required()->sanitize('special_chars'), $validator->key('comment')->required()->sanitize('special_chars'), $validator->key('website')->required()->sanitize('url'), $validator->key('age')->required()->sanitize('number') ]; }); if ($result->isValid()) { echo "所有字段清理验证通过!\n"; echo "清理后的数据:\n"; foreach ($data as $key => $value) { echo " {$key}: {$value}\n"; } } else { echo "验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { foreach ($errors as $error) { echo " {$field}: {$error}\n"; } } } echo "\n8. 自定义错误消息\n"; $result = Validator::sanitize('special_chars', [], '输入包含非法字符')->validate(''); if ($result->isInvalid()) { echo "自定义错误消息: " . $result->errors()->first('value') . "\n"; } echo "\n9. 链式清理\n"; $rule = new \Awesome\Validation\Rules\SanitizeRule('special_chars'); $cleaned = $rule->getConvertedValue(' '); $cleaned = trim($cleaned); echo "先清理特殊字符再去除空格: " . $cleaned . "\n"; echo "\n10. SQL 注入防护示例\n"; $maliciousInput = "admin' OR '1'='1"; $rule = new \Awesome\Validation\Rules\SanitizeRule('special_chars'); $cleaned = $rule->getConvertedValue($maliciousInput); echo "原始输入: " . $maliciousInput . "\n"; echo "清理后的输入: " . $cleaned . "\n"; echo "\n=== 示例完成 ===\n"; examples/url_validation.php000064400000007617151665713370012140 0ustar00validate('https://www.example.com'); echo "验证 'https://www.example.com': " . ($result ? '通过' : '失败') . "\n"; $result = $rule->validate('http://example.com/path?query=value'); echo "验证 'http://example.com/path?query=value': " . ($result ? '通过' : '失败') . "\n"; $result = $rule->validate('not-a-url'); echo "验证 'not-a-url': " . ($result ? '通过' : '失败') . "\n"; echo "\n2. 仅允许 HTTPS 协议\n"; $rule = new \Awesome\Validation\Rules\UrlRule(['https']); $result = $rule->validate('https://www.example.com'); echo "验证 'https://www.example.com': " . ($result ? '通过' : '失败') . "\n"; $result = $rule->validate('http://www.example.com'); echo "验证 'http://www.example.com': " . ($result ? '通过' : '失败') . "\n"; echo "\n3. 允许 HTTP 和 HTTPS 协议\n"; $rule = new \Awesome\Validation\Rules\UrlRule(['http', 'https']); $result = $rule->validate('https://www.example.com'); echo "验证 'https://www.example.com': " . ($result ? '通过' : '失败') . "\n"; $result = $rule->validate('http://www.example.com'); echo "验证 'http://www.example.com': " . ($result ? '通过' : '失败') . "\n"; $result = $rule->validate('ftp://www.example.com'); echo "验证 'ftp://www.example.com': " . ($result ? '通过' : '失败') . "\n"; echo "\n4. 验证带端口的 URL\n"; $rule = new \Awesome\Validation\Rules\UrlRule(); $result = $rule->validate('https://www.example.com:443'); echo "验证 'https://www.example.com:443': " . ($result ? '通过' : '失败') . "\n"; $result = $rule->validate('http://localhost:8080'); echo "验证 'http://localhost:8080': " . ($result ? '通过' : '失败') . "\n"; echo "\n5. 验证带用户信息的 URL\n"; $result = $rule->validate('https://user:pass@example.com'); echo "验证 'https://user:pass@example.com': " . ($result ? '通过' : '失败') . "\n"; echo "\n6. 验证带片段的 URL\n"; $result = $rule->validate('https://www.example.com/page#section'); echo "验证 'https://www.example.com/page#section': " . ($result ? '通过' : '失败') . "\n"; echo "\n7. 组合验证(网站配置表单)\n"; $data = [ 'website_url' => 'https://www.example.com', 'api_endpoint' => 'https://api.example.com/v1', 'cdn_url' => 'http://cdn.example.com', 'callback_url' => 'https://client.example.com/callback' ]; $result = Validator::validateKeys($data, function($validator) { return [ $validator->key('website_url')->required()->url(['https']), $validator->key('api_endpoint')->required()->url(['https']), $validator->key('cdn_url')->required()->url(['http', 'https']), $validator->key('callback_url')->required()->url(['https']) ]; }); if ($result->isValid()) { echo "所有 URL 字段验证通过!\n"; } else { echo "验证失败:\n"; foreach ($result->errors()->all() as $field => $errors) { foreach ($errors as $error) { echo " {$field}: {$error}\n"; } } } echo "\n8. 自定义错误消息\n"; $result = Validator::url(['https'], '仅允许 HTTPS 协议的 URL')->validate('http://example.com'); if ($result->isInvalid()) { echo "自定义错误消息: " . $result->errors()->first('value') . "\n"; } echo "\n9. URL 长度限制组合\n"; $result = Validator::url()->maxLength(200)->validate('https://www.example.com'); echo "验证短 URL: " . ($result->isValid() ? '通过' : '失败') . "\n"; $longUrl = 'https://www.example.com/' . str_repeat('path/', 50); $result = Validator::url()->maxLength(200)->validate($longUrl); echo "验证长 URL: " . ($result->isValid() ? '通过' : '失败') . "\n"; if ($result->isInvalid()) { echo "错误信息: " . $result->errors()->first('value') . "\n"; } echo "\n=== 示例完成 ===\n"; examples/validate_or_fail.php000064400000014406151665713370012402 0ustar00minLength(5)->validateOrFail('hello world'); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n2. 基本验证 - 失败案例\n"; try { $result = Validator::string()->minLength(5)->validateOrFail('hi'); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n3. 手机号验证 - 成功案例\n"; try { $result = Validator::phone()->validateOrFail('13800138000'); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n4. 手机号验证 - 失败案例\n"; try { $result = Validator::phone()->validateOrFail('invalid'); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n5. 多规则验证 - 第一个规则失败\n"; try { $result = Validator::string() ->minLength(5) ->phone() ->validateOrFail('hi'); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; echo "注意:只返回第一个失败的规则错误\n"; } echo "\n6. 多规则验证 - 第二个规则失败\n"; try { $result = Validator::string() ->minLength(5) ->phone() ->validateOrFail('invalid phone'); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; echo "注意:只返回第一个失败的规则错误\n"; } echo "\n7. 数组字段验证 - 成功案例\n"; try { $data = [ 'username' => '张三', 'phone' => '13800138000', 'email' => 'test@example.com' ]; $result = Validator::validateKeysOrFail($data, function($validator) { return [ $validator->key('username')->required()->chineseLength(2, 10), $validator->key('phone')->required()->phone(), $validator->key('email')->required()->email() ]; }); echo "✓ 所有字段验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n8. 数组字段验证 - 第一个字段失败\n"; try { $data = [ 'username' => '张', 'phone' => 'invalid', 'email' => 'invalid' ]; $result = Validator::validateKeysOrFail($data, function($validator) { return [ $validator->key('username')->required()->chineseLength(2, 10), $validator->key('phone')->required()->phone(), $validator->key('email')->required()->email() ]; }); echo "✓ 所有字段验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; echo "注意:只返回第一个失败的字段错误\n"; } echo "\n9. KeyValidator 验证 - 成功案例\n"; try { $data = ['phone' => '13800138000']; $result = Validator::make($data) ->key('phone') ->phone() ->validateOrFail(); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n10. KeyValidator 验证 - 失败案例\n"; try { $data = ['phone' => 'invalid']; $result = Validator::make($data) ->key('phone') ->phone() ->validateOrFail(); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n11. EachValidator 验证 - 成功案例\n"; try { $data = ['phones' => ['13800138000', '13912345678', '18612345678']]; $result = Validator::make($data) ->key('phones') ->each() ->phone() ->validateOrFail(); echo "✓ 所有元素验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n12. EachValidator 验证 - 第一个元素失败\n"; try { $data = ['phones' => ['invalid', '13912345678', '18612345678']]; $result = Validator::make($data) ->key('phones') ->each() ->phone() ->validateOrFail(); echo "✓ 所有元素验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; echo "注意:只返回第一个失败的元素错误\n"; } echo "\n13. EachValidator 验证 - 第二个元素失败\n"; try { $data = ['phones' => ['13800138000', 'invalid', '18612345678']]; $result = Validator::make($data) ->key('phones') ->each() ->phone() ->validateOrFail(); echo "✓ 所有元素验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; echo "注意:只返回第一个失败的元素错误\n"; } echo "\n14. IP地址验证 - 成功案例\n"; try { $result = Validator::ip('v4')->validateOrFail('192.168.1.1'); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n15. IP地址验证 - 失败案例\n"; try { $result = Validator::ip('v4')->validateOrFail('256.168.1.1'); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n16. 域名验证 - 成功案例\n"; try { $result = Validator::domain()->validateOrFail('example.com'); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n17. 域名验证 - 失败案例\n"; try { $result = Validator::domain()->validateOrFail('invalid'); echo "✓ 验证通过\n"; } catch (ValidationException $e) { echo "✗ 验证失败: {$e->getMessage()}\n"; } echo "\n=== 示例完成 ===\n"; echo "\n总结:\n"; echo "- validateOrFail() 方法在遇到第一个验证失败时立即抛出异常\n"; echo "- 不会继续验证后续规则或字段\n"; echo "- 适用于需要快速失败的场景\n"; echo "- 与 validate() 方法相比,validate() 会收集所有错误信息\n"; phpunit.xml000064400000001024151665713370006770 0ustar00 tests src src/Awesome/Validation/Builder.php000064400000024073151665713370013150 0ustar00currentValue = $value; } public function string(?string $message = null): self { $rule = new StringRule($message); $this->addRule($rule); return $this; } public function integer(?string $message = null): self { $rule = new IntegerRule($message); $this->addRule($rule); return $this; } public function strictInteger(?string $message = null): self { $rule = new StrictIntegerRule($message); $this->addRule($rule); return $this; } public function safeInteger(?string $message = null): self { $rule = new SafeIntegerRule($message); $this->addRule($rule); return $this; } public function even(?string $message = null): self { $rule = new EvenRule($message); $this->addRule($rule); return $this; } public function odd(?string $message = null): self { $rule = new OddRule($message); $this->addRule($rule); return $this; } public function positive(?string $message = null): self { $rule = new PositiveRule($message); $this->addRule($rule); return $this; } public function negative(?string $message = null): self { $rule = new NegativeRule($message); $this->addRule($rule); return $this; } public function prime(?string $message = null): self { $rule = new PrimeRule($message); $this->addRule($rule); return $this; } public function numeric(?string $message = null): self { $rule = new Rules\NumericRule($message); $this->addRule($rule); return $this; } public function required(?string $message = null): self { $rule = new RequiredRule($message); $this->addRule($rule); return $this; } public function email(?string $message = null): self { $rule = new EmailRule($message); $this->addRule($rule); return $this; } public function minLength(int $min, ?string $message = null): self { $rule = new MinLengthRule($min, $message); $this->addRule($rule); return $this; } public function maxLength(int $max, ?string $message = null): self { $rule = new MaxLengthRule($max, $message); $this->addRule($rule); return $this; } public function min(float $min, bool $inclusive = true, ?string $message = null): self { $rule = new MinRule($min, $inclusive, $message); $this->addRule($rule); return $this; } public function between(float $min, float $max, bool $minInc = true, bool $maxInc = true, ?string $message = null): self { $rule = new BetweenRule($min, $max, $minInc, $maxInc, $message); $this->addRule($rule); return $this; } public function max(float $max, bool $inclusive = false, ?string $message = null): self { $rule = new Rules\MaxRule($max, $inclusive, $message); $this->addRule($rule); return $this; } public function in(array $values, ?string $message = null): self { $rule = new InRule($values, $message); $this->addRule($rule); return $this; } public function regex(string $pattern, ?string $message = null): self { $rule = new RegexRule($pattern, $message); $this->addRule($rule); return $this; } public function custom(callable $callback, ?string $message = null): self { $rule = new CustomRule($callback, $message); $this->addRule($rule); return $this; } public function chineseLength(int $min, int $max, ?string $message = null): self { $rule = new ChineseLengthRule($min, $max, $message); $this->addRule($rule); return $this; } public function phone(?string $message = null): self { $rule = new PhoneRule($message); $this->addRule($rule); return $this; } public function ip(string $version = 'v4', ?string $message = null): self { $rule = new IpRule($version, $message); $this->addRule($rule); return $this; } public function domain(?string $message = null): self { $rule = new DomainRule($message); $this->addRule($rule); return $this; } public function date(?string $format = null, ?string $message = null): self { $rule = new DateRule($format, $message); $this->addRule($rule); return $this; } public function url(?array $allowedSchemes = null, ?string $message = null): self { $rule = new UrlRule($allowedSchemes, $message); $this->addRule($rule); return $this; } public function json(?int $maxDepth = null, ?int $flags = null, ?string $message = null): self { $rule = new JsonRule($maxDepth, $flags, $message); $this->addRule($rule); return $this; } public function sanitize(string $filterType = 'special_chars', array $options = [], ?string $message = null): self { $rule = new SanitizeRule($filterType, $options, $message); $this->addRule($rule); return $this; } public function transform(callable $callback): self { $this->transformations[] = $callback; $this->transformCacheValid = false; return $this; } public function message(string $message): self { if (!empty($this->rules)) { $lastRule = end($this->rules); $lastRule->setMessage($message); } return $this; } public function messages(array $messages): self { $this->messages = array_merge($this->messages, $messages); return $this; } public function validate(mixed $value = null): ValidationResult { $value = $value !== null ? $this->applyTransformations($value) : $this->getCachedTransformedValue(); $errors = new MessageBag(); foreach ($this->rules as $rule) { if (!$rule->validate($value)) { $message = $this->getRuleMessage($rule); $field = $this->field ?? 'value'; $errors->add($field, $message); } else { $value = $rule->getConvertedValue($value); } } return new ValidationResult($errors->isEmpty(), $errors); } public function validateOrFail(mixed $value = null): ValidationResult { $value = $value !== null ? $this->applyTransformations($value) : $this->getCachedTransformedValue(); foreach ($this->rules as $rule) { if (!$rule->validate($value)) { $message = $this->getRuleMessage($rule); $field = $this->field ?? 'value'; throw ValidationException::fromMessage($field, $message); } else { $value = $rule->getConvertedValue($value); } } return new ValidationResult(true, new MessageBag()); } public function check(mixed $value = null): bool { return $this->validate($value)->isValid(); } private function applyTransformations(mixed $value): mixed { foreach ($this->transformations as $callback) { $value = $callback($value); } return $value; } private function getCachedTransformedValue(): mixed { if (!$this->transformCacheValid) { $this->cachedTransformedValue = $this->applyTransformations($this->currentValue); $this->transformCacheValid = true; } return $this->cachedTransformedValue; } public function setField(string $field): self { $this->field = $field; return $this; } private function addRule(AbstractRule $rule): void { $this->rules[] = $rule; } private function getRuleMessage(AbstractRule $rule): string { $ruleName = $rule->getName(); if (isset($this->messages[$ruleName])) { return $this->messages[$ruleName]; } return $rule->getFormattedMessage(['field' => $this->field ?? 'value']); } public function key(string $key): KeyValidator { if (!is_array($this->value)) { throw new \InvalidArgumentException('Builder value must be an array to use key() method'); } return new KeyValidator($this->value, $key); } }src/Awesome/Validation/EachValidator.php000064400000013253151665713370014266 0ustar00data = $data; $this->parentKey = $parentKey; } public function required(?string $message = null): self { $rule = new Rules\RequiredRule($message); $this->rules[] = $rule; return $this; } public function string(?string $message = null): self { $rule = new Rules\StringRule($message); $this->rules[] = $rule; return $this; } public function integer(?string $message = null): self { $rule = new Rules\IntegerRule($message); $this->rules[] = $rule; return $this; } public function numeric(?string $message = null): self { $rule = new Rules\NumericRule($message); $this->rules[] = $rule; return $this; } public function email(?string $message = null): self { $rule = new Rules\EmailRule($message); $this->rules[] = $rule; return $this; } public function minLength(int $min, ?string $message = null): self { $rule = new Rules\MinLengthRule($min, $message); $this->rules[] = $rule; return $this; } public function maxLength(int $max, ?string $message = null): self { $rule = new Rules\MaxLengthRule($max, $message); $this->rules[] = $rule; return $this; } public function in(array $values, ?string $message = null): self { $rule = new Rules\InRule($values, $message); $this->rules[] = $rule; return $this; } public function regex(string $pattern, ?string $message = null): self { $rule = new Rules\RegexRule($pattern, $message); $this->rules[] = $rule; return $this; } public function custom(callable $callback, ?string $message = null): self { $rule = new Rules\CustomRule($callback, $message); $this->rules[] = $rule; return $this; } public function chineseLength(int $min, int $max, ?string $message = null): self { $rule = new Rules\ChineseLengthRule($min, $max, $message); $this->rules[] = $rule; return $this; } public function phone(?string $message = null): self { $rule = new Rules\PhoneRule($message); $this->rules[] = $rule; return $this; } public function ip(string $version = 'v4', ?string $message = null): self { $rule = new Rules\IpRule($version, $message); $this->rules[] = $rule; return $this; } public function domain(?string $message = null): self { $rule = new Rules\DomainRule($message); $this->rules[] = $rule; return $this; } public function date(?string $format = null, ?string $message = null): self { $rule = new Rules\DateRule($format, $message); $this->rules[] = $rule; return $this; } public function url(?array $allowedSchemes = null, ?string $message = null): self { $rule = new Rules\UrlRule($allowedSchemes, $message); $this->rules[] = $rule; return $this; } public function json(?int $maxDepth = null, ?int $flags = null, ?string $message = null): self { $rule = new Rules\JsonRule($maxDepth, $flags, $message); $this->rules[] = $rule; return $this; } public function sanitize(string $filterType = 'special_chars', array $options = [], ?string $message = null): self { $rule = new Rules\SanitizeRule($filterType, $options, $message); $this->rules[] = $rule; return $this; } public function validate(): ValidationResult { $errors = new Messages\MessageBag(); if (!is_array($this->data)) { $errors->add($this->parentKey, '必须是数组'); return new ValidationResult(false, $errors); } foreach ($this->data as $index => $item) { $builder = new Builder($item); $builder->setField("{$this->parentKey}.{$index}"); foreach ($this->rules as $rule) { if (!$rule->validate($item)) { $message = $this->getRuleMessage($rule); $field = "{$this->parentKey}.{$index}"; $errors->add($field, $message); } } } return new ValidationResult($errors->isEmpty(), $errors); } public function validateOrFail(): ValidationResult { if (!is_array($this->data)) { throw ValidationException::fromMessage($this->parentKey, '必须是数组'); } foreach ($this->data as $index => $item) { $builder = new Builder($item); $field = "{$this->parentKey}.{$index}"; $builder->setField($field); foreach ($this->rules as $rule) { if (!$rule->validate($item)) { $message = $rule->getFormattedMessage(['field' => $field]); throw ValidationException::fromMessage($field, $message); } } } return new ValidationResult(true, new Messages\MessageBag()); } private function getRuleMessage(Rules\AbstractRule $rule): string { $ruleName = $rule->getName(); if (isset($this->messages[$ruleName])) { return $this->messages[$ruleName]; } return $rule->getFormattedMessage(['field' => $this->parentKey]); } } src/Awesome/Validation/KeyValidator.php000064400000016566151665713370014170 0ustar00data = $data; $this->key = $key; $this->builder = new Builder($this->getValue()); $this->builder->setField($key); } public function optional(): self { $this->isOptional = true; return $this; } private function hasKey(): bool { $keys = explode('.', $this->key); $value = $this->data; foreach ($keys as $key) { if (!is_array($value) || !array_key_exists($key, $value)) { return false; } $value = $value[$key]; } return true; } private function getValue(): mixed { $keys = explode('.', $this->key); $value = $this->data; foreach ($keys as $key) { if (!is_array($value) || !array_key_exists($key, $value)) { return null; } $value = $value[$key]; } return $value; } public function required(?string $message = null): self { $this->builder->required($message); return $this; } public function string(?string $message = null): self { $this->builder->string($message); return $this; } public function integer(?string $message = null): self { $this->builder->integer($message); return $this; } public function strictInteger(?string $message = null): self { $this->builder->strictInteger($message); return $this; } public function safeInteger(?string $message = null): self { $this->builder->safeInteger($message); return $this; } public function even(?string $message = null): self { $this->builder->even($message); return $this; } public function odd(?string $message = null): self { $this->builder->odd($message); return $this; } public function positive(?string $message = null): self { $this->builder->positive($message); return $this; } public function negative(?string $message = null): self { $this->builder->negative($message); return $this; } public function prime(?string $message = null): self { $this->builder->prime($message); return $this; } public function numeric(?string $message = null): self { $this->builder->numeric($message); return $this; } public function email(?string $message = null): self { $this->builder->email($message); return $this; } public function minLength(int $min, ?string $message = null): self { $this->builder->minLength($min, $message); return $this; } public function maxLength(int $max, ?string $message = null): self { $this->builder->maxLength($max, $message); return $this; } public function in(array $values, ?string $message = null): self { $this->builder->in($values, $message); return $this; } public function regex(string $pattern, ?string $message = null): self { $this->builder->regex($pattern, $message); return $this; } public function custom(callable $callback, ?string $message = null): self { $this->builder->custom($callback, $message); return $this; } public function chineseLength(int $min, int $max, ?string $message = null): self { $this->builder->chineseLength($min, $max, $message); return $this; } public function phone(?string $message = null): self { $this->builder->phone($message); return $this; } public function ip(string $version = 'v4', ?string $message = null): self { $this->builder->ip($version, $message); return $this; } public function domain(?string $message = null): self { $this->builder->domain($message); return $this; } public function date(?string $format = null, ?string $message = null): self { $this->builder->date($format, $message); return $this; } public function url(?array $allowedSchemes = null, ?string $message = null): self { $this->builder->url($allowedSchemes, $message); return $this; } public function json(?int $maxDepth = null, ?int $flags = null, ?string $message = null): self { $this->builder->json($maxDepth, $flags, $message); return $this; } public function sanitize(string $filterType = 'special_chars', array $options = [], ?string $message = null): self { $this->builder->sanitize($filterType, $options, $message); return $this; } public function transform(callable $callback): self { $this->builder->transform($callback); return $this; } public function min(float $min, bool $inclusive = true, ?string $message = null): self { $this->builder->min($min, $inclusive, $message); return $this; } public function between(float $min, float $max, bool $minInc = true, bool $maxInc = true, ?string $message = null): self { $this->builder->between($min, $max, $minInc, $maxInc, $message); return $this; } public function max(float $max, bool $inclusive = false, ?string $message = null): self { $this->builder->max($max, $inclusive, $message); return $this; } public function validate(): ValidationResult { if (!$this->hasKey() && $this->isOptional) { $result = new ValidationResult(true, new Messages\MessageBag()); $result->addLog($this->key, 'skipped', 'Key does not exist and is optional'); return $result; } $result = $this->builder->validate(); if ($result->isValid()) { $result->addLog($this->key, 'validated', 'Validation passed'); } else { $result->addLog($this->key, 'failed', 'Validation failed'); } return $result; } public function validateOrFail(): ValidationResult { if (!$this->hasKey() && $this->isOptional) { $result = new ValidationResult(true, new Messages\MessageBag()); $result->addLog($this->key, 'skipped', 'Key does not exist and is optional'); return $result; } try { $result = $this->builder->validateOrFail(); $result->addLog($this->key, 'validated', 'Validation passed'); return $result; } catch (ValidationException $e) { // We don't have direct access to the result here, but we can reconstruct it for logging if needed // Actually validateOrFail throws, so we just let it throw. // The logs might be incomplete in case of an exception if we don't catch and re-throw. throw $e; } } public function each(): EachValidator { $value = $this->getValue(); if (!is_array($value)) { throw new \InvalidArgumentException('Field value must be an array to use each() method'); } return new EachValidator($value, $this->key); } } src/Awesome/Validation/Messages/MessageBag.php000064400000003040151665713370015316 0ustar00messages[$field])) { $this->messages[$field] = []; } $this->messages[$field][] = $message; } public function get(string $field): array { return $this->messages[$field] ?? []; } public function all(): array { return $this->messages; } public function first(string $field): ?string { $messages = $this->get($field); return $messages[0] ?? null; } public function has(string $field): bool { return isset($this->messages[$field]) && !empty($this->messages[$field]); } public function isEmpty(): bool { return empty($this->messages); } public function toArray(): array { return $this->messages; } public function toJson(): string { return json_encode($this->messages, JSON_UNESCAPED_UNICODE); } public function jsonSerialize(): array { return $this->messages; } public function __toString(): string { $messages = []; foreach ($this->messages as $field => $fieldMessages) { foreach ($fieldMessages as $message) { $messages[] = $message; } } return implode("\n", $messages); } }src/Awesome/Validation/Rules/AbstractRule.php000064400000005734151665713370015252 0ustar00message = $message; $this->validateParameters(); } abstract public function validate(mixed $value): bool; public function getConvertedValue(mixed $value): mixed { return $value; } public function getName(): string { return $this->name; } public function setMessage(string $message): self { $this->message = $message; return $this; } public function getMessage(): string { return $this->message ?? $this->getDefaultMessage(); } public function getParameters(): array { return $this->parameters; } abstract public function getDefaultMessage(): string; public function getFormattedMessage(array $context = []): string { $message = $this->getMessage(); $replacements = array_merge($this->parameters, $context); foreach ($replacements as $key => $value) { if (is_array($value)) { $value = implode(', ', $value); } $message = str_replace('{' . $key . '}', (string)$value, $message); } return $message; } protected function validateParameters(): void { } protected function assertParameterExists(string $key): void { if (!isset($this->parameters[$key])) { throw new \InvalidArgumentException("Parameter '{$key}' is required for rule '{$this->name}'"); } } protected function assertParameterType(string $key, string $expectedType): void { $this->assertParameterExists($key); $value = $this->parameters[$key]; $isValid = match($expectedType) { 'string' => is_string($value), 'int', 'integer' => is_int($value), 'float', 'double' => is_float($value), 'bool', 'boolean' => is_bool($value), 'array' => is_array($value), 'callable' => is_callable($value), default => false }; if (!$isValid) { $actualType = get_debug_type($value); throw new \InvalidArgumentException("Parameter '{$key}' must be of type {$expectedType}, {$actualType} given"); } } protected function assertParameterInArray(string $key, array $allowedValues): void { $this->assertParameterExists($key); $value = $this->parameters[$key]; if (!in_array($value, $allowedValues, true)) { $allowed = implode(', ', array_map(fn($v) => var_export($v, true), $allowedValues)); throw new \InvalidArgumentException("Parameter '{$key}' must be one of [{$allowed}], " . var_export($value, true) . " given"); } } }src/Awesome/Validation/Rules/BetweenRule.php000064400000002012151665713370015062 0ustar00minInclusive ? $value >= $this->min : $value > $this->min; $high = $this->maxInclusive ? $value <= $this->max : $value < $this->max; return $low && $high; } public function getDefaultMessage(): string { $left = $this->minInclusive ? '[' : '('; $right = $this->maxInclusive ? ']' : ')'; return "{field} 必须在 {$left}{$this->min}, {$this->max}{$right} 之间"; } } src/Awesome/Validation/Rules/ChineseLengthRule.php000064400000001412151665713370016214 0ustar00parameters['min'] = $min; $this->parameters['max'] = $max; } public function validate(mixed $value): bool { if (!is_string($value)) { return false; } $length = mb_strlen($value, 'UTF-8'); return $length >= $this->parameters['min'] && $length <= $this->parameters['max']; } public function getDefaultMessage(): string { return '{field} 中文字符长度必须在 {min} 到 {max} 之间'; } } src/Awesome/Validation/Rules/CustomRule.php000064400000001021151665713370014742 0ustar00callback = $callback; } public function validate(mixed $value): bool { return ($this->callback)($value); } public function getDefaultMessage(): string { return '{field} 验证失败'; } }src/Awesome/Validation/Rules/DateRule.php000064400000003651151665713370014360 0ustar00parameters['format'] = $format; parent::__construct($message); } protected function validateParameters(): void { if ($this->format !== null && !is_string($this->format)) { throw new \InvalidArgumentException('Format parameter must be a string or null'); } } public function validate(mixed $value): bool { if (!is_string($value)) { return false; } if ($this->format !== null) { return $this->validateWithFormat($value); } return $this->validateWithoutFormat($value); } private function validateWithFormat(string $value): bool { $date = \DateTime::createFromFormat($this->format, $value); return $date !== false && $date->format($this->format) === $value; } private function validateWithoutFormat(string $value): bool { try { new \DateTime($value); return true; } catch (\Exception $e) { return false; } } public function getConvertedValue(mixed $value): mixed { if (!is_string($value)) { return $value; } if ($this->format !== null) { return \DateTime::createFromFormat($this->format, $value) ?: $value; } try { return new \DateTime($value); } catch (\Exception $e) { return $value; } } public function getDefaultMessage(): string { if ($this->format !== null) { return '{field} 必须是有效的日期格式: {format}'; } return '{field} 必须是有效的日期'; } }src/Awesome/Validation/Rules/DomainRule.php000064400000001300151665713370014677 0ustar00parameters['values'] = $values; } public function validate(mixed $value): bool { return in_array($value, $this->parameters['values'], true); } public function getDefaultMessage(): string { return '{field} 必须在指定列表中'; } }src/Awesome/Validation/Rules/IntegerRule.php000064400000001300151665713370015065 0ustar00convertedValue = $result; return true; } return false; } public function getConvertedValue(mixed $value): mixed { return $this->convertedValue ?? $value; } public function getDefaultMessage(): string { return '{field} 必须是整数'; } }src/Awesome/Validation/Rules/IpRule.php000064400000002431151665713370014046 0ustar00version) { 'v4' => filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false, 'v6' => filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false, 'any' => filter_var($value, FILTER_VALIDATE_IP) !== false, default => false, }; } public function getParameters(): array { return [ 'version' => $this->version, ]; } public function getDefaultMessage(): string { return match ($this->version) { 'v4' => '{field} 必须是有效的 IPv4 地址', 'v6' => '{field} 必须是有效的 IPv6 地址', 'any' => '{field} 必须是有效的 IP 地址', default => '{field} 必须是有效的 IP 地址', }; } } src/Awesome/Validation/Rules/JsonRule.php000064400000004106151665713370014410 0ustar00parameters['maxDepth'] = $maxDepth; $this->parameters['flags'] = $flags; parent::__construct($message); } protected function validateParameters(): void { if ($this->maxDepth !== null && !is_int($this->maxDepth)) { throw new \InvalidArgumentException('maxDepth parameter must be an integer or null'); } if ($this->maxDepth !== null && $this->maxDepth < 1) { throw new \InvalidArgumentException('maxDepth must be greater than 0'); } if ($this->flags !== null && !is_int($this->flags)) { throw new \InvalidArgumentException('flags parameter must be an integer or null'); } } public function validate(mixed $value): bool { if (!is_string($value)) { return false; } return $this->isValidJson($value); } private function isValidJson(string $value): bool { $flags = $this->flags ?? JSON_THROW_ON_ERROR; $maxDepth = $this->maxDepth ?? 512; try { json_decode($value, flags: $flags, depth: $maxDepth); return json_last_error() === JSON_ERROR_NONE; } catch (\JsonException $e) { return false; } } public function getConvertedValue(mixed $value): mixed { if (!is_string($value)) { return $value; } $flags = $this->flags ?? JSON_THROW_ON_ERROR; $maxDepth = $this->maxDepth ?? 512; try { return json_decode($value, associative: true, flags: $flags, depth: $maxDepth); } catch (\JsonException $e) { return $value; } } public function getDefaultMessage(): string { return '{field} 必须是有效的JSON字符串'; } }src/Awesome/Validation/Rules/MaxLengthRule.php000064400000001173151665713370015367 0ustar00parameters['max'] = $max; } public function validate(mixed $value): bool { if (!is_string($value) && !is_array($value)) { return false; } return strlen($value) <= $this->parameters['max']; } public function getDefaultMessage(): string { return '{field} 长度不能大于 {max}'; } }src/Awesome/Validation/Rules/MaxRule.php000064400000001746151665713370014233 0ustar00parameters['max'] = $max; $this->parameters['inclusive'] = $inclusive; } public function validate(mixed $value): bool { if (!is_numeric($value)) { return false; } $value = (float) $value; $max = $this->parameters['max']; $inclusive = $this->parameters['inclusive']; return $inclusive ? $value <= $max : $value < $max; } public function getDefaultMessage(): string { $max = $this->parameters['max']; $inclusive = $this->parameters['inclusive']; return $inclusive ? "{field} 必须小于或等于 {$max}" : "{field} 必须小于 {$max}"; } } src/Awesome/Validation/Rules/MinLengthRule.php000064400000001173151665713370015365 0ustar00parameters['min'] = $min; } public function validate(mixed $value): bool { if (!is_string($value) && !is_array($value)) { return false; } return strlen($value) >= $this->parameters['min']; } public function getDefaultMessage(): string { return '{field} 长度不能小于 {min}'; } }src/Awesome/Validation/Rules/MinRule.php000064400000001362151665713370014223 0ustar00inclusive ? $value >= $this->min : $value > $this->min; } public function getDefaultMessage(): string { return $this->inclusive ? "{field} 必须大于或等于 {$this->min}" : "{field} 必须大于 {$this->min}"; } } src/Awesome/Validation/Rules/NegativeRule.php000064400000000640151665713370015240 0ustar00 0; } public function getDefaultMessage(): string { return '{field} 必须是正数'; } } src/Awesome/Validation/Rules/PrimeRule.php000064400000001452151665713370014554 0ustar00parameters['pattern'] = $pattern; $this->parameters['maxLength'] = $maxLength; parent::__construct($message); } protected function validateParameters(): void { $this->assertParameterType('pattern', 'string'); $this->assertParameterType('maxLength', 'int'); if ($this->maxLength <= 0) { throw new \InvalidArgumentException('maxLength must be greater than 0'); } $this->validatePatternSafety(); } private function validatePatternSafety(): void { $pattern = $this->parameters['pattern']; foreach (self::DANGEROUS_PATTERNS as $dangerousPattern) { if (preg_match($dangerousPattern, $pattern)) { throw new \InvalidArgumentException( 'Pattern contains potentially dangerous constructs that could lead to ReDoS attacks' ); } } } public function validate(mixed $value): bool { if (!is_string($value)) { return false; } if (strlen($value) > $this->maxLength) { return false; } return $this->safeMatch($value); } private function safeMatch(string $value): bool { $pattern = $this->parameters['pattern']; $result = @preg_match($pattern, $value); if ($result === false) { $errorCode = preg_last_error(); if ($errorCode !== PREG_NO_ERROR) { return false; } return false; } return $result === 1; } public function getDefaultMessage(): string { return '{field} 格式不正确'; } }src/Awesome/Validation/Rules/RequiredRule.php000064400000000637151665713370015264 0ustar00= PHP_INT_MIN && $val <= PHP_INT_MAX; } public function getDefaultMessage(): string { return '{field} 超出安全整数范围'; } } src/Awesome/Validation/Rules/SanitizeRule.php000064400000003741151665713370015271 0ustar00 FILTER_SANITIZE_EMAIL, 'url' => FILTER_SANITIZE_URL, 'encoded' => FILTER_SANITIZE_ENCODED, 'special_chars' => FILTER_SANITIZE_SPECIAL_CHARS, 'full_special_chars' => FILTER_SANITIZE_FULL_SPECIAL_CHARS, 'number' => FILTER_SANITIZE_NUMBER_INT, 'float' => FILTER_SANITIZE_NUMBER_FLOAT, ]; public function __construct( private string $filterType = 'special_chars', private array $options = [], ?string $message = null ) { parent::__construct($message); $this->parameters['filterType'] = $filterType; $this->parameters['options'] = $options; } protected function validateParameters(): void { if (!isset(self::FILTERS[$this->filterType])) { $validFilters = implode(', ', array_keys(self::FILTERS)); throw new \InvalidArgumentException("Invalid filter type '{$this->filterType}'. Valid types: {$validFilters}"); } if (!is_array($this->options)) { throw new \InvalidArgumentException('Options must be an array'); } } public function validate(mixed $value): bool { return true; } public function getConvertedValue(mixed $value): mixed { if (!is_string($value) && !is_int($value) && !is_float($value)) { return $value; } $filter = self::FILTERS[$this->filterType]; $options = $this->options; if (!empty($options)) { $result = filter_var($value, $filter, $options); } else { $result = filter_var($value, $filter); } return $result !== false ? $result : $value; } public function getDefaultMessage(): string { return '{field} 已被清理'; } }src/Awesome/Validation/Rules/StrictIntegerRule.php000064400000000546151665713370016271 0ustar00parameters['allowedSchemes'] = $allowedSchemes; parent::__construct($message); } protected function validateParameters(): void { if ($this->allowedSchemes !== null && !is_array($this->allowedSchemes)) { throw new \InvalidArgumentException('allowedSchemes parameter must be an array or null'); } if ($this->allowedSchemes !== null) { foreach ($this->allowedSchemes as $scheme) { if (!is_string($scheme)) { throw new \InvalidArgumentException('All allowed schemes must be strings'); } } } } public function validate(mixed $value): bool { if (!is_string($value)) { return false; } $filtered = filter_var($value, FILTER_VALIDATE_URL); if ($filtered === false) { return false; } if ($this->allowedSchemes !== null && !empty($this->allowedSchemes)) { return $this->validateScheme($value); } return true; } private function validateScheme(string $url): bool { $parsed = parse_url($url); if ($parsed === false || !isset($parsed['scheme'])) { return false; } return in_array(strtolower($parsed['scheme']), array_map('strtolower', $this->allowedSchemes), true); } public function getDefaultMessage(): string { if (empty($this->allowedSchemes)) { return '{field} 必须是有效的URL'; } $schemes = implode(', ', $this->allowedSchemes); return '{field} 必须是有效的URL,且协议必须是: ' . $schemes; } }src/Awesome/Validation/ValidationException.php000064400000002731151665713370015530 0ustar00errors = $errors; if ($message === '') { $message = $this->formatErrorMessage(); } parent::__construct($message, $code, $previous); } public static function fromResult(ValidationResult $result, string $message = ''): self { return new self($result->errors(), $message); } public static function fromMessage(string $field, string $message): self { $errors = new MessageBag(); $errors->add($field, $message); return new self($errors); } public function errors(): MessageBag { return $this->errors; } protected function formatErrorMessage(): string { $messages = []; foreach ($this->errors->all() as $field => $fieldErrors) { foreach ($fieldErrors as $error) { $messages[] = $error; } } return implode("\n", $messages); } public function toArray(): array { return $this->errors->all(); } public function toJson(): string { return $this->errors->toJson(); } } src/Awesome/Validation/ValidationResult.php000064400000003067151665713370015053 0ustar00valid = $valid; $this->errors = $errors; $this->logs = $logs; } public function isValid(): bool { return $this->valid; } public function isInvalid(): bool { return !$this->valid; } public function errors(): MessageBag { return $this->errors; } public function getLogs(): array { return $this->logs; } public function addLog(string $field, string $status, string $message = ''): void { $this->logs[] = [ 'field' => $field, 'status' => $status, // 'validated', 'skipped', 'failed' 'message' => $message, 'timestamp' => microtime(true) ]; } public function firstError(): ?string { $all = $this->errors->all(); if (empty($all)) { return null; } $firstField = array_key_first($all); return $this->errors->first($firstField); } public function toArray(): array { return $this->errors->toArray(); } public function toJson(): string { return $this->errors->toJson(); } public function __toString(): string { return (string)$this->errors; } }src/Awesome/Validation/Validator.php000064400000020242151665713370013501 0ustar00string($message); } public static function integer(?string $message = null): Builder { return (new Builder())->integer($message); } public static function numeric(?string $message = null): Builder { return (new Builder())->numeric($message); } public static function required(?string $message = null): Builder { return (new Builder())->required($message); } public static function email(?string $message = null): Builder { return (new Builder())->email($message); } public static function array(?string $message = null): Builder { return (new Builder())->custom(fn($v) => is_array($v), $message); } public static function chineseLength(int $min, int $max, ?string $message = null): Builder { return (new Builder())->chineseLength($min, $max, $message); } public static function phone(?string $message = null): Builder { return (new Builder())->phone($message); } public static function ip(string $version = 'v4', ?string $message = null): Builder { return (new Builder())->ip($version, $message); } public static function domain(?string $message = null): Builder { return (new Builder())->domain($message); } public static function date(?string $format = null, ?string $message = null): Builder { return (new Builder())->date($format, $message); } public static function url(?array $allowedSchemes = null, ?string $message = null): Builder { return (new Builder())->url($allowedSchemes, $message); } public static function json(?int $maxDepth = null, ?int $flags = null, ?string $message = null): Builder { return (new Builder())->json($maxDepth, $flags, $message); } public static function sanitize(string $filterType = 'special_chars', array $options = [], ?string $message = null): Builder { return (new Builder())->sanitize($filterType, $options, $message); } public static function make(array $data): Builder { return new Builder($data); } public static function validateKeys(array $data, callable $callback): ValidationResult { $builder = new Builder($data); $validators = $callback($builder); if (!is_array($validators)) { throw new \InvalidArgumentException('Callback must return an array of validator instances'); } $errors = new Messages\MessageBag(); $allLogs = []; foreach ($validators as $validator) { if (!$validator instanceof KeyValidator && !$validator instanceof EachValidator) { throw new \InvalidArgumentException('All items must be KeyValidator or EachValidator instances'); } $result = $validator->validate(); $allLogs = array_merge($allLogs, $result->getLogs()); if ($result->isInvalid()) { foreach ($result->errors()->all() as $field => $fieldErrors) { foreach ($fieldErrors as $error) { $errors->add($field, $error); } } } } return new ValidationResult($errors->isEmpty(), $errors, $allLogs); } public static function validate(array $data, array $rules): ValidationResult { $errors = new Messages\MessageBag(); foreach ($rules as $field => $ruleString) { $value = self::getNestedValue($data, $field); $builder = new Builder($value); $builder->setField($field); $ruleList = explode('|', $ruleString); foreach ($ruleList as $rule) { self::applyRule($builder, $rule); } $result = $builder->validate(); if ($result->isInvalid()) { foreach ($result->errors()->all() as $fieldErrors) { foreach ($fieldErrors as $error) { $errors->add($field, $error); } } } } return new ValidationResult($errors->isEmpty(), $errors); } private static function getNestedValue(array $data, string $field): mixed { $keys = explode('.', $field); $value = $data; foreach ($keys as $key) { if (!is_array($value) || !array_key_exists($key, $value)) { return null; } $value = $value[$key]; } return $value; } private static function applyRule(Builder $builder, string $rule): void { $message = null; if (str_contains($rule, ',message:')) { [$rule, $message] = explode(',message:', $rule, 2); } if (str_contains($rule, ':')) { [$ruleName, $parameters] = explode(':', $rule, 2); $paramArray = explode(',', $parameters); match ($ruleName) { 'min' => $builder->minLength((int)$paramArray[0], $message), 'max' => $builder->maxLength((int)$paramArray[0], $message), 'in' => $builder->in($paramArray, $message), 'regex' => $builder->regex($paramArray[0], $message), 'custom' => $builder->custom(fn($v) => eval('return ' . $paramArray[0] . ';'), $message), 'chineseLength' => $builder->chineseLength((int)$paramArray[0], (int)$paramArray[1], $message), 'ip' => $builder->ip($paramArray[0] ?? 'v4', $message), default => null }; } else { match ($rule) { 'required' => $builder->required($message), 'string' => $builder->string($message), 'integer' => $builder->integer($message), 'numeric' => $builder->numeric($message), 'email' => $builder->email($message), 'phone' => $builder->phone($message), 'domain' => $builder->domain($message), default => null }; } } public static function validateOrFail(array $data, array $rules): ValidationResult { $result = self::validate($data, $rules); if ($result->isInvalid()) { throw ValidationException::fromResult($result); } return $result; } public static function validateKeysOrFail(array $data, callable $callback): ValidationResult { $builder = new Builder($data); $validators = $callback($builder); if (!is_array($validators)) { throw new \InvalidArgumentException('Callback must return an array of validator instances'); } $allLogs = []; foreach ($validators as $validator) { if (!$validator instanceof KeyValidator && !$validator instanceof EachValidator) { throw new \InvalidArgumentException('All items must be KeyValidator or EachValidator instances'); } $result = $validator->validateOrFail(); $allLogs = array_merge($allLogs, $result->getLogs()); } return new ValidationResult(true, new Messages\MessageBag(), $allLogs); } public static function validateNested(array $dataData, array $rules): ValidationResult { $errors = new Messages\MessageBag(); foreach ($dataData as $index => $item) { $itemErrors = self::validate($item, $rules); if ($itemErrors->isInvalid()) { foreach ($itemErrors->errors()->all() as $field => $fieldErrors) { foreach ($fieldErrors as $error) { $errors->add("{$index}.{$field}", $error); } } } } return new ValidationResult($errors->isEmpty(), $errors); } }tests/Feature/EachValidatorTest.php000064400000011757151665713370013411 0ustar00 [ ['name' => 'John', 'age' => '25'], ['name' => 'Jane', 'age' => '30'] ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('users')->each()->string()->integer() ]; }); $this->assertFalse($result->isValid()); } public function testEachValidationWithValidData() { $data = [ 'numbers' => ['1', '2', '3', '4', '5'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('numbers')->each()->integer() ]; }); $this->assertTrue($result->isValid()); } public function testEachValidationWithInvalidData() { $data = [ 'numbers' => ['1', '2', 'abc', '4', '5'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('numbers')->each()->integer() ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('numbers.2', $errors); } public function testEachValidationWithEmails() { $data = [ 'emails' => [ 'john@example.com', 'jane@example.com', 'invalid-email' ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('emails')->each()->email() ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('emails.2', $errors); } public function testEachValidationWithMinLength() { $data = [ 'names' => ['John', 'Jane', 'A', 'Bob'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('names')->each()->string()->minLength(2) ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('names.2', $errors); } public function testEachValidationWithNestedArray() { $data = [ 'data' => [ 'users' => [ ['name' => 'John', 'age' => '25'], ['name' => 'Jane', 'age' => '30'] ] ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('data.users')->each()->string()->integer() ]; }); $this->assertFalse($result->isValid()); } public function testEachValidationWithCustomMessage() { $data = [ 'ages' => ['25', '30', 'not-a-number'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('ages')->each()->integer('年龄必须是整数') ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('ages.2', $errors); $this->assertStringContainsString('年龄必须是整数', $errors['ages.2'][0]); } public function testEachValidationWithInRule() { $data = [ 'statuses' => ['active', 'inactive', 'invalid'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('statuses')->each()->in(['active', 'inactive', 'pending']) ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('statuses.2', $errors); } public function testEachValidationWithEmptyArray() { $data = [ 'items' => [] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('items')->each()->string() ]; }); $this->assertTrue($result->isValid()); } public function testEachValidationWithNonArrayValue() { $this->expectException(\InvalidArgumentException::class); $data = [ 'not_array' => 'string value' ]; Validator::validateKeys($data, function($builder) { return [ $builder->key('not_array')->each()->string() ]; }); } } tests/Feature/KeyValidatorTest.php000064400000014635151665713370013277 0ustar00 'john_doe', 'email' => 'john@example.com', 'age' => '25' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string()->minLength(5), $builder->key('email')->required()->email(), $builder->key('age')->required()->integer() ]; }); $this->assertTrue($result->isValid()); } public function testKeyValidationWithInvalidData() { $data = [ 'username' => 'ab', 'email' => 'invalid-email', 'age' => 'not-a-number' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string()->minLength(5), $builder->key('email')->required()->email(), $builder->key('age')->required()->integer() ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('username', $errors); $this->assertArrayHasKey('email', $errors); $this->assertArrayHasKey('age', $errors); } public function testKeyValidationWithNestedFields() { $data = [ 'user' => [ 'profile' => [ 'name' => 'John Doe', 'email' => 'john@example.com' ] ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('user.profile.name')->required()->string()->minLength(2), $builder->key('user.profile.email')->required()->email() ]; }); $this->assertTrue($result->isValid()); } public function testKeyValidationWithNestedFieldsInvalid() { $data = [ 'user' => [ 'profile' => [ 'name' => 'J', 'email' => 'invalid' ] ] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('user.profile.name')->required()->string()->minLength(2), $builder->key('user.profile.email')->required()->email() ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('user.profile.name', $errors); $this->assertArrayHasKey('user.profile.email', $errors); } public function testKeyValidationWithCustomMessages() { $data = [ 'username' => 'ab', 'email' => 'invalid' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string()->minLength(5, '用户名至少需要5个字符'), $builder->key('email')->required()->email('请输入有效的邮箱地址') ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertStringContainsString('用户名至少需要5个字符', $errors['username'][0]); $this->assertStringContainsString('请输入有效的邮箱地址', $errors['email'][0]); } public function testKeyValidationWithInRule() { $data = [ 'status' => 'active', 'priority' => 'high' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('status')->required()->in(['active', 'inactive', 'pending']), $builder->key('priority')->required()->in(['low', 'medium', 'high']) ]; }); $this->assertTrue($result->isValid()); } public function testKeyValidationWithRegexRule() { $data = [ 'phone' => '13800138000' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('phone')->required()->regex('/^1[3-9]\d{9}$/') ]; }); $this->assertTrue($result->isValid()); } public function testKeyValidationWithCustomRule() { $data = [ 'password' => 'Password123' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('password') ->required() ->string() ->minLength(8) ->custom(function($value) { return preg_match('/[A-Z]/', $value) && preg_match('/[0-9]/', $value); }, '密码必须包含大写字母和数字') ]; }); $this->assertTrue($result->isValid()); } public function testKeyValidationWithMissingField() { $data = [ 'username' => 'john' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('username')->required()->string(), $builder->key('email')->required()->email() ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('email', $errors); } public function testKeyMethodOnBuilder() { $data = [ 'username' => 'john', 'email' => 'john@example.com' ]; $builder = Validator::make($data); $keyValidator = $builder->key('username'); $result = $keyValidator->required()->string()->minLength(3)->validate(); $this->assertTrue($result->isValid()); } public function testKeyMethodWithInvalidValue() { $this->expectException(\InvalidArgumentException::class); $builder = new \Awesome\Validation\Builder('not an array'); $builder->key('username'); } } tests/Feature/MaxValidationTest.php000064400000011466151665713370013440 0ustar00max(100, false)->validate(99); $this->assertTrue($result->isValid()); $result = $builder->max(100, false)->validate(100); $this->assertFalse($result->isValid()); $result = $builder->max(100, false)->validate(101); $this->assertFalse($result->isValid()); } public function testInclusiveMaxValidationWithBuilder() { $builder = new Builder(); // 等于最大值应该通过(inclusive 模式) $result = $builder->max(100, true)->validate(100); $this->assertTrue($result->isValid()); // 小于最大值应该通过 $result = $builder->max(100, true)->validate(99); $this->assertTrue($result->isValid()); // 大于最大值不应该通过 $result = $builder->max(100, true)->validate(101); $this->assertFalse($result->isValid()); } public function testMaxValidationInKeyValidator() { $data = [ 'age' => 25, 'score' => 85, ]; $result = Validator::validateKeys($data, function($v) { return [ $v->key('age')->numeric()->max(150, true), // 小于等于150 $v->key('score')->numeric()->max(100, true), // 小于等于100 ]; }); $this->assertTrue($result->isValid()); } public function testMaxValidationFails() { $data = [ 'age' => 200, ]; $result = Validator::validateKeys($data, function($v) { return [ $v->key('age')->numeric()->max(150, true), ]; }); $this->assertFalse($result->isValid()); $this->assertArrayHasKey('age', $result->errors()->all()); } public function testExclusiveMaxWithEqualValue() { $data = [ 'limit' => 100, ]; // exclusive 模式:等于最大值应该失败 $result = Validator::validateKeys($data, function($v) { return [ $v->key('limit')->numeric()->max(100, false), ]; }); $this->assertFalse($result->isValid()); } public function testMaxWithStringNumber() { // 测试字符串数字 $builder = new Builder(); $result = $builder->numeric()->max(100, true)->validate('50'); $this->assertTrue($result->isValid()); $result = $builder->numeric()->max(100, true)->validate('100'); $this->assertTrue($result->isValid()); $result = $builder->numeric()->max(100, true)->validate('150'); $this->assertFalse($result->isValid()); } public function testMaxWithFloat() { $builder = new Builder(); // 测试浮点数最大值 $result = $builder->max(3.14, true)->validate(3.14); $this->assertTrue($result->isValid()); $result = $builder->max(3.14, true)->validate(2.718); $this->assertTrue($result->isValid()); $result = $builder->max(3.14, false)->validate(3.14); $this->assertFalse($result->isValid()); } public function testMaxErrorMessage() { $builder = new Builder(); $result = $builder->max(100, false)->validate(150); $this->assertFalse($result->isValid()); $this->assertCount(1, $result->errors()->all()); $errorMessage = $result->firstError(); $this->assertStringContainsString('必须小于', $errorMessage); $this->assertStringContainsString('100', $errorMessage); } public function testMaxInclusiveErrorMessage() { $builder = new Builder(); $result = $builder->max(100, true)->validate(150); $this->assertFalse($result->isValid()); // 使用 firstError() 获取第一个错误 $errorMessage = $result->firstError(); $this->assertStringContainsString('必须小于或等于', $errorMessage); } public function testMaxWithCustomMessage() { $builder = new Builder(); $customMessage = '年龄不能超过150岁'; $result = $builder->max(150, true, $customMessage)->validate(200); $this->assertFalse($result->isValid()); // 使用 firstError() 获取第一个错误 $errorMessage = $result->firstError(); $this->assertEquals($customMessage, $errorMessage); } } tests/Feature/NumericValidationTest.php000064400000006520151665713370014310 0ustar00 '99.99', 'quantity' => '10', 'discount' => '0.5' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('price')->numeric(), $builder->key('quantity')->numeric(), $builder->key('discount')->numeric() ]; }); $this->assertTrue($result->isValid()); } public function testNumericValidationWithInvalidData() { $data = [ 'price' => 'not-a-number', 'quantity' => 'abc', 'discount' => null ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('price')->numeric(), $builder->key('quantity')->numeric(), $builder->key('discount')->numeric() ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('price', $errors); $this->assertArrayHasKey('quantity', $errors); $this->assertArrayHasKey('discount', $errors); } public function testNumericValidationWithEach() { $data = [ 'prices' => ['99.99', '10.50', 'abc', '25.00'] ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('prices')->each()->numeric() ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('prices.2', $errors); } public function testNumericValidationWithCustomMessage() { $data = [ 'amount' => 'invalid' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('amount')->numeric('金额必须是数字') ]; }); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertStringContainsString('金额必须是数字', $errors['amount'][0]); } public function testNumericValidationWithIntegerAndFloat() { $data = [ 'integer_value' => '100', 'float_value' => '99.99', 'negative_value' => '-50.5' ]; $result = Validator::validateKeys($data, function($builder) { return [ $builder->key('integer_value')->numeric(), $builder->key('float_value')->numeric(), $builder->key('negative_value')->numeric() ]; }); $this->assertTrue($result->isValid()); } public function testNumericValidationWithBuilder() { $result = Validator::numeric()->validate('123.45'); $this->assertTrue($result->isValid()); $result = Validator::numeric()->validate('not-a-number'); $this->assertFalse($result->isValid()); } } tests/Feature/OptionalKeyValidationTest.php000064400000004371151665713370015146 0ustar00 'admin']; $result = Validator::validateKeys($data, function($v) { return [ $v->key('username')->required(), $v->key('email')->optional()->email() // email 不存在,应跳过 ]; }); $this->assertTrue($result->isValid()); $logs = $result->getLogs(); // Find email log $emailLog = null; foreach ($logs as $log) { if ($log['field'] === 'email') { $emailLog = $log; break; } } $this->assertNotNull($emailLog); $this->assertEquals('skipped', $emailLog['status']); } public function testOptionalKeyValidatedWhenPresent() { $data = ['username' => 'admin', 'email' => 'invalid-email']; $result = Validator::validateKeys($data, function($v) { return [ $v->key('email')->optional()->email() ]; }); $this->assertFalse($result->isValid()); $logs = $result->getLogs(); $this->assertEquals('failed', $logs[0]['status']); } public function testOptionalKeyValidatedWhenValid() { $data = ['username' => 'admin', 'email' => 'test@example.com']; $result = Validator::validateKeys($data, function($v) { return [ $v->key('email')->optional()->email() ]; }); $this->assertTrue($result->isValid()); $logs = $result->getLogs(); $this->assertEquals('validated', $logs[0]['status']); } public function testNestedOptionalKeySkipped() { $data = ['profile' => ['name' => 'admin']]; $result = Validator::validateKeys($data, function($v) { return [ $v->key('profile.age')->optional()->integer() ]; }); $this->assertTrue($result->isValid()); $logs = $result->getLogs(); $this->assertEquals('skipped', $logs[0]['status']); } } tests/Feature/TransformTest.php000064400000002477151665713370012655 0ustar00transform('trim') ->transform('strtolower') ->string() ->minLength(5) ->validate(); $this->assertTrue($result->isValid()); // 内部值应该是 'hello' } public function testNumericTransformation() { $builder = new Builder('100'); $result = $builder->transform(fn($v) => (int)$v) ->integer() ->min(100) ->validate(); $this->assertTrue($result->isValid()); } public function testTransformInKeyValidator() { $data = [ 'email' => ' USER@EXAMPLE.COM ' ]; $result = Validator::validateKeys($data, function($v) { return [ $v->key('email')->transform('trim')->transform('strtolower')->email() ]; }); $this->assertTrue($result->isValid()); } } tests/Feature/ValidateOrFailTest.php000064400000013555151665713370013527 0ustar00minLength(5)->validateOrFail('hello world'); $this->assertTrue($result->isValid()); } public function testValidateOrFailWithInvalidData() { $this->expectException(ValidationException::class); $this->expectExceptionMessage('value 长度不能小于 5'); Validator::string()->minLength(5)->validateOrFail('hi'); } public function testValidateOrFailWithMultipleRules() { $this->expectException(ValidationException::class); $this->expectExceptionMessage('value 必须是有效的手机号码'); Validator::string()->phone()->validateOrFail('invalid'); } public function testValidateKeysOrFailWithValidData() { $data = [ 'username' => '张三', 'phone' => '13800138000', 'email' => 'test@example.com' ]; $result = Validator::validateKeysOrFail($data, function($validator) { return [ $validator->key('username')->required()->chineseLength(2, 10), $validator->key('phone')->required()->phone(), $validator->key('email')->required()->email() ]; }); $this->assertTrue($result->isValid()); } public function testValidateKeysOrFailWithInvalidData() { $this->expectException(ValidationException::class); $data = [ 'username' => '张', 'phone' => '13800138000', 'email' => 'test@example.com' ]; Validator::validateKeysOrFail($data, function($validator) { return [ $validator->key('username')->required()->chineseLength(2, 10), $validator->key('phone')->required()->phone(), $validator->key('email')->required()->email() ]; }); } public function testValidateKeysOrFailStopsAtFirstError() { $this->expectException(ValidationException::class); $this->expectExceptionMessage('username 中文字符长度必须在 2 到 10 之间'); $data = [ 'username' => '张', 'phone' => 'invalid', 'email' => 'invalid' ]; Validator::validateKeysOrFail($data, function($validator) { return [ $validator->key('username')->required()->chineseLength(2, 10), $validator->key('phone')->required()->phone(), $validator->key('email')->required()->email() ]; }); } public function testKeyValidatorValidateOrFail() { $data = ['phone' => '13800138000']; $result = Validator::make($data) ->key('phone') ->phone() ->validateOrFail(); $this->assertTrue($result->isValid()); } public function testKeyValidatorValidateOrFailWithInvalidData() { $this->expectException(ValidationException::class); $this->expectExceptionMessage('phone 必须是有效的手机号码'); $data = ['phone' => 'invalid']; Validator::make($data) ->key('phone') ->phone() ->validateOrFail(); } public function testEachValidatorValidateOrFail() { $data = ['phones' => ['13800138000', '13912345678', '18612345678']]; $result = Validator::make($data) ->key('phones') ->each() ->phone() ->validateOrFail(); $this->assertTrue($result->isValid()); } public function testEachValidatorValidateOrFailWithInvalidData() { $this->expectException(ValidationException::class); $this->expectExceptionMessage('phones.0 必须是有效的手机号码'); $data = ['phones' => ['invalid', '13912345678', '18612345678']]; Validator::make($data) ->key('phones') ->each() ->phone() ->validateOrFail(); } public function testEachValidatorValidateOrFailStopsAtFirstError() { $this->expectException(ValidationException::class); $this->expectExceptionMessage('phones.1 必须是有效的手机号码'); $data = ['phones' => ['13800138000', 'invalid', '18612345678']]; Validator::make($data) ->key('phones') ->each() ->phone() ->validateOrFail(); } public function testValidateOrFailWithCustomMessage() { $this->expectException(ValidationException::class); $this->expectExceptionMessage('自定义错误消息'); Validator::string()->minLength(5, '自定义错误消息')->validateOrFail('hi'); } public function testValidateOrFailWithIpValidation() { $result = Validator::ip('v4')->validateOrFail('192.168.1.1'); $this->assertTrue($result->isValid()); } public function testValidateOrFailWithInvalidIp() { $this->expectException(ValidationException::class); $this->expectExceptionMessage('value 必须是有效的 IPv4 地址'); Validator::ip('v4')->validateOrFail('256.168.1.1'); } public function testValidateOrFailWithDomainValidation() { $result = Validator::domain()->validateOrFail('example.com'); $this->assertTrue($result->isValid()); } public function testValidateOrFailWithInvalidDomain() { $this->expectException(ValidationException::class); $this->expectExceptionMessage('value 必须是有效的域名'); Validator::domain()->validateOrFail('invalid'); } } tests/Feature/ValidatorAdvancedTest.php000064400000014111151665713370014241 0ustar00 'ab', 'email' => 'invalid-email' ]; $rules = [ 'username' => 'required|min:3,message:用户名至少需要3个字符', 'email' => 'required|email,message:请输入有效的邮箱地址' ]; $result = Validator::validate($data, $rules); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('username', $errors); $this->assertArrayHasKey('email', $errors); $this->assertStringContainsString('用户名至少需要3个字符', $errors['username'][0]); $this->assertStringContainsString('请输入有效的邮箱地址', $errors['email'][0]); } public function testValidateOrFailWithValidData() { $data = [ 'username' => 'john', 'email' => 'john@example.com' ]; $rules = [ 'username' => 'required|min:3', 'email' => 'required|email' ]; $result = Validator::validateOrFail($data, $rules); $this->assertTrue($result->isValid()); } public function testValidateOrFailWithInvalidData() { $this->expectException(ValidationException::class); $data = [ 'username' => 'ab', 'email' => 'invalid' ]; $rules = [ 'username' => 'required|min:3', 'email' => 'required|email' ]; Validator::validateOrFail($data, $rules); } public function testValidateNestedArray() { $data = [ [ 'name' => 'John', 'age' => '25' ], [ 'name' => 'A', 'age' => 'not-a-number' ], [ 'name' => 'Jane', 'age' => '30' ] ]; $rules = [ 'name' => 'required|min:2', 'age' => 'required|integer' ]; $result = Validator::validateNested($data, $rules); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('1.name', $errors); $this->assertArrayHasKey('1.age', $errors); } public function testValidateNestedArrayWithAllValid() { $data = [ [ 'name' => 'John', 'age' => '25' ], [ 'name' => 'Jane', 'age' => '30' ] ]; $rules = [ 'name' => 'required|min:2', 'age' => 'required|integer' ]; $result = Validator::validateNested($data, $rules); $this->assertTrue($result->isValid()); } public function testValidateWithInRule() { $data = [ 'status' => 'pending', 'priority' => 'high' ]; $rules = [ 'status' => 'required|in:pending,active,completed', 'priority' => 'required|in:low,medium,high' ]; $result = Validator::validate($data, $rules); $this->assertTrue($result->isValid()); } public function testValidateWithInRuleInvalid() { $data = [ 'status' => 'invalid_status' ]; $rules = [ 'status' => 'required|in:pending,active,completed' ]; $result = Validator::validate($data, $rules); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('status', $errors); } public function testValidateWithRegexRule() { $data = [ 'phone' => '13800138000' ]; $rules = [ 'phone' => 'required|regex:/^1[3-9]\d{9}$/' ]; $result = Validator::validate($data, $rules); $this->assertTrue($result->isValid()); } public function testValidateWithRegexRuleInvalid() { $data = [ 'phone' => '123456' ]; $rules = [ 'phone' => 'required|regex:/^1[3-9]\d{9}$/' ]; $result = Validator::validate($data, $rules); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('phone', $errors); } public function testValidateWithNestedFields() { $data = [ 'user' => [ 'profile' => [ 'name' => 'John', 'email' => 'invalid-email' ] ] ]; $rules = [ 'user.profile.name' => 'required|min:2', 'user.profile.email' => 'required|email' ]; $result = Validator::validate($data, $rules); $this->assertFalse($result->isValid()); $errors = $result->errors()->all(); $this->assertArrayHasKey('user.profile.email', $errors); } public function testValidationExceptionFromValidationResult() { $data = [ 'username' => 'ab' ]; $rules = [ 'username' => 'required|min:3' ]; try { Validator::validateOrFail($data, $rules); $this->fail('Expected ValidationException was not thrown'); } catch (ValidationException $e) { $this->assertInstanceOf(ValidationException::class, $e); $this->assertStringContainsString('username', $e->getMessage()); $errors = $e->toArray(); $this->assertArrayHasKey('username', $errors); } } } tests/Feature/ValidatorTest.php000064400000002120151665713370012610 0ustar00validate('hello'); $this->assertTrue($result->isValid()); } public function testIntegerValidation() { $result = Validator::integer()->validate(123); $this->assertTrue($result->isValid()); } public function testEmailValidation() { $result = Validator::email()->validate('test@example.com'); $this->assertTrue($result->isValid()); } public function testChainedValidation() { $result = Validator::string() ->minLength(5) ->maxLength(10) ->validate('hello'); $this->assertTrue($result->isValid()); } public function testCustomMessage() { $result = Validator::string('必须是字符串')->validate(123); $this->assertEquals('必须是字符串', $result->firstError()); } }tests/Unit/BuilderTest.php000064400000003657151665713370011615 0ustar00string(); $result = $builder->validate('hello'); $this->assertTrue($result->isValid()); } public function testStringRuleWithInvalidValue() { $builder = new Builder(); $builder->string(); $result = $builder->validate(123); $this->assertTrue($result->isInvalid()); } public function testChainedRules() { $builder = new Builder(); $builder->string()->minLength(5)->maxLength(10); $result = $builder->validate('hello'); $this->assertTrue($result->isValid()); } public function testChainedRulesWithInvalidValue() { $builder = new Builder(); $builder->string()->minLength(5); $result = $builder->validate('hi'); $this->assertTrue($result->isInvalid()); } public function testCustomMessage() { $builder = new Builder(); $builder->string('必须是字符串'); $result = $builder->validate(123); $this->assertEquals('必须是字符串', $result->firstError()); } public function testMessagesArray() { $builder = new Builder(); $builder->string()->minLength(5)->messages([ 'string' => '必须是字符串', 'minLength' => '至少5个字符' ]); $result = $builder->validate(123); $this->assertEquals('必须是字符串', $result->firstError()); } public function testCheckMethod() { $builder = new Builder(); $builder->string(); $this->assertTrue($builder->check('hello')); $this->assertFalse($builder->check(123)); } }tests/Unit/Messages/MessageBagTest.php000064400000004331151665713370013762 0ustar00add('username', '用户名必填'); $this->assertEquals(['用户名必填'], $bag->get('username')); } public function testGetAllMessages() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $bag->add('email', '邮箱格式错误'); $expected = [ 'username' => ['用户名必填'], 'email' => ['邮箱格式错误'] ]; $this->assertEquals($expected, $bag->all()); } public function testFirstMessage() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $bag->add('username', '用户名太短'); $this->assertEquals('用户名必填', $bag->first('username')); } public function testHasField() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $this->assertTrue($bag->has('username')); $this->assertFalse($bag->has('email')); } public function testIsEmpty() { $bag = new MessageBag(); $this->assertTrue($bag->isEmpty()); $bag->add('username', '用户名必填'); $this->assertFalse($bag->isEmpty()); } public function testToArray() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $this->assertEquals(['username' => ['用户名必填']], $bag->toArray()); } public function testToJson() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $json = $bag->toJson(); $decoded = json_decode($json, true); $this->assertEquals(['username' => ['用户名必填']], $decoded); } public function testToString() { $bag = new MessageBag(); $bag->add('username', '用户名必填'); $this->assertEquals('用户名必填', (string)$bag); } }tests/Unit/Rules/AbstractRuleTest.php000064400000004057151665713370013707 0ustar00assertEquals('test', $rule->getName()); } public function testSetAndGetMessage() { $rule = new TestRule('自定义消息'); $this->assertEquals('自定义消息', $rule->getMessage()); $rule->setMessage('新消息'); $this->assertEquals('新消息', $rule->getMessage()); } public function testGetParameters() { $rule = new TestRuleWithParams(5, 10); $this->assertEquals(['min' => 5, 'max' => 10], $rule->getParameters()); } public function testGetDefaultMessage() { $rule = new TestRule(); $this->assertEquals('默认消息', $rule->getDefaultMessage()); } public function testGetFormattedMessage() { $rule = new TestRuleWithParams(5, 10); $message = $rule->getFormattedMessage(['field' => 'username']); $this->assertStringContainsString('username', $message); $this->assertStringContainsString('5', $message); $this->assertStringContainsString('10', $message); } } class TestRule extends AbstractRule { protected string $name = 'test'; public function validate(mixed $value): bool { return true; } public function getDefaultMessage(): string { return '默认消息'; } } class TestRuleWithParams extends AbstractRule { protected string $name = 'test'; public function __construct(int $min, int $max, ?string $message = null) { parent::__construct($message); $this->parameters['min'] = $min; $this->parameters['max'] = $max; } public function validate(mixed $value): bool { return true; } public function getDefaultMessage(): string { return '{field} 必须在 {min} 到 {max} 之间'; } }tests/Unit/Rules/BetweenRuleTest.php000064400000001757151665713370013541 0ustar00assertTrue($rule->validate(1)); $this->assertTrue($rule->validate(10)); $this->assertTrue($rule->validate(5)); $this->assertFalse($rule->validate(0)); $this->assertFalse($rule->validate(11)); } public function testOpenInterval() { $rule = new BetweenRule(1, 10, false, false); $this->assertFalse($rule->validate(1)); $this->assertFalse($rule->validate(10)); $this->assertTrue($rule->validate(5)); } public function testHalfOpenInterval() { $rule = new BetweenRule(1, 10, true, false); $this->assertTrue($rule->validate(1)); $this->assertFalse($rule->validate(10)); } } tests/Unit/Rules/ChineseLengthRuleTest.php000064400000003672151665713370014666 0ustar00assertTrue($rule->validate('你好')); $this->assertTrue($rule->validate('你好世界')); $this->assertTrue($rule->validate('你好世界今天天气不错')); } public function testInvalidChineseLengthTooShort() { $rule = new ChineseLengthRule(2, 10); $this->assertFalse($rule->validate('你')); } public function testInvalidChineseLengthTooLong() { $rule = new ChineseLengthRule(2, 10); $this->assertFalse($rule->validate('你好世界今天天气不错啊')); } public function testNonStringValue() { $rule = new ChineseLengthRule(2, 10); $this->assertFalse($rule->validate(123)); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate(['你好'])); } public function testMixedChineseAndEnglish() { $rule = new ChineseLengthRule(2, 10); $this->assertTrue($rule->validate('你好world')); $this->assertTrue($rule->validate('Hello世界')); } public function testCustomMessage() { $rule = new ChineseLengthRule(2, 10, '自定义错误消息'); $this->assertEquals('自定义错误消息', $rule->getMessage()); } public function testGetParameters() { $rule = new ChineseLengthRule(2, 10); $this->assertEquals(['min' => 2, 'max' => 10], $rule->getParameters()); } public function testGetDefaultMessage() { $rule = new ChineseLengthRule(2, 10); $this->assertEquals('{field} 中文字符长度必须在 {min} 到 {max} 之间', $rule->getDefaultMessage()); } } tests/Unit/Rules/DateRuleTest.php000064400000011057151665713370013017 0ustar00assertTrue($rule->validate('2024-01-01')); $this->assertTrue($rule->validate('2024-12-31')); $this->assertTrue($rule->validate('2024-01-15 10:30:00')); $this->assertTrue($rule->validate('now')); $this->assertTrue($rule->validate('+1 day')); } public function testInvalidDateWithoutFormat() { $rule = new DateRule(); $this->assertFalse($rule->validate('not-a-date')); $this->assertFalse($rule->validate(123)); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate([])); } public function testValidDateWithFormat() { $rule = new DateRule('Y-m-d'); $this->assertTrue($rule->validate('2024-01-01')); $this->assertTrue($rule->validate('2024-12-31')); $this->assertTrue($rule->validate('2024-06-15')); } public function testInvalidDateWithFormat() { $rule = new DateRule('Y-m-d'); $this->assertFalse($rule->validate('2024-01-01 10:30:00')); $this->assertFalse($rule->validate('01/01/2024')); $this->assertFalse($rule->validate('2024/01/01')); $this->assertFalse($rule->validate('not-a-date')); } public function testValidDateWithCustomFormat() { $rule = new DateRule('d/m/Y'); $this->assertTrue($rule->validate('01/01/2024')); $this->assertTrue($rule->validate('31/12/2024')); $this->assertTrue($rule->validate('15/06/2024')); } public function testInvalidDateWithCustomFormat() { $rule = new DateRule('d/m/Y'); $this->assertFalse($rule->validate('2024-01-01')); $this->assertFalse($rule->validate('01-01-2024')); $this->assertFalse($rule->validate('2024/01/01')); } public function testValidDateWithTimeFormat() { $rule = new DateRule('H:i:s'); $this->assertTrue($rule->validate('10:30:00')); $this->assertTrue($rule->validate('23:59:59')); $this->assertTrue($rule->validate('00:00:00')); } public function testInvalidDateWithTimeFormat() { $rule = new DateRule('H:i:s'); $this->assertFalse($rule->validate('25:00:00')); $this->assertFalse($rule->validate('10:30')); $this->assertFalse($rule->validate('not-a-time')); } public function testGetConvertedValueWithoutFormat() { $rule = new DateRule(); $result = $rule->getConvertedValue('2024-01-01'); $this->assertInstanceOf(\DateTime::class, $result); $this->assertEquals('2024-01-01', $result->format('Y-m-d')); } public function testGetConvertedValueWithFormat() { $rule = new DateRule('Y-m-d'); $result = $rule->getConvertedValue('2024-01-01'); $this->assertInstanceOf(\DateTime::class, $result); $this->assertEquals('2024-01-01', $result->format('Y-m-d')); } public function testGetConvertedValueWithInvalidInput() { $rule = new DateRule(); $result = $rule->getConvertedValue('not-a-date'); $this->assertEquals('not-a-date', $result); } public function testGetDefaultMessage() { $rule = new DateRule(); $this->assertEquals('{field} 必须是有效的日期', $rule->getDefaultMessage()); } public function testGetDefaultMessageWithFormat() { $rule = new DateRule('Y-m-d'); $this->assertEquals('{field} 必须是有效的日期格式: {format}', $rule->getDefaultMessage()); } public function testCustomMessage() { $rule = new DateRule('Y-m-d', '自定义错误消息'); $this->assertEquals('自定义错误消息', $rule->getMessage()); } public function testGetFormattedMessage() { $rule = new DateRule('Y-m-d'); $message = $rule->getFormattedMessage(['field' => '出生日期']); $this->assertEquals('出生日期 必须是有效的日期格式: Y-m-d', $message); } public function testGetName() { $rule = new DateRule(); $this->assertEquals('date', $rule->getName()); } public function testGetParameters() { $rule = new DateRule('Y-m-d'); $params = $rule->getParameters(); $this->assertIsArray($params); $this->assertEquals('Y-m-d', $params['format']); } }tests/Unit/Rules/DomainRuleTest.php000064400000003731151665713370013351 0ustar00assertTrue($rule->validate('example.com')); $this->assertTrue($rule->validate('www.example.com')); $this->assertTrue($rule->validate('subdomain.example.com')); $this->assertTrue($rule->validate('my-domain.example.com')); $this->assertTrue($rule->validate('test123.example.com')); $this->assertTrue($rule->validate('a.b.c.example.com')); } public function testInvalidDomains() { $rule = new DomainRule(); $this->assertFalse($rule->validate('example')); $this->assertFalse($rule->validate('example.')); $this->assertFalse($rule->validate('.example.com')); $this->assertFalse($rule->validate('-example.com')); $this->assertFalse($rule->validate('example-.com')); $this->assertFalse($rule->validate('example..com')); $this->assertFalse($rule->validate('example.c')); $this->assertFalse($rule->validate('example.1a')); } public function testNonStringValue() { $rule = new DomainRule(); $this->assertFalse($rule->validate(123)); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate(['example.com'])); } public function testCustomMessage() { $rule = new DomainRule('自定义错误消息'); $this->assertEquals('自定义错误消息', $rule->getMessage()); } public function testGetParameters() { $rule = new DomainRule(); $this->assertEquals([], $rule->getParameters()); } public function testGetDefaultMessage() { $rule = new DomainRule(); $this->assertEquals('{field} 必须是有效的域名', $rule->getDefaultMessage()); } } tests/Unit/Rules/EmailRuleTest.php000064400000001275151665713370013172 0ustar00assertTrue($rule->validate('test@example.com')); $this->assertTrue($rule->validate('user.name+tag@domain.co.uk')); } public function testInvalidEmail() { $rule = new EmailRule(); $this->assertFalse($rule->validate('invalid')); $this->assertFalse($rule->validate('invalid@')); $this->assertFalse($rule->validate('@example.com')); $this->assertFalse($rule->validate(123)); } }tests/Unit/Rules/IntegerRuleTest.php000064400000001613151665713370013534 0ustar00assertTrue($rule->validate(123)); $this->assertTrue($rule->validate(0)); $this->assertTrue($rule->validate(-456)); $this->assertTrue($rule->validate('123')); $this->assertTrue($rule->validate('0')); $this->assertTrue($rule->validate('-456')); } public function testInvalidInteger() { $rule = new IntegerRule(); $this->assertFalse($rule->validate('12.34')); $this->assertFalse($rule->validate(12.34)); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate('abc')); $this->assertFalse($rule->validate([])); } }tests/Unit/Rules/IntegerTypeTest.php000064400000002664151665713370013555 0ustar00assertTrue($rule->validate(123)); $this->assertTrue($rule->validate('123')); // 十六进制 (0x) $this->assertTrue($rule->validate('0x1A')); // 八进制 (0) $this->assertTrue($rule->validate('0777')); // 非整数 $this->assertFalse($rule->validate('12.34')); $this->assertFalse($rule->validate('abc')); } public function testStrictIntegerRule() { $rule = new StrictIntegerRule(); $this->assertTrue($rule->validate(123)); $this->assertFalse($rule->validate('123')); $this->assertFalse($rule->validate(12.3)); } public function testSafeIntegerRule() { $rule = new SafeIntegerRule(); $this->assertTrue($rule->validate(PHP_INT_MAX)); $this->assertTrue($rule->validate(PHP_INT_MIN)); // 超出范围(使用字符串模拟大数) $largeValue = (string)PHP_INT_MAX . '0'; $this->assertFalse($rule->validate($largeValue)); } } tests/Unit/Rules/IpRuleTest.php000064400000005763151665713370012521 0ustar00assertTrue($rule->validate('192.168.1.1')); $this->assertTrue($rule->validate('10.0.0.1')); $this->assertTrue($rule->validate('172.16.0.1')); $this->assertTrue($rule->validate('8.8.8.8')); $this->assertTrue($rule->validate('255.255.255.255')); } public function testInvalidIPv4Addresses() { $rule = new IpRule('v4'); $this->assertFalse($rule->validate('192.168.1')); $this->assertFalse($rule->validate('192.168.1.1.1')); $this->assertFalse($rule->validate('256.168.1.1')); $this->assertFalse($rule->validate('192.168.1.256')); $this->assertFalse($rule->validate('192.168.1.1a')); } public function testValidIPv6Addresses() { $rule = new IpRule('v6'); $this->assertTrue($rule->validate('2001:0db8:85a3:0000:0000:8a2e:0370:7334')); $this->assertTrue($rule->validate('2001:db8:85a3::8a2e:370:7334')); $this->assertTrue($rule->validate('::1')); $this->assertTrue($rule->validate('fe80::1')); } public function testInvalidIPv6Addresses() { $rule = new IpRule('v6'); $this->assertFalse($rule->validate('192.168.1.1')); $this->assertFalse($rule->validate('2001:0db8:85a3:0000:0000:8a2e:0370:7334:1234')); $this->assertFalse($rule->validate('2001:0db8:85a3:0000:0000:8a2e:0370:733g')); } public function testAnyIPAddresses() { $rule = new IpRule('any'); $this->assertTrue($rule->validate('192.168.1.1')); $this->assertTrue($rule->validate('2001:0db8:85a3:0000:0000:8a2e:0370:7334')); $this->assertTrue($rule->validate('::1')); } public function testNonStringValue() { $rule = new IpRule('v4'); $this->assertFalse($rule->validate(19216811)); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate(['192.168.1.1'])); } public function testCustomMessage() { $rule = new IpRule('v4', '自定义错误消息'); $this->assertEquals('自定义错误消息', $rule->getMessage()); } public function testGetParameters() { $rule = new IpRule('v4'); $this->assertEquals(['version' => 'v4'], $rule->getParameters()); } public function testGetDefaultMessage() { $ruleV4 = new IpRule('v4'); $ruleV6 = new IpRule('v6'); $ruleAny = new IpRule('any'); $this->assertEquals('{field} 必须是有效的 IPv4 地址', $ruleV4->getDefaultMessage()); $this->assertEquals('{field} 必须是有效的 IPv6 地址', $ruleV6->getDefaultMessage()); $this->assertEquals('{field} 必须是有效的 IP 地址', $ruleAny->getDefaultMessage()); } } tests/Unit/Rules/JsonRuleTest.php000064400000014607151665713370013057 0ustar00assertTrue($rule->validate('{"key": "value"}')); $this->assertTrue($rule->validate('{"number": 123}')); $this->assertTrue($rule->validate('{"boolean": true}')); $this->assertTrue($rule->validate('{"array": [1, 2, 3]}')); $this->assertTrue($rule->validate('{"nested": {"key": "value"}}')); } public function testValidJsonArray() { $rule = new JsonRule(); $this->assertTrue($rule->validate('[1, 2, 3]')); $this->assertTrue($rule->validate('["a", "b", "c"]')); $this->assertTrue($rule->validate('[[]]')); $this->assertTrue($rule->validate('[{"key": "value"}]')); } public function testValidJsonSimpleValues() { $rule = new JsonRule(); $this->assertTrue($rule->validate('null')); $this->assertTrue($rule->validate('true')); $this->assertTrue($rule->validate('false')); $this->assertTrue($rule->validate('123')); $this->assertTrue($rule->validate('"string"')); } public function testInvalidJson() { $rule = new JsonRule(); $this->assertFalse($rule->validate('not-json')); $this->assertFalse($rule->validate('{key: "value"}')); $this->assertFalse($rule->validate("{'key': 'value'}")); $this->assertFalse($rule->validate('{"key": undefined}')); $this->assertFalse($rule->validate('{"key": value}')); $this->assertFalse($rule->validate(123)); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate([])); } public function testInvalidJsonWithSyntaxErrors() { $rule = new JsonRule(); $this->assertFalse($rule->validate('{"key": "value"')); $this->assertFalse($rule->validate('{"key": "value"}]')); $this->assertFalse($rule->validate('{"key": "value",}')); $this->assertFalse($rule->validate('[1, 2, 3')); } public function testValidJsonWithMaxDepth() { $rule = new JsonRule(10); $this->assertTrue($rule->validate('{"key": "value"}')); $this->assertTrue($rule->validate('{"key": {"nested": "value"}}')); $this->assertTrue($rule->validate('{"key": {"nested": {"deep": "value"}}}')); } public function testInvalidJsonWithMaxDepth() { $rule = new JsonRule(1); $this->assertFalse($rule->validate('{"key": {"nested": {"deep": "value"}}}')); } public function testValidJsonWithFlags() { $rule = new JsonRule(null, JSON_OBJECT_AS_ARRAY); $this->assertTrue($rule->validate('{"key": "value"}')); $this->assertTrue($rule->validate('[1, 2, 3]')); } public function testGetConvertedValue() { $rule = new JsonRule(); $result = $rule->getConvertedValue('{"key": "value"}'); $this->assertIsArray($result); $this->assertEquals('value', $result['key']); } public function testGetConvertedValueWithArray() { $rule = new JsonRule(); $result = $rule->getConvertedValue('[1, 2, 3]'); $this->assertIsArray($result); $this->assertEquals([1, 2, 3], $result); } public function testGetConvertedValueWithNestedStructure() { $rule = new JsonRule(); $result = $rule->getConvertedValue('{"key": {"nested": "value"}}'); $this->assertIsArray($result); $this->assertIsArray($result['key']); $this->assertEquals('value', $result['key']['nested']); } public function testGetConvertedValueWithInvalidInput() { $rule = new JsonRule(); $result = $rule->getConvertedValue('not-json'); $this->assertEquals('not-json', $result); } public function testGetConvertedValueWithNonStringInput() { $rule = new JsonRule(); $result = $rule->getConvertedValue(123); $this->assertEquals(123, $result); } public function testGetDefaultMessage() { $rule = new JsonRule(); $this->assertEquals('{field} 必须是有效的JSON字符串', $rule->getDefaultMessage()); } public function testCustomMessage() { $rule = new JsonRule(10, JSON_OBJECT_AS_ARRAY, '自定义错误消息'); $this->assertEquals('自定义错误消息', $rule->getMessage()); } public function testGetFormattedMessage() { $rule = new JsonRule(); $message = $rule->getFormattedMessage(['field' => '配置数据']); $this->assertEquals('配置数据 必须是有效的JSON字符串', $message); } public function testGetName() { $rule = new JsonRule(); $this->assertEquals('json', $rule->getName()); } public function testGetParameters() { $rule = new JsonRule(10, JSON_OBJECT_AS_ARRAY); $params = $rule->getParameters(); $this->assertIsArray($params); $this->assertEquals(10, $params['maxDepth']); $this->assertEquals(JSON_OBJECT_AS_ARRAY, $params['flags']); } public function testGetParametersWithNullValues() { $rule = new JsonRule(); $params = $rule->getParameters(); $this->assertIsArray($params); $this->assertNull($params['maxDepth']); $this->assertNull($params['flags']); } public function testValidateWithEmptyString() { $rule = new JsonRule(); $this->assertFalse($rule->validate('')); } public function testValidateWithWhitespace() { $rule = new JsonRule(); $this->assertTrue($rule->validate(' {"key": "value"} ')); $this->assertTrue($rule->validate("\n{\"key\": \"value\"}\n")); } public function testValidateWithUnicode() { $rule = new JsonRule(); $this->assertTrue($rule->validate('{"key": "中文"}')); $this->assertTrue($rule->validate('{"emoji": "🎉"}')); } public function testValidateWithSpecialCharacters() { $rule = new JsonRule(); $this->assertTrue($rule->validate('{"key": "value\\nwith\\nnewlines"}')); $this->assertTrue($rule->validate('{"key": "value\\ttabs"}')); $this->assertTrue($rule->validate('{"key": "value\\"quote\\""}')); } }tests/Unit/Rules/MathPropertyTest.php000064400000004206151665713370013746 0ustar00assertTrue($rule->validate(2)); $this->assertTrue($rule->validate('4')); $this->assertFalse($rule->validate(3)); } public function testOddRule() { $rule = new OddRule(); $this->assertTrue($rule->validate(3)); $this->assertTrue($rule->validate('5')); $this->assertFalse($rule->validate(2)); } public function testPositiveRule() { $rule = new PositiveRule(); $this->assertTrue($rule->validate(1)); $this->assertTrue($rule->validate('0.1')); $this->assertFalse($rule->validate(0)); $this->assertFalse($rule->validate(-1)); } public function testNegativeRule() { $rule = new NegativeRule(); $this->assertTrue($rule->validate(-1)); $this->assertTrue($rule->validate('-0.1')); $this->assertFalse($rule->validate(0)); $this->assertFalse($rule->validate(1)); } public function testPrimeRule() { $rule = new PrimeRule(); $this->assertTrue($rule->validate(2)); $this->assertTrue($rule->validate(3)); $this->assertTrue($rule->validate(5)); $this->assertTrue($rule->validate(7)); $this->assertTrue($rule->validate(11)); $this->assertTrue($rule->validate(13)); $this->assertTrue($rule->validate(17)); $this->assertTrue($rule->validate(19)); $this->assertFalse($rule->validate(1)); $this->assertFalse($rule->validate(4)); $this->assertFalse($rule->validate(6)); $this->assertFalse($rule->validate(9)); $this->assertFalse($rule->validate(15)); $this->assertFalse($rule->validate(21)); $this->assertFalse($rule->validate(25)); } } tests/Unit/Rules/MaxRuleTest.php000064400000006506151665713370012672 0ustar00assertTrue($rule->validate(99)); $this->assertTrue($rule->validate(99.99)); $this->assertTrue($rule->validate('99')); // 等于最大值不应通过(exclusive 模式) $this->assertFalse($rule->validate(100)); $this->assertFalse($rule->validate(100.0)); $this->assertFalse($rule->validate('100')); // 大于最大值不应通过 $this->assertFalse($rule->validate(101)); $this->assertFalse($rule->validate(100.01)); } public function testInclusiveMaxValidation() { $rule = new MaxRule(100, true); // 小于最大值应通过 $this->assertTrue($rule->validate(99)); $this->assertTrue($rule->validate(99.99)); // 等于最大值应通过(inclusive 模式) $this->assertTrue($rule->validate(100)); $this->assertTrue($rule->validate(100.0)); $this->assertTrue($rule->validate('100')); // 大于最大值不应通过 $this->assertFalse($rule->validate(101)); $this->assertFalse($rule->validate(100.01)); } public function testNonNumericValue() { $rule = new MaxRule(100); $this->assertFalse($rule->validate('abc')); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate([])); $this->assertFalse($rule->validate(new \stdClass())); } public function testDefaultMessageForExclusive() { $rule = new MaxRule(100, false); $message = $rule->getDefaultMessage(); $this->assertStringContainsString('必须小于', $message); $this->assertStringContainsString('100', $message); } public function testDefaultMessageForInclusive() { $rule = new MaxRule(100, true); $message = $rule->getDefaultMessage(); $this->assertStringContainsString('必须小于或等于', $message); $this->assertStringContainsString('100', $message); } public function testCustomMessage() { $customMessage = '年龄必须在合理范围内'; $rule = new MaxRule(150, false, $customMessage); $this->assertEquals($customMessage, $rule->getMessage()); } public function testNegativeMaxValue() { $rule = new MaxRule(-10, true); $this->assertTrue($rule->validate(-15)); $this->assertTrue($rule->validate(-10)); $this->assertFalse($rule->validate(-5)); } public function testFloatMaxValue() { $rule = new MaxRule(3.14, true); $this->assertTrue($rule->validate(3.14)); $this->assertTrue($rule->validate(2.718)); $this->assertFalse($rule->validate(3.15)); } public function testZeroMaxValue() { $rule = new MaxRule(0, true); $this->assertTrue($rule->validate(0)); $this->assertTrue($rule->validate(-1)); $this->assertFalse($rule->validate(1)); } } tests/Unit/Rules/MinRuleTest.php000064400000001466151665713370012670 0ustar00assertTrue($rule->validate(10)); $this->assertTrue($rule->validate(11)); $this->assertFalse($rule->validate(9)); } public function testExclusiveMinValidation() { $rule = new MinRule(10, false); $this->assertFalse($rule->validate(10)); $this->assertTrue($rule->validate(11)); $this->assertFalse($rule->validate(9)); } public function testNonNumericValue() { $rule = new MinRule(10); $this->assertFalse($rule->validate('abc')); } } tests/Unit/Rules/NumericRuleTest.php000064400000001675151665713370013551 0ustar00assertTrue($rule->validate(123)); $this->assertTrue($rule->validate(0)); $this->assertTrue($rule->validate(-456)); $this->assertTrue($rule->validate(12.34)); $this->assertTrue($rule->validate('123')); $this->assertTrue($rule->validate('0')); $this->assertTrue($rule->validate('-456')); $this->assertTrue($rule->validate('12.34')); } public function testInvalidNumeric() { $rule = new NumericRule(); $this->assertFalse($rule->validate('abc')); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate([])); $this->assertFalse($rule->validate(true)); } } tests/Unit/Rules/PhoneRuleTest.php000064400000003341151665713370013210 0ustar00assertTrue($rule->validate('13800138000')); $this->assertTrue($rule->validate('13912345678')); $this->assertTrue($rule->validate('15012345678')); $this->assertTrue($rule->validate('18612345678')); $this->assertTrue($rule->validate('19912345678')); } public function testInvalidPhoneNumbers() { $rule = new PhoneRule(); $this->assertFalse($rule->validate('12800138000')); $this->assertFalse($rule->validate('1380013800')); $this->assertFalse($rule->validate('138001380000')); $this->assertFalse($rule->validate('1380013800a')); $this->assertFalse($rule->validate('12345678901')); } public function testNonStringValue() { $rule = new PhoneRule(); $this->assertFalse($rule->validate(13800138000)); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate(['13800138000'])); } public function testCustomMessage() { $rule = new PhoneRule('自定义错误消息'); $this->assertEquals('自定义错误消息', $rule->getMessage()); } public function testGetParameters() { $rule = new PhoneRule(); $this->assertEquals([], $rule->getParameters()); } public function testGetDefaultMessage() { $rule = new PhoneRule(); $this->assertEquals('{field} 必须是有效的手机号码', $rule->getDefaultMessage()); } } tests/Unit/Rules/RequiredRuleTest.php000064400000001313151665713370013714 0ustar00assertTrue($rule->validate('hello')); $this->assertTrue($rule->validate(123)); $this->assertTrue($rule->validate([1, 2, 3])); } public function testInvalidRequired() { $rule = new RequiredRule(); $this->assertFalse($rule->validate(null)); $this->assertFalse($rule->validate('')); $this->assertFalse($rule->validate(' ')); $this->assertFalse($rule->validate([])); } }tests/Unit/Rules/SanitizeRuleTest.php000064400000016166151665713370013736 0ustar00assertTrue($rule->validate('any value')); $this->assertTrue($rule->validate('')); $this->assertTrue($rule->validate(null)); $this->assertTrue($rule->validate(123)); $this->assertTrue($rule->validate([])); } public function testSanitizeEmail() { $rule = new SanitizeRule('email'); $result = $rule->getConvertedValue('test@example.com'); $this->assertStringNotContainsString('string'); $this->assertStringNotContainsString('string'); $this->assertStringNotContainsString('测试'); $this->assertStringNotContainsString(''; $result = $rule->getConvertedValue($xssPayload); $this->assertStringNotContainsString('