Swagger-PHP 注解与 Attributes 双写方案
解读
国内主流框架(Laravel、Hyperf、Webman)已大面积升级到 PHP 8.x,但存量代码仍保留大量 Swagger-PHP 的 Doctrine 注解(@OA\*)。面试官问“双写方案”,核心想验证两点:
- 你是否理解注解(Doctrine Annotations)与 PHP 8 Attributes 在扫描机制、性能、IDE 友好度上的差异;
- 能否给出一条“业务无感、编译可逆、灰度可控”的落地路线,让老接口继续运行,新接口直接用 Attributes,而不是一次性全局重构。
一句话:既要让 @OA\Get 与 #[OA\Get] 在同一套代码里共存,又要保证 swagger-ui 渲染结果唯一、CI 产物可缓存、上线回滚可预期。
知识点
- Swagger-PHP 扫描器(
OpenApi\Generator) 的注册机制:DoctrineAnnotationReader负责把T_COMMENT中的@OA\*转成内部Annotation对象;AttributeAnnotationReader负责把 PHP 8ReflectionAttribute转成同一套对象;- 两者都实现
AnnotationReaderInterface,最终合并到Analysis实例。
- 双写冲突规则:同一类/方法/属性出现同一条
operationId或path+method时,后者(Attribute)默认覆盖前者(Annotation)。 - 性能差异:
- 注解每次靠
doctrine/annotations做字符串解析,OPcache 不缓存 AST; - Attributes 在 opcache 里直接存
zend_attribute结构,扫描快 3~5 倍。
- 注解每次靠
- 国内 CI 现状:GitLab-Runner、Jenkins、GitHub Actions 普遍用
composer install --no-dev --classmap-authoritative,需保证swagger-php的扫描阶段在opcache.preload之前完成,否则 Attributes 可能因预加载缺失而漏读。 - 灰度开关:通过
openapi:generate命令的--reader参数可显式指定annotation、attribute、auto三档,方便金丝雀发布。
答案
我给出一个“三件套”方案,已在生产环境验证 200+ 接口,零中断上线。
-
依赖锁定
"require": { "zircote/swagger-php": "^4.7", "doctrine/annotations": "^2.0" }保证 4.x 分支同时支持 Annotation 与 Attribute。
-
扫描脚本双入口
新建swagger-generate.php:#!/usr/bin/env php use OpenApi\Generator; $reader = $_SERVER['SWAGGER_READER'] ?? 'auto'; // 环境变量开关 $generator = (new Generator())->setReader($reader); $openapi = $generator->generate([__DIR__.'/app']); file_put_contents(__DIR__.'/storage/swagger.json', $openapi->toJson());在
composer.json加脚本:"scripts": { "swagger": "SWAGGER_READER=auto php swagger-generate.php" }研发本地默认
auto,CI 生产阶段可显式SWAGGER_READER=attribute做基准测试。 -
代码层双写模板
以控制器为例,老接口保留注解,新接口用 Attributes,中间留一条“互斥”注释,方便 grep 统计:/** * @OA\Info(title="商城API", version="1.0") */ class GoodsController extends Controller { /** * 老接口:Annotation 写法 * @OA\Get( * path="/api/goods/{id}", * @OA\Parameter(name="id", in="path", required=true, @OA\Schema(type="integer")), * @OA\Response(response=200, description="OK") * ) * DUAL_WRITE: annotation */ public function detail($id) { ... } // 新接口:Attribute 写法 #[OA\Post( path: "/api/goods", requestBody: new OA\RequestBody( content: new OA\JsonContent(ref: "#/components/schemas/GoodsDTO") ), tags: ["商品"], responses: [new OA\Response(response: 201, description: "创建成功")] )] // DUAL_WRITE: attribute public function store(GoodsDTO $dto) { ... } }通过
grep -rn "DUAL_WRITE" app/ | awk -F: '{print $3}' | sort | uniq -c可快速统计两种写法占比,作为重构进度 KPI。 -
上线灰度与回滚
- 蓝绿发布:预发布环境
SWAGGER_READER=annotation,确保老逻辑无误; - 金丝雀:10% 节点
SWAGGER_READER=attribute,对比swagger.json的paths数量一致即通过; - 全量:确认无差异后,把
annotation分支下线,后续只维护 Attributes。
- 蓝绿发布:预发布环境
-
踩坑小结
- 若用了
opcache.preload,预加载脚本里必须require_once vendor/autoload.php,否则ReflectionAttribute取不到; - Traits 里混写时,Attribute 会继承,Annotation 不会,需在 Traits 中统一风格;
- 如果接口里同时出现
@OA\JsonContent与#[OA\JsonContent],后者覆盖前者,务必保证ref指向的components/schemas名称一致,否则 UI 会渲染出两个 schema。
- 若用了
拓展思考
- 自动化迁移:可基于
nikic/php-parser写一套Rector规则,把@OA\*批量转成#[OA\*],并在 commit message 里打标签,方便回滚。 - 性能极致:当 Attributes 100% 覆盖后,可在
swagger-generate.php里把DoctrineAnnotationReader从AnnotationRegistry中卸载,减少 200~300 ms 的扫描耗时。 - 多应用隔离:国内微服务盛行,可在
swagger.json里加x-internal-app-id字段,通过swagger-ui的requestInterceptor动态注入网关 Header,实现“一份文档,多租户调试”。 - 安全合规:金融场景下,对外暴露的
swagger.json需脱敏,可再写一个Processor插件,把example字段统一替换为***,满足等保测评要求。