From 996b751f294a37d033838e63dc65acd0d10718b7 Mon Sep 17 00:00:00 2001 From: tw Date: Wed, 22 Jul 2026 14:02:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(dto):=20=E5=8D=87=E7=BA=A7=20jsonmapper=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=B9=B6=E5=A2=9E=E5=BC=BA=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=AE=89=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 netresearch/jsonmapper 从 ~5.0.0 升级到 ~6.0.0 - 为 DtoCommon 类的方法添加明确的类型声明 - 为 JsonMapper 类的方法添加类型声明和返回类型 - 更新 README 文档中的 PHP 版本要求和特性说明 - 优化 MethodParametersManager 中的数组类型检查逻辑 - 增强嵌套对象映射的类型安全性和错误处理 - 改进 DTO 别名 setter 方法的访问权限判断逻辑 - 优化属性类型注解解析和命名空间处理 --- README.md | 305 ++++++++++++++++++--------- composer.json | 2 +- src/DtoCommon.php | 8 +- src/JsonMapper.php | 32 ++- src/Scan/MethodParametersManager.php | 2 +- 5 files changed, 228 insertions(+), 121 deletions(-) diff --git a/README.md b/README.md index 8f3776d..af6bf0d 100644 --- a/README.md +++ b/README.md @@ -3,26 +3,29 @@ [![Latest Stable Version](https://img.shields.io/packagist/v/tangwei/dto)](https://packagist.org/packages/tangwei/dto) [![Total Downloads](https://img.shields.io/packagist/dt/tangwei/dto)](https://packagist.org/packages/tangwei/dto) [![License](https://img.shields.io/packagist/l/tangwei/dto)](https://github.com/tw2066/dto) -[![PHP Version](https://img.shields.io/badge/php-%3E%3D8.1-blue)](https://www.php.net) +[![PHP Version](https://img.shields.io/badge/php-%3E%3D8.2-blue)](https://www.php.net) [English](./README_EN.md) | 中文 -基于 [Hyperf](https://github.com/hyperf/hyperf) 框架的 DTO (数据传输对象) 映射和验证库,使用 PHP 8.1+ 的属性(Attributes)特性,提供优雅的请求参数绑定和验证方案。 +基于 [Hyperf](https://github.com/hyperf/hyperf) 框架的 DTO(数据传输对象)映射和验证库,使用 PHP 8 Attributes 特性,提供优雅的请求参数绑定和验证方案。 ## ✨ 特性 -- 🚀 **自动映射** - 请求参数自动映射到 PHP DTO 类 -- 🎯 **类型安全** - 利用 PHP 8.1+ 的类型系统,提供完整的类型提示 -- 🔄 **递归支持** - 支持数组、嵌套对象、递归结构 -- ✅ **数据验证** - 集成 Hyperf 验证器,提供丰富的验证注解 -- 📝 **多种参数源** - 支持 Body、Query、FormData、Header 等多种参数来源 -- 🎨 **代码优雅** - 基于 PHP 8 Attributes,代码简洁易读 -- 🔧 **易于扩展** - 支持自定义验证规则和类型转换 +- 🚀 **自动映射** — 请求参数自动映射到 PHP DTO 类 +- 🎯 **类型安全** — 利用 PHP 8.2+ 的类型系统,提供完整的类型提示 +- 🔄 **递归支持** — 支持数组、嵌套对象、递归结构(最大嵌套深度 100) +- ✅ **数据验证** — 集成 Hyperf 验证器,提供约 90 个验证注解 +- 📝 **多种参数源** — 支持 Body、Query、FormData、Header 等多种参数来源 +- 🎨 **代码优雅** — 基于 PHP 8 Attributes,代码简洁易读 +- 🔧 **易于扩展** — 支持自定义验证规则和响应字段名转换 +- 📡 **RPC 支持** — 兼容 JSON-RPC(TCP/HTTP)服务的参数验证 ## 📋 环境要求 -- PHP >= 8.1 -- Hyperf +- PHP >= 8.2 +- Hyperf ~3.2 +- (可选)hyperf/validation — 数据验证 +- (可选)symfony/serializer + symfony/property-access — RPC 对象序列化 ## 📦 安装 @@ -30,7 +33,7 @@ composer require tangwei/dto ``` -安装后,组件会自动注册,无需额外配置。 +安装后组件通过 `ConfigProvider` 自动注册,无需额外配置。 ## 📖 快速开始 @@ -81,6 +84,8 @@ class UserController } ``` +请求 `/user/info?name=tom&age=20` 时,`$request` 会自动填充并验证;验证失败抛出 `Hyperf\Validation\ValidationException`。 + ## 📚 注解说明 ### 参数来源注解 @@ -89,7 +94,7 @@ class UserController #### RequestBody -获取 POST/PUT/PATCH 请求的 Body 参数 +获取 POST/PUT/PATCH 请求的 Body 参数: ```php use Hyperf\DTO\Annotation\Contracts\RequestBody; @@ -103,7 +108,7 @@ public function create(#[RequestBody] CreateUserRequest $request) #### RequestQuery -获取 URL 查询参数(GET 参数) +获取 URL 查询参数(GET 参数): ```php use Hyperf\DTO\Annotation\Contracts\RequestQuery; @@ -117,7 +122,7 @@ public function list(#[RequestQuery] QueryRequest $request) #### RequestFormData -获取表单请求数据(Content-Type: multipart/form-data) +获取表单请求数据(Content-Type: multipart/form-data): ```php use Hyperf\DTO\Annotation\Contracts\RequestFormData; @@ -132,7 +137,7 @@ public function upload(#[RequestFormData] UploadRequest $formData) #### RequestHeader -获取请求头信息 +获取请求头信息(一个方法中最多只能有一个 `RequestHeader` 参数): ```php use Hyperf\DTO\Annotation\Contracts\RequestHeader; @@ -146,7 +151,7 @@ public function info(#[RequestHeader] HeaderRequest $headers) #### Valid -启用验证,必须与其他参数来源注解一起使用 +启用验证,必须与参数来源注解一起使用: ```php #[PostMapping(path: 'create')] @@ -172,7 +177,10 @@ public function update( } ``` -> ⚠️ **注意**:同一个方法不能同时使用 `RequestBody` 和 `RequestFormData` 注解 +> ⚠️ **注意**: +> - 同一参数上 `RequestBody`、`RequestQuery`、`RequestFormData` 互斥,只能标注其一 +> - 同一方法中 `RequestBody` 与 `RequestFormData` 不能同时存在于不同参数上 +> - 违反以上约束会在服务启动扫描阶段抛出 `Hyperf\DTO\Exception\DtoException`,提前暴露错误 ## 📝 完整示例 @@ -258,15 +266,17 @@ class CreateRequest #### 嵌套对象 DTO +嵌套对象会递归映射并递归验证(验证规则取嵌套类自身的注解): + ```php namespace App\Request; class UserRequest { public string $name; - + public int $age; - + // 嵌套对象 public Address $address; } @@ -274,9 +284,9 @@ class UserRequest class Address { public string $province; - + public string $city; - + public string $street; } ``` @@ -299,10 +309,46 @@ class BatchRequest * @var User[] */ public array $users; - - // 使用 ArrayType 注解显式指定类型 + + // 使用 ArrayType 注解显式指定类型(优先级高于 @var) #[ArrayType(User::class)] public array $members; + + // 简单类型也可以使用 PhpType 枚举 + #[ArrayType(\Hyperf\DTO\Type\PhpType::INT)] + public array $scores; +} +``` + +#### 请求体为 JSON 数组 + +控制器方法形参声明为 `array`,配合 `@param` 注解指定元素类型,可实现 JSON 数组的批量映射与逐项验证: + +```php +/** + * @param User[] $users + */ +#[PostMapping(path: 'batch')] +public function batch(#[RequestBody] #[Valid] array $users): array +{ + // $users 为 User[],每个元素都已验证并映射 +} +``` + +#### 枚举类型 + +PHP 8.1+ 的 BackedEnum 可直接作为属性类型,映射时自动按值转换: + +```php +enum Status: int +{ + case ACTIVE = 1; + case DISABLED = 0; +} + +class UserRequest +{ + public Status $status; // 请求传 1 时自动映射为 Status::ACTIVE } ``` @@ -315,10 +361,10 @@ use Hyperf\DTO\Annotation\JSONField; class ApiRequest { - // 将请求中的 user_name 映射到 userName + // 将请求中的 user_name 映射到 userName,响应序列化时也输出 user_name #[JSONField('user_name')] public string $userName; - + #[JSONField('user_age')] public int $userAge; } @@ -326,33 +372,25 @@ class ApiRequest ## ✅ 数据验证 -### 内置验证注解 - > 需要先安装 Hyperf 验证器:`composer require hyperf/validation` -本库提供了丰富的验证注解,包括: - -- `Required` - 必填项 -- `Integer` - 整数 -- `Numeric` - 数字 -- `Between` - 范围验证 -- `Min` / `Max` - 最小/最大值 -- `Email` - 邮箱格式 -- `Url` - URL 格式 -- `Date` - 日期格式 -- `DateFormat` - 指定日期格式 -- `Boolean` - 布尔值 -- `Alpha` - 字母 -- `AlphaNum` - 字母和数字 -- `AlphaDash` - 字母、数字、破折号、下划线 -- `Image` - 图片文件 -- `Json` - JSON 格式 -- `Nullable` - 可为空 -- `In` - 在指定值中 -- `NotIn` - 不在指定值中 -- `Regex` - 正则表达式 -- `Unique` - 数据库唯一 -- `Exists` - 数据库存在 +### 内置验证注解 + +本库提供 90+ 个验证注解(命名空间 `Hyperf\DTO\Annotation\Validation`),与 Laravel 验证规则一一对应,常用的包括: + +| 分类 | 注解 | +|------|------| +| 必填 | `Required`、`RequiredIf`、`RequiredUnless`、`RequiredWith`、`RequiredWithAll`、`RequiredWithout`、`RequiredWithoutAll`、`RequiredArrayKeys`、`Present`、`Filled` | +| 类型 | `Integer`、`Numeric`、`Boolean`、`Str`、`Arr`、`File`、`Image`、`Json`、`Decimal` | +| 大小 | `Between`、`Min`、`Max`、`Size`、`Digits`、`DigitsBetween`、`MinDigits`、`MaxDigits`、`MultipleOf`、`Dimensions`(图片尺寸) | +| 格式 | `Email`、`Url`、`ActiveUrl`、`Ip`、`Ipv4`、`Ipv6`、`Date`、`DateEquals`、`DateFormat`、`Uuid`、`Ulid`、`Regex`、`NotRegex`、`MacAddress`、`HexColor`、`Lowercase`、`Uppercase`、`Ascii`、`Timezone` | +| 字符串 | `Alpha`、`AlphaNum`、`AlphaDash`、`StartsWith`、`EndsWith`、`DoesntStartWith`、`DoesntEndWith`、`Contains` | +| 比较 | `Gt`、`Gte`、`Lt`、`Lte`、`Same`、`Different`、`Confirmed`、`Before`、`After`、`BeforeOrEqual`、`AfterOrEqual` | +| 枚举 | `In`、`NotIn`、`InArray`、`Distinct` | +| 文件 | `Mimes`、`Mimetypes`、`Extensions` | +| 数据库 | `Unique`、`Exists`(支持传入 Model 类名自动解析表名) | +| 排除 | `Exclude`、`ExcludeIf`、`ExcludeUnless`、`ExcludeWith`、`ExcludeWithout`、`Prohibits`、`Missing`、`MissingIf`、`MissingUnless`、`MissingWith`、`MissingWithAll` | +| 其他 | `Nullable`、`Sometimes`、`Bail`、`Accepted`、`AcceptedIf`、`Declined`、`Validation`(自定义规则) | ### 使用示例 @@ -387,20 +425,22 @@ public function query(#[RequestQuery] #[Valid] DemoQuery $request) #### 自定义错误消息 +每个验证注解的最后一个参数为自定义消息: + ```php class UserRequest { - #[Required("用户名不能为空”)] + #[Required('用户名不能为空')] public string $name; - #[Between(18, 100, "年龄必须在 18-100 之间")] + #[Between(18, 100, '年龄必须在 18-100 之间')] public int $age; } ``` #### 使用 Validation 注解 -`Validation` 注解支持 Laravel 风格的验证规则: +`Validation` 注解支持 Laravel 风格的验证规则字符串,并可通过 `customKey` 验证数组元素: ```php use Hyperf\DTO\Annotation\Validation\Validation; @@ -408,15 +448,17 @@ use Hyperf\DTO\Annotation\Validation\Validation; class ComplexRequest { // 使用管道符分隔多个规则 - #[Validation("required|string|min:3|max:50”)] + #[Validation('required|string|min:3|max:50')] public string $username; // 数组元素验证 - #[Validation("integer”, customKey: 'ids.*')] + #[Validation('integer', customKey: 'ids.*')] public array $ids; } ``` +> ⚠️ **注意**:字符串形式的规则按 `|` 拆分、按 `:` 提取参数,因此规则本身包含 `|` 或 `:` 时(如 `regex:/^(a|b)$/`、`date_format:H:i`)会被错误拆分。涉及正则的规则请使用 `Regex` 专用注解或数组形式。 + ### 自定义验证规则 继承 `BaseValidation` 类即可创建自定义验证规则: @@ -427,11 +469,11 @@ namespace App\Validation; use Attribute; use Hyperf\DTO\Annotation\Validation\BaseValidation; -#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] +#[Attribute(Attribute::TARGET_PROPERTY)] class Phone extends BaseValidation { - protected $rule = 'regex:/^1[3-9]\\d{9}$/'; - + protected mixed $rule = 'regex:/^1[3-9]\d{9}$/'; + public function __construct(string $messages = '手机号格式不正确') { parent::__construct($messages); @@ -443,6 +485,7 @@ class Phone extends BaseValidation ```php use App\Validation\Phone; +use Hyperf\DTO\Annotation\Validation\Required; class RegisterRequest { @@ -451,8 +494,94 @@ class RegisterRequest public string $mobile; } ``` + +## ⚙️ 配置 + +组件无需配置即可工作。如需定制,创建 `config/autoload/dto.php`(或 `api_docs.php`): + +```php + false, + + // DTO 代理文件生成目录 + 'proxy_dir' => BASE_PATH . '/runtime/container/proxy/', + + // 属性默认值级别: + // 0 - 不注入默认值(jsonSerialize 时用 ?? 默认值兜底,推荐) + // 1 - 为简单类型属性注入默认值(int=0、string=''、array=[]、bool=false) + // 2 - 在 1 的基础上,将类类型属性也改为可空并默认 null + 'dto_default_value_level' => 0, + + // 全局响应字段名转换(camel / studly / snake / none / custom) + 'responses_global_convert' => Convert::SNAKE, +]; +``` + +> ⚠️ 注意:`scan_cacheable` 读取的是**顶层**配置键。若使用独立的 `dto.php` 配置文件,请确保该键位于配置根级。 + ## 🔧 高级功能 +### 手动映射 + +脱离 HTTP 请求场景时,可直接使用 `Mapper` 静态门面: + +```php +use Hyperf\DTO\Mapper; + +// 数组/对象 → DTO +$user = Mapper::map(['name' => 'tom', 'age' => 20], new User()); + +// 数组 → DTO 数组 +$users = Mapper::mapArray($list, User::class); + +// 对象间属性复制(支持 Arrayable 模型) +Mapper::copyProperties($model, new UserResponse()); +``` + +### 响应字段名转换 + +#### 类级转换 + +```php +use Hyperf\DTO\Annotation\Dto; +use Hyperf\DTO\Type\Convert; + +#[Dto(responseConvert: Convert::SNAKE)] +class UserResponse +{ + public string $userName; // 序列化输出 user_name + public int $loginCount; // 序列化输出 login_count +} +``` + +#### 全局转换 + +在配置中设置 `responses_global_convert`(见上文「配置」章节),类级 `#[Dto]` 注解优先级更高。 + +#### 自定义转换 + +使用 `Convert::CUSTOM` 前需注册转换闭包(如在 `BootApplication` 监听器中): + +```php +use Hyperf\DTO\Type\ConvertCustom; + +ConvertCustom::setClosure(fn (string $name) => 'prefix_' . $name); +``` + +### 事件 + +组件在启动阶段会派发事件,可监听以扩展行为: + +| 事件 | 时机 | +|------|------| +| `Hyperf\DTO\Event\BeforeDtoStart` | 主要用于自动化测试中手动触发 DTO 扫描 | +| `Hyperf\DTO\Event\AfterDtoStart` | 每个 server 的路由扫描完成后派发,携带 server 配置与路由器 | + ### RPC 支持 在 JSON-RPC 服务中返回 PHP 对象,需要配置序列化支持。 @@ -460,8 +589,8 @@ class RegisterRequest #### 1. 安装依赖 ```bash -composer require symfony/serializer ^5.0|^6.0 -composer require symfony/property-access ^5.0|^6.0 +composer require symfony/serializer +composer require symfony/property-access ``` #### 2. 配置 Aspect @@ -479,47 +608,14 @@ return [ 在 `config/autoload/dependencies.php` 中添加: ```php -use Hyperf\Serializer\SerializerFactory; use Hyperf\Serializer\Serializer; +use Hyperf\Serializer\SerializerFactory; return [ Hyperf\Contract\NormalizerInterface::class => new SerializerFactory(Serializer::class), ]; ``` -### 自定义类型转换 - -如果需要自定义类型转换逻辑,可以实现自己的转换器: - -```php -namespace App\Convert; - -use Hyperf\DTO\Type\ConvertCustom; - -class CustomConvert implements ConvertCustom -{ - public function convert(mixed $value): mixed - { - // 自定义转换逻辑 - return $value; - } -} -``` - -在 DTO 类中使用: - -```php -use Hyperf\DTO\Annotation\Dto; -use Hyperf\DTO\Type\Convert; - -#[Dto(Convert::SNAKE)] -class UserResponse -{ - public string $name; - public int $age; -} -``` - ## 💡 最佳实践 ### 1. DTO 类结构设计 @@ -542,9 +638,9 @@ class UserResponse namespace App\Exception\Handler; use Hyperf\ExceptionHandler\ExceptionHandler; +use Hyperf\HttpMessage\Stream\SwooleStream; use Hyperf\Validation\ValidationException; use Psr\Http\Message\ResponseInterface; -use Hyperf\HttpMessage\Stream\SwooleStream; class ValidationExceptionHandler extends ExceptionHandler { @@ -570,6 +666,11 @@ class ValidationExceptionHandler extends ExceptionHandler } ``` +### 4. 生产环境部署 + +- 设置 `scan_cacheable = true`,在构建/发布阶段执行一次扫描生成代理文件(`runtime/container/proxy/*.dto.proxy.php`),运行时直接加载,减少启动开销 +- 代理文件按源文件修改时间自动过期重建(`scan_cacheable = false` 时) + ## 📚 常见问题 ### Q: 为什么验证没有生效? @@ -596,14 +697,22 @@ public array $users; ### Q: 可以同时使用 RequestBody 和 RequestFormData 吗? -A: 不可以。这两个注解是互斥的,因为它们处理不同的请求类型。 +A: 不可以。这两个注解是互斥的,因为它们处理不同的请求类型,启动扫描阶段会抛出 `DtoException`。 ### Q: 如何处理文件上传? A: 使用 `RequestFormData` 注解,然后通过 `$this->request->file()` 获取文件。 +### Q: 嵌套 DTO 的验证规则如何生效? + +A: 外层 DTO 验证通过后,组件会递归验证嵌套对象(含对象数组的每个元素),规则取嵌套类属性上的验证注解。嵌套字段为空时跳过递归验证,如需强制必填请在外层属性上加 `#[Required]`。 + +### Q: DTO 嵌套层级有限制吗? + +A: 有,最大嵌套深度为 100,防止循环引用导致无限递归,超限会抛出 `DtoException`。 + ## 🔗 相关链接 - [Hyperf 官方文档](https://hyperf.wiki) - [Hyperf Validation](https://hyperf.wiki/3.1/#/zh-cn/validation) -- [PHP Attributes](https://www.php.net/manual/zh/language.attributes.php) \ No newline at end of file +- [PHP Attributes](https://www.php.net/manual/zh/language.attributes.php) diff --git a/composer.json b/composer.json index 9b93c09..828e798 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ ], "require": { "php": ">=8.2", - "netresearch/jsonmapper": "~5.0.0", + "netresearch/jsonmapper": "~6.0.0", "hyperf/http-server": "~3.2.0", "hyperf/di": "~3.2.0", "hyperf/validation": "~3.2.0", diff --git a/src/DtoCommon.php b/src/DtoCommon.php index 55cfc7d..78013c7 100644 --- a/src/DtoCommon.php +++ b/src/DtoCommon.php @@ -25,22 +25,22 @@ public function getTypeName(ReflectionProperty|ReflectionParameter $rprop): stri return 'string'; } - public function isSimpleType($type) + public function isSimpleType(string $type): bool { return parent::isSimpleType($type); } - public function getFullNamespace($type, $strNs) + public function getFullNamespace(?string $type, string $strNs): ?string { return parent::getFullNamespace($type, $strNs); } - public function isArrayOfType($strType) + public function isArrayOfType(string $strType): bool { return parent::isArrayOfType($strType); } - public function getSafeName($name) + public function getSafeName(string $name): string { return parent::getSafeName($name); } diff --git a/src/JsonMapper.php b/src/JsonMapper.php index 96ac2ad..041d746 100644 --- a/src/JsonMapper.php +++ b/src/JsonMapper.php @@ -22,7 +22,7 @@ class JsonMapper extends \JsonMapper * @param string $message Text to log * @param array $context Additional information */ - protected function log($level, $message, array $context = []) + protected function log(string $level, string $message, array $context = []): void { if ($this->logger) { $this->logger->log('debug', $message, $context); @@ -42,9 +42,9 @@ protected function log($level, $message, array $context = []) * Third value: type of the property * Fourth value: if the property is nullable */ - protected function inspectProperty(ReflectionClass $rc, $name) + protected function inspectProperty(ReflectionClass $rc, string $name): array { - // 修改 + // 修改:别名 setter 方法优先 $isSetDtoMethod = true; $setter = DtoConfig::getDtoAliasMethodName($name); if (! $rc->hasMethod($setter)) { @@ -55,7 +55,7 @@ protected function inspectProperty(ReflectionClass $rc, $name) if ($rc->hasMethod($setter)) { $rmeth = $rc->getMethod($setter); - // 修改 + // 修改:DTO 别名 setter 为私有方法,同样允许访问 if ($rmeth->isPublic() || $this->bIgnoreVisibility || $isSetDtoMethod) { $isNullable = false; $rparams = $rmeth->getParameters(); @@ -77,7 +77,7 @@ protected function inspectProperty(ReflectionClass $rc, $name) } $docblock = $rmeth->getDocComment(); - $annotations = static::parseAnnotations($docblock); + $annotations = static::parseAnnotations((string) $docblock); if (!isset($annotations['param'][0])) { return array(true, $rmeth, null, $isNullable); @@ -110,20 +110,18 @@ protected function inspectProperty(ReflectionClass $rc, $name) if ($rprop !== null) { if ($rprop->isPublic() || $this->bIgnoreVisibility) { $docblock = $rprop->getDocComment(); - if (PHP_VERSION_ID >= 80000 && $docblock === false - && $class->hasMethod('__construct') - ) { + if ($docblock === false && $class->hasMethod('__construct')) { $docblock = $class->getMethod('__construct')->getDocComment(); } - // 修改 + // 修改:优先读取 ArrayType 注解,并使用 DocBlockFactory 解析命名空间 $annotations = $this->parseAnnotationsNew($rc, $rprop, $docblock); if (!isset($annotations['var'][0])) { - if (PHP_VERSION_ID >= 80000 && $rprop->hasType() - && isset($annotations['param']) - ) { + if ($rprop->hasType() && isset($annotations['param'])) { foreach ($annotations['param'] as $param) { - if (strpos($param, '$' . $rprop->getName()) !== false) { + if (strpos($param . ' ', '$' . $rprop->getName() . ' ') !== false + || strpos($param . "\t", '$' . $rprop->getName() . "\t") !== false + ) { list($type) = explode(' ', $param); return array( true, $rprop, $type, $this->isNullable($type) @@ -134,7 +132,7 @@ protected function inspectProperty(ReflectionClass $rc, $name) // If there is no annotations (higher priority) inspect // if there's a scalar type being defined - if (PHP_VERSION_ID >= 70400 && $rprop->hasType()) { + if ($rprop->hasType()) { $rPropType = $rprop->getType(); $propTypeName = $this->stringifyReflectionType($rPropType); if ($this->isSimpleType($propTypeName)) { @@ -172,13 +170,13 @@ protected function inspectProperty(ReflectionClass $rc, $name) } /** - * Copied from PHPUnit 3.7.29, Util/Test.php. + * 解析属性的类型注解,优先使用 ArrayType 注解,其次解析 @var 标签. * - * @param false|string $docblock Full method docblock + * @param false|string $docblock 属性或构造方法的 docblock * * @return array Array of arrays. * Key is the "@"-name like "param", - * each value is an array of the rest of the @-lines + * each value is an array of the @-lines */ public function parseAnnotationsNew(ReflectionClass $rc, ReflectionProperty $reflectionProperty, $docblock): array { diff --git a/src/Scan/MethodParametersManager.php b/src/Scan/MethodParametersManager.php index 1245ce6..041e711 100644 --- a/src/Scan/MethodParametersManager.php +++ b/src/Scan/MethodParametersManager.php @@ -133,7 +133,7 @@ public function scanClassMethodParam(string $className, string $methodName): voi $varType = $annotations[$paramName] ?? null; $varType = $this->dtoCommon->getFullNamespace($varType, $strNs); // 数组类型 - if ($this->dtoCommon->isArrayOfType($varType)) { + if ($varType !== null && $this->dtoCommon->isArrayOfType($varType)) { $isSimpleType = false; $arrType = substr($varType, 0, -2); // 数组的简单类型 eg: int[] string[]