From 9876456b8923de5e0418054c186deb43f8ec49f0 Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 23 Jul 2026 11:01:54 +0800 Subject: [PATCH 1/3] =?UTF-8?q?docs(api):=20=E6=B7=BB=E5=8A=A0=20CLAUDE.md?= =?UTF-8?q?=20=E5=BC=80=E5=8F=91=E6=8C=87=E5=8D=97=E5=B9=B6=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加详细的项目架构核心说明 - 更新环境要求至 PHP 8.2 和 Hyperf ~3.2 - 升级 swagger-ui 版本至 5.27.1 - 修正 knife4j 访问路径说明 - 更新 hyperf.wiki 链接至 3.2 版本 feat(swagger): 支持路径参数识别和循环引用处理 - 新增路由路径参数检测功能,仅将路径占位符参数生成为 path 类型 - 实现循环引用类的防无限递归处理机制 - 为非 DTO 扫描类属性提供反射类型兜底方案 test(swagger): 添加参数生成和响应覆盖测试用例 - 创建 GenerateParametersTest 验证路径参数识别 - 添加 GenerateResponsesTest 测试注解优先级覆盖 - 实现循环引用 DTO 测试用例 refactor(swagger): 优化组件初始化和数据合并顺序 - 调整 SwaggerComponents 类初始化顺序防止递归问题 - 修正 ApiResponse 注解与全局配置合并优先级 - 更新参数生成器构造函数注入路由信息 --- CLAUDE.md | 61 +++++++++++++++++++ README.md | 14 ++--- README_EN.md | 20 +++--- src/Swagger/GenerateParameters.php | 12 ++++ src/Swagger/GenerateResponses.php | 3 +- src/Swagger/SwaggerComponents.php | 21 ++++++- src/Swagger/SwaggerPaths.php | 2 +- tests/GenerateParametersTest.php | 90 +++++++++++++++++++++++++++ tests/GenerateResponsesTest.php | 98 ++++++++++++++++++++++++++++++ tests/Request/TreeNode.php | 19 ++++++ tests/Request/TreeSibling.php | 16 +++++ tests/SwaggerSchemasTest.php | 29 +++++++++ 12 files changed, 366 insertions(+), 19 deletions(-) create mode 100644 CLAUDE.md create mode 100644 tests/GenerateParametersTest.php create mode 100644 tests/GenerateResponsesTest.php create mode 100644 tests/Request/TreeNode.php create mode 100644 tests/Request/TreeSibling.php diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..83c905d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,61 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## 项目概述 + +`tangwei/apidocs` — 基于 Hyperf 的 Swagger/OpenAPI 3.x 文档自动生成组件。通过 PHP 8 Attributes 扫描控制器路由,在应用启动时生成 OpenAPI 描述文件,并内置多种文档 UI(Swagger UI、Knife4j、Redoc、RapiDoc、Scalar)及 llms.txt 输出。支持 Swoole / Swow / phar 部署。 + +## 常用命令 + +```bash +composer test # 运行全部测试(phpunit -c phpunit.xml) +vendor/bin/phpunit -c phpunit.xml --filter testMethodName tests/SwaggerPathsTest.php # 运行单个测试 +composer analyse # PHPStan 静态分析(-l 0,仅 src/) +composer cs-fix # php-cs-fixer 格式化 src 和 tests +``` + +CI 矩阵为 PHP 8.2/8.3/8.4 + Hyperf 3.2(pin `hyperf/di:3.2.*` + `tangwei/dto:dev-master`)。本包通过 `tangwei/dto ~3.2` 传递依赖 Hyperf ~3.2,不兼容 Hyperf 3.1。 + +## 架构核心 + +### 启动期生成流水线(理解本组件的关键) + +OpenAPI 文件**不是请求时生成的**,而是在应用启动时由事件监听器驱动: + +1. `BootAppRouteListener`(BootApplication 事件)— 在第一个 HTTP server 的路由上注册 `{prefix_url}` 路由组(UI 页面、`/webjars/*`、`{httpName}.json/yaml`、llms.txt 等),并把文档访问 URL 写入静态属性供 `AfterWorkerStartListener` 打印。 +2. `AfterDtoStartListener`(`Hyperf\DTO\Event\AfterDtoStart` 事件,由 tangwei/dto 在扫描完路由后发出)— **每个 server 触发一次**:遍历该 server 的全部路由 Handler,对每个 `控制器@方法` 调 `SwaggerPaths::addPath()` 解析注解生成 `OA\PathItem`,最后 `SwaggerOpenApi::save()` 写入 `output_dir/{serverName}.json|yaml`。 +3. `SwaggerOpenApi` 是**按 server 累积状态**的构建器:`init(serverName)` 重置 → 各 Generate 类向其 SplPriorityQueue(paths/tags 按 position 排序)投递 → `save()` 落盘 → `clean()` 释放。多 server 应用会为每个 server 各生成一份文件。 + +**注意**:`dtoConfig->isScanCacheable()` 为 true 时 `AfterDtoStartListener` 跳过生成(第 56-58 行提前 return)——扫描缓存模式下运行环境可能没有 output_dir 中的文件。 + +### 注解 → OpenAPI 的转换链 + +- `SwaggerPaths::addPath()` 读取类/方法注解(`#[Api]`、`#[ApiOperation]`、`#[ApiHeader]`、`#[ApiResponse]`、`#[ApiFormData]`、`#[ApiSecurity]`),委托给: + - `GenerateParameters` — 从方法签名 + DTO 类生成 parameters/requestBody + - `GenerateResponses` — 从方法**返回类型**(`MethodDefinitionCollector`)+ `#[ApiResponse]` + 全局 `GlobalResponse` 配置生成 responses;控制器方法返回具体类才能获得准确文档 + - `SwaggerComponents` — DTO 类的 `#[ApiModelProperty]`/验证注解 → `components.schemas`,继承自 tangwei/dto 的 `PropertyManager` +- `SwaggerConfig` 用 JsonMapper(`bIgnoreVisibility`)把 `config/autoload/api_docs.php` 直接映射到私有属性——**配置键名必须与属性名一致**(snake_case),新增配置项 = 新增同名私有属性。 + +### ApiVariable 代理类机制 + +`#[ApiVariable]` 标记的 DTO 属性(类型在运行时才能确定的"可变类型")由 `GenerateProxyClass` 在运行时通过 PHP-Parser 重写原类 AST(`Ast\ResponseVisitor` 替换属性类型和命名空间为 `ApiDocs\Proxy`),写入 `proxy_dir`(默认 `runtime/container/proxy/`)供 schema 生成使用。 + +### 文件服务端点 + +`SwaggerController`(json/yaml/md/静态文件)和 `SwaggerUiController`(各 UI 页面 + knife4j webjars)按请求实例化。静态资源路径硬编码指向 `vendor/tangwei/swagger-ui/dist` 和 `vendor/tangwei/knife4j-ui/dist`(knife4j-ui 是 suggest 依赖,未安装时相关路由会 500)。三类端点校验方式不同:`getFile` 用 scandir 白名单精确匹配,`knife4jFile` 用 sanitize + realpath 前缀校验(嵌套路径无法白名单)。`fileResponse` 在 Swoole 下用 `SwooleFileStream`(sendfile),Swow/phar 下退回 `file_get_contents`。 + +### 与 tangwei/dto 的关系 + +本组件重度依赖 `tangwei/dto`(`Hyperf\DTO\*`):注解扫描(`ApiAnnotation::classMetadata`)、DTO 验证、属性管理、Mapper 均来自该包。修改扫描/注解相关行为时,先确认逻辑在本包还是 dto 包。 + +## 测试约定 + +- 测试基类 `SwaggerUiControllerTestable` 重写了构造函数且**不调 `parent::__construct`**——父类构造函数的逻辑(目录检查、scandir)在测试中不会被覆盖到。 +- `tests/Request/` 下的 DTO 是多个测试共用的 fixture。 +- CI 在 hyperf/hyperf 容器镜像中运行,本地无 Swoole 也可跑 phpunit(测试不依赖 server 启动)。 + +## 示例与文档 + +- `example/` 目录是注解用法的活文档(各参数注解、分页、枚举、递归类型的完整示例),改注解行为时对照它验证。 +- README.md / README_EN.md 需保持同步;环境要求以 composer.json 为准(README 中的版本号容易滞后)。 diff --git a/README.md b/README.md index c2eb952..97fbfe2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Latest Stable Version](https://img.shields.io/packagist/v/tangwei/apidocs)](https://packagist.org/packages/tangwei/apidocs) [![Total Downloads](https://img.shields.io/packagist/dt/tangwei/apidocs)](https://packagist.org/packages/tangwei/apidocs) [![License](https://img.shields.io/packagist/l/tangwei/apidocs)](https://github.com/tw2066/api-docs) -[![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) | 中文 @@ -22,8 +22,8 @@ ## 📋 环境要求 -- PHP >= 8.1 -- Hyperf >= 3.0 +- PHP >= 8.2 +- Hyperf ~3.2 - Swoole >= 5.0 或 Swow ## 💡 使用须知 @@ -139,7 +139,7 @@ return [ | 设置swagger资源路径,cdn资源 |-------------------------------------------------------------------------- */ - 'prefix_swagger_resources' => 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.5.0', + 'prefix_swagger_resources' => 'https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.27.1', /* |-------------------------------------------------------------------------- @@ -719,7 +719,7 @@ public function upload(#[RequestFormData] UploadRequest $request) 访问不同的 UI 界面: - **Swagger UI**: `http://your-host:9501/swagger` -- **Knife4j**: `http://your-host:9501/swagger/knife4j` +- **Knife4j**: `http://your-host:9501/swagger/doc`(需安装 `tangwei/knife4j-ui`) - **Redoc**: `http://your-host:9501/swagger/redoc` - **RapiDoc**: `http://your-host:9501/swagger/rapidoc` - **Scalar**: `http://your-host:9501/swagger/scalar` @@ -769,7 +769,7 @@ class DemoQuery ### RPC 支持 -[返回 PHP 对象](https://hyperf.wiki/3.1/#/zh-cn/json-rpc?id=%e8%bf%94%e5%9b%9e-php-%e5%af%b9%e8%b1%a1) +[返回 PHP 对象](https://hyperf.wiki/3.2/#/zh-cn/json-rpc?id=%e8%bf%94%e5%9b%9e-php-%e5%af%b9%e8%b1%a1) aspects.php 中配置: @@ -865,7 +865,7 @@ public function getUser(): UserResponse ### Q: 支持哪些验证规则? -A: 支持所有 Hyperf Validation 规则。详见 [Hyperf 验证器文档](https://hyperf.wiki/3.1/#/zh-cn/validation)。 +A: 支持所有 Hyperf Validation 规则。详见 [Hyperf 验证器文档](https://hyperf.wiki/3.2/#/zh-cn/validation)。 ### Q: `AutoController` 注解支持吗? diff --git a/README_EN.md b/README_EN.md index 8454f62..1210583 100644 --- a/README_EN.md +++ b/README_EN.md @@ -3,7 +3,7 @@ [![Latest Stable Version](https://img.shields.io/packagist/v/tangwei/apidocs)](https://packagist.org/packages/tangwei/apidocs) [![Total Downloads](https://img.shields.io/packagist/dt/tangwei/apidocs)](https://packagist.org/packages/tangwei/apidocs) [![License](https://img.shields.io/packagist/l/tangwei/apidocs)](https://github.com/tw2066/api-docs) -[![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.md) @@ -11,7 +11,7 @@ Automatic Swagger/OpenAPI documentation generator for the [Hyperf](https://githu ## ✨ Features -- 🚀 **Auto Generation** - Automatically generate OpenAPI 3.0 documentation based on PHP 8 Attributes +- 🚀 **Auto Generation** - Automatically generate OpenAPI 3.0/3.1 documentation based on PHP 8 Attributes - 🎯 **Type Safety** - Support DTO mode with automatic parameter mapping to PHP classes - 📝 **Multiple UIs** - Support Swagger UI, Knife4j, Redoc, RapiDoc, Scalar, and more - ✅ **Data Validation** - Integrate Hyperf validator with rich validation annotations @@ -22,8 +22,8 @@ Automatic Swagger/OpenAPI documentation generator for the [Hyperf](https://githu ## 📋 Requirements -- PHP >= 8.1 -- Hyperf >= 3.0 +- PHP >= 8.2 +- Hyperf ~3.2 - Swoole >= 5.0 or Swow ## 💡 Important Notes @@ -237,14 +237,16 @@ return [ php bin/hyperf.php start ``` -After successful startup, visit `http://your-host:9501/swagger` to view the API documentation. - ``` [INFO] Swagger docs url at http://0.0.0.0:9501/swagger [INFO] Worker#0 started. [INFO] HTTP Server listening at 0.0.0.0:9501 ``` +- After successful startup, visit `http://your-host:9501/swagger` to view the API documentation. +- Visit `http://your-host:9501/swagger/llms.txt` for links to a Markdown page per controller, which can be used by AI to quickly access the API documentation. +- Other servers can visit `http://your-host:9501/swagger/{service-name}.md` to access the Markdown documentation of the `{service-name}` server. + ## 📖 Usage Guide ### Basic Example @@ -848,7 +850,7 @@ public function upload(#[RequestFormData] UploadRequest $request) Access different UI interfaces: - **Swagger UI**: `http://your-host:9501/swagger` -- **Knife4j**: `http://your-host:9501/swagger/knife4j` +- **Knife4j**: `http://your-host:9501/swagger/doc` (requires `tangwei/knife4j-ui`) - **Redoc**: `http://your-host:9501/swagger/redoc` - **RapiDoc**: `http://your-host:9501/swagger/rapidoc` - **Scalar**: `http://your-host:9501/swagger/scalar` @@ -898,7 +900,7 @@ class DemoQuery ### RPC Support -[Return PHP Object](https://hyperf.wiki/3.1/#/en/json-rpc?id=returning-php-objects) +[Return PHP Object](https://hyperf.wiki/3.2/#/en/json-rpc?id=returning-php-objects) Configure in aspects.php: @@ -996,7 +998,7 @@ public function getUser(): UserResponse ### Q: What validation rules are supported? -A: All Hyperf Validation rules are supported. See [Hyperf Validation Documentation](https://hyperf.wiki/3.1/#/en/validation). +A: All Hyperf Validation rules are supported. See [Hyperf Validation Documentation](https://hyperf.wiki/3.2/#/en/validation). ### Q: Does `AutoController` annotation work? diff --git a/src/Swagger/GenerateParameters.php b/src/Swagger/GenerateParameters.php index a7014a5..8b036ff 100644 --- a/src/Swagger/GenerateParameters.php +++ b/src/Swagger/GenerateParameters.php @@ -32,6 +32,7 @@ public function __construct( protected string $action, protected array $apiHeaderArr, protected array $apiFormDataArr, + protected string $route, protected ContainerInterface $container, protected MethodDefinitionCollectorInterface $methodDefinitionCollector, protected SwaggerComponents $swaggerComponents, @@ -57,6 +58,9 @@ public function generate(): array // 判断是否为简单类型 $simpleSwaggerType = $this->common->getSimpleType2SwaggerType($parameterClassName); if ($simpleSwaggerType !== null) { + if (! $this->isPathParam($paramName)) { + continue; + } $parameter = new OA\Parameter(); $parameter->required = true; $parameter->name = $paramName; @@ -270,4 +274,12 @@ protected function getPropertiesByBaseParam(array $baseParam): array } return ['propertyArr' => $propertyArr, 'requiredArr' => $requiredArr]; } + + /** + * 判断参数是否为路由路径占位符(支持 {id} 和 {id:\d+} 形式). + */ + protected function isPathParam(string $paramName): bool + { + return preg_match('/\{' . preg_quote($paramName, '/') . '(:[^}]*)?\}/', $this->route) === 1; + } } diff --git a/src/Swagger/GenerateResponses.php b/src/Swagger/GenerateResponses.php index fa0fc1b..42c5eec 100644 --- a/src/Swagger/GenerateResponses.php +++ b/src/Swagger/GenerateResponses.php @@ -50,8 +50,9 @@ public function generate(): array $content && $response->content = $content; $arr[$code] = $response; - $annotationResp && $arr = Arr::merge($arr, $annotationResp); + // 优先级:方法级 ApiResponse 注解 > 全局 responses 配置 $globalResp && $arr = Arr::merge($arr, $globalResp); + $annotationResp && $arr = Arr::merge($arr, $annotationResp); return array_values($arr); } diff --git a/src/Swagger/SwaggerComponents.php b/src/Swagger/SwaggerComponents.php index 39772da..c6d2464 100644 --- a/src/Swagger/SwaggerComponents.php +++ b/src/Swagger/SwaggerComponents.php @@ -13,6 +13,7 @@ use Hyperf\DTO\Annotation\Validation\Required; use Hyperf\DTO\ApiAnnotation; use Hyperf\DTO\DtoConfig; +use Hyperf\DTO\Scan\Property; use Hyperf\DTO\Scan\PropertyManager; use OpenApi\Attributes as OA; use OpenApi\Generator; @@ -55,6 +56,10 @@ public function getProperties(string $className): array $property = new OA\Property(); $fieldName = $reflectionProperty->getName(); $propertyManager = $this->propertyManager->getProperty($className, $fieldName); + if ($propertyManager === null) { + // 属性未被 DTO 扫描器登记(如代理类),按反射类型兜底 + $propertyManager = $this->buildPropertyFromReflection($reflectionProperty); + } // 适配ApiVariable注解 $sourceClassName = $this->generateProxyClass?->getSourceClassname($className) ?? $className; @@ -151,6 +156,8 @@ public function generateSchemas(string $className) } $schema = new OA\Schema(); $schema->schema = $simpleClassName; + // 先登记再解析属性,防止循环引用类(A ↔ B)导致无限递归 + $this->schemas[$simpleClassName] = $schema; $data = $this->getProperties($className); $schema->properties = $data['propertyArr']; @@ -160,7 +167,19 @@ public function generateSchemas(string $className) $schema->description = $apiModel->value; } $data['requiredArr'] && $schema->required = $data['requiredArr']; - $this->schemas[$simpleClassName] = $schema; return $this->schemas[$simpleClassName]; } + + protected function buildPropertyFromReflection(\ReflectionProperty $reflectionProperty): Property + { + $property = new Property(); + $phpType = $this->common->getTypeName($reflectionProperty); + if ($this->common->isSimpleType($phpType)) { + $property->phpSimpleType = $phpType; + } else { + $property->isSimpleType = false; + $property->className = $phpType; + } + return $property; + } } diff --git a/src/Swagger/SwaggerPaths.php b/src/Swagger/SwaggerPaths.php index 6807ab5..09e5369 100644 --- a/src/Swagger/SwaggerPaths.php +++ b/src/Swagger/SwaggerPaths.php @@ -92,7 +92,7 @@ public function addPath(string $className, string $methodName, string $route, st $method = strtolower($methods); /** @var GenerateParameters $generateParameters */ - $generateParameters = make(GenerateParameters::class, [$className, $methodName, $apiHeaderArr, $apiFormDataArr]); + $generateParameters = make(GenerateParameters::class, [$className, $methodName, $apiHeaderArr, $apiFormDataArr, $route]); /** @var GenerateResponses $generateResponses */ $generateResponses = make(GenerateResponses::class, [$className, $methodName, $apiResponseArr]); $parameters = $generateParameters->generate(); diff --git a/tests/GenerateParametersTest.php b/tests/GenerateParametersTest.php new file mode 100644 index 0000000..f0f9280 --- /dev/null +++ b/tests/GenerateParametersTest.php @@ -0,0 +1,90 @@ +generate('/user/{id}', [ + $this->reflectionType('int', 'id'), + ]); + + $this->assertCount(1, $result['parameter']); + $this->assertSame('path', $result['parameter'][0]->in); + $this->assertTrue($result['parameter'][0]->required); + $this->assertSame('id', $result['parameter'][0]->name); + } + + /** + * 带正则约束的占位符 {id:\d+} 也应识别为 path 参数. + */ + public function testPathParamWithRegexConstraint(): void + { + $result = $this->generate('/user/{id:\d+}', [ + $this->reflectionType('int', 'id'), + ]); + + $this->assertSame('path', $result['parameter'][0]->in); + $this->assertTrue($result['parameter'][0]->required); + } + + private function reflectionType(string $type, string $name, bool $allowsNull = false, bool $defaultValueAvailable = false): ReflectionType + { + return new ReflectionType($type, $allowsNull, [ + 'defaultValueAvailable' => $defaultValueAvailable, + 'defaultValue' => null, + 'name' => $name, + 'attributes' => [], + ]); + } + + private function generate(string $route, array $definitions): array + { + $swaggerCommon = new SwaggerCommon(); + $container = m::mock(ContainerInterface::class); + $methodDefinitionCollector = m::mock(MethodDefinitionCollectorInterface::class); + $methodDefinitionCollector->shouldReceive('getParameters')->andReturn($definitions); + + $generateParameters = new GenerateParameters( + 'DemoController', + 'list', + [], + [], + $route, + $container, + $methodDefinitionCollector, + new SwaggerComponents($swaggerCommon, new PropertyManager($swaggerCommon, new PropertyEnum()), null), + $swaggerCommon, + new PropertyManager($swaggerCommon, new PropertyEnum()), + m::mock(MethodParametersManager::class), + ); + return $generateParameters->generate(); + } +} diff --git a/tests/GenerateResponsesTest.php b/tests/GenerateResponsesTest.php new file mode 100644 index 0000000..ae770d1 --- /dev/null +++ b/tests/GenerateResponsesTest.php @@ -0,0 +1,98 @@ +shouldReceive('getResponsesCode')->andReturn('200'); + $swaggerConfig->shouldReceive('getGlobalReturnResponsesClass')->andReturn(''); + $swaggerConfig->shouldReceive('getResponses')->andReturn([ + ['response' => 401, 'description' => 'Global Unauthorized'], + ]); + + $apiResponse = new ApiResponse(null, 401, 'Annotation Unauthorized'); + + $generateResponses = $this->makeGenerateResponses($swaggerConfig, [$apiResponse]); + $responses = $generateResponses->generate(); + + $resp401 = null; + foreach ($responses as $response) { + if ((int) $response->response === 401) { + $resp401 = $response; + } + } + $this->assertNotNull($resp401); + $this->assertSame('Annotation Unauthorized', $resp401->description); + } + + /** + * 注解未覆盖的状态码仍使用全局配置. + */ + public function testGlobalResponseKeptWhenNotOverridden(): void + { + $swaggerConfig = m::mock(SwaggerConfig::class); + $swaggerConfig->shouldReceive('getResponsesCode')->andReturn('200'); + $swaggerConfig->shouldReceive('getGlobalReturnResponsesClass')->andReturn(''); + $swaggerConfig->shouldReceive('getResponses')->andReturn([ + ['response' => 500, 'description' => 'Global System Error'], + ]); + + $generateResponses = $this->makeGenerateResponses($swaggerConfig, []); + $responses = $generateResponses->generate(); + + $descriptions = array_map(fn ($r) => $r->description, $responses); + $this->assertContains('Global System Error', $descriptions); + } + + private function makeGenerateResponses(SwaggerConfig $swaggerConfig, array $apiResponseArr): GenerateResponses + { + $container = m::mock(ContainerInterface::class); + $container->shouldReceive('has')->andReturn(false); + $container->shouldReceive('get')->with(MethodDefinitionCollectorInterface::class)->andReturn(new MethodDefinitionCollector()); + + $swaggerCommon = new SwaggerCommon(); + return new GenerateResponses( + DemoBodyRequest::class, + 'getBo', + $apiResponseArr, + $swaggerConfig, + new MethodDefinitionCollector(), + $container, + new SwaggerComponents($swaggerCommon, new PropertyManager($swaggerCommon, new PropertyEnum()), null), + $swaggerCommon, + m::mock(GenerateProxyClass::class), + ); + } +} diff --git a/tests/Request/TreeNode.php b/tests/Request/TreeNode.php new file mode 100644 index 0000000..5e8fc1f --- /dev/null +++ b/tests/Request/TreeNode.php @@ -0,0 +1,19 @@ +assertContains('name', $propertyNames); $this->assertContains('age', $propertyNames); } + + /** + * 自引用与互相引用的 DTO 不会导致 generateSchemas 无限递归. + */ + public function testCircularReferenceSchemas(): void + { + $swaggerCommon = new SwaggerCommon(); + $swaggerComponents = new SwaggerComponents($swaggerCommon, new PropertyManager($swaggerCommon, new PropertyEnum()), null); + + $schema = $swaggerComponents->generateSchemas(TreeNode::class); + $properties = $schema->properties; + + $propertyMap = []; + foreach ($properties as $property) { + $propertyMap[$property->property] = $property; + } + // 自引用 + $this->assertSame('#/components/schemas/TreeNode', $propertyMap['child']->ref); + // 互相引用 A → B,B 的 schema 也应生成且 B → A 正常回指 + $this->assertSame('#/components/schemas/TreeSibling', $propertyMap['sibling']->ref); + $schemas = $swaggerComponents->getSchemas(); + $this->assertArrayHasKey('TreeNode', $schemas); + $this->assertArrayHasKey('TreeSibling', $schemas); + $siblingProperties = $schemas['TreeSibling']->properties; + $this->assertSame('#/components/schemas/TreeNode', $siblingProperties[1]->ref); + + $swaggerCommon->simpleClassNameClear(); + } } From ee8e45734499e51bb2f4309ed059be9ca540ee45 Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 23 Jul 2026 13:40:56 +0800 Subject: [PATCH 2/3] =?UTF-8?q?chore(ci):=20=E6=9B=B4=E6=96=B0=20composer?= =?UTF-8?q?=20=E5=91=BD=E4=BB=A4=E5=8F=82=E6=95=B0=E4=BB=A5=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E4=BE=9D=E8=B5=96=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 composer update 命令从 -o 参数更改为 -oW 参数 - 提升依赖更新过程的严格性和一致性 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4163805..cc6ec9e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: php .github/workflows/add_composer_stability.php composer require hyperf/di:3.2.* composer require tangwei/dto:dev-master - composer update -o + composer update -oW composer info - name: Static Analysis From ce526e79b83ac4dbe563ec0387709949c84172b6 Mon Sep 17 00:00:00 2001 From: tangwei Date: Thu, 23 Jul 2026 13:44:49 +0800 Subject: [PATCH 3/3] =?UTF-8?q?chore(deps):=20=E6=9B=B4=E6=96=B0=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=AE=89=E8=A3=85=E5=91=BD=E4=BB=A4=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 composer require 的 -W 参数避免重复约束 - 移除 composer update 的 -W 参数优化执行效率 - 保持依赖安装的一致性配置 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cc6ec9e..51ed723 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,8 +31,8 @@ jobs: run: | php .github/workflows/add_composer_stability.php composer require hyperf/di:3.2.* - composer require tangwei/dto:dev-master - composer update -oW + composer require tangwei/dto:dev-master -W + composer update -o composer info - name: Static Analysis