Swagger-PHP 注解与 Attributes 双写方案

解读

国内主流框架(Laravel、Hyperf、Webman)已大面积升级到 PHP 8.x,但存量代码仍保留大量 Swagger-PHP 的 Doctrine 注解(@OA\*)。面试官问“双写方案”,核心想验证两点:

  1. 你是否理解注解(Doctrine Annotations)与 PHP 8 Attributes 在扫描机制、性能、IDE 友好度上的差异;
  2. 能否给出一条“业务无感、编译可逆、灰度可控”的落地路线,让老接口继续运行,新接口直接用 Attributes,而不是一次性全局重构。

一句话:既要让 @OA\Get#[OA\Get] 在同一套代码里共存,又要保证 swagger-ui 渲染结果唯一、CI 产物可缓存、上线回滚可预期。

知识点

  1. Swagger-PHP 扫描器(OpenApi\Generator) 的注册机制:
    • DoctrineAnnotationReader 负责把 T_COMMENT 中的 @OA\* 转成内部 Annotation 对象;
    • AttributeAnnotationReader 负责把 PHP 8 ReflectionAttribute 转成同一套对象;
    • 两者都实现 AnnotationReaderInterface,最终合并到 Analysis 实例。
  2. 双写冲突规则:同一类/方法/属性出现同一条 operationIdpath+method 时,后者(Attribute)默认覆盖前者(Annotation)。
  3. 性能差异:
    • 注解每次靠 doctrine/annotations 做字符串解析,OPcache 不缓存 AST;
    • Attributes 在 opcache 里直接存 zend_attribute 结构,扫描快 3~5 倍。
  4. 国内 CI 现状:GitLab-Runner、Jenkins、GitHub Actions 普遍用 composer install --no-dev --classmap-authoritative,需保证 swagger-php 的扫描阶段在 opcache.preload 之前完成,否则 Attributes 可能因预加载缺失而漏读。
  5. 灰度开关:通过 openapi:generate 命令的 --reader 参数可显式指定 annotationattributeauto 三档,方便金丝雀发布。

答案

我给出一个“三件套”方案,已在生产环境验证 200+ 接口,零中断上线。

  1. 依赖锁定

    "require": {
        "zircote/swagger-php": "^4.7",
        "doctrine/annotations": "^2.0"
    }
    

    保证 4.x 分支同时支持 Annotation 与 Attribute。

  2. 扫描脚本双入口
    新建 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 做基准测试。

  3. 代码层双写模板
    以控制器为例,老接口保留注解,新接口用 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。

  4. 上线灰度与回滚

    • 蓝绿发布:预发布环境 SWAGGER_READER=annotation,确保老逻辑无误;
    • 金丝雀:10% 节点 SWAGGER_READER=attribute,对比 swagger.jsonpaths 数量一致即通过;
    • 全量:确认无差异后,把 annotation 分支下线,后续只维护 Attributes。
  5. 踩坑小结

    • 若用了 opcache.preload,预加载脚本里必须 require_once vendor/autoload.php,否则 ReflectionAttribute 取不到;
    • Traits 里混写时,Attribute 会继承,Annotation 不会,需在 Traits 中统一风格;
    • 如果接口里同时出现 @OA\JsonContent#[OA\JsonContent],后者覆盖前者,务必保证 ref 指向的 components/schemas 名称一致,否则 UI 会渲染出两个 schema。

拓展思考

  1. 自动化迁移:可基于 nikic/php-parser 写一套 Rector 规则,把 @OA\* 批量转成 #[OA\*],并在 commit message 里打标签,方便回滚。
  2. 性能极致:当 Attributes 100% 覆盖后,可在 swagger-generate.php 里把 DoctrineAnnotationReaderAnnotationRegistry 中卸载,减少 200~300 ms 的扫描耗时。
  3. 多应用隔离:国内微服务盛行,可在 swagger.json 里加 x-internal-app-id 字段,通过 swagger-uirequestInterceptor 动态注入网关 Header,实现“一份文档,多租户调试”。
  4. 安全合规:金融场景下,对外暴露的 swagger.json 需脱敏,可再写一个 Processor 插件,把 example 字段统一替换为 ***,满足等保测评要求。