| name | webman-tech-swagger-best-practices |
| description | webman-tech/swagger 最佳实践。使用场景:用户为 webman 接口编写 OpenAPI 文档时,给出明确的推荐写法。 |
webman-tech/swagger 最佳实践
核心原则
- DTO 即文档:用 DTO 类定义请求/响应结构,swagger 自动从中提取类型和验证规则
@handle 一行搞定请求+响应:用 FormClass@handle 同时声明请求 schema 和响应 schema
- Tag 放在 Controller 类上,不要每个方法都写
DTO 的写法详见 webman-tech-dto-best-practices skill。
快速开始
推荐:集中管理注册逻辑
将 swagger 注册逻辑封装到一个专用类,而不是分散在配置文件里:
use OpenApi\Annotations as OAA;
use OpenApi\Attributes as OA;
use Symfony\Component\Finder\Finder;
use WebmanTech\Swagger\Swagger;
final class SwaggerRegister
{
public static function register(): void
{
Swagger::create()->registerRoute([
'register_route' => true,
'swagger_ui' => [
'tag_sort' => ['认证', '用户', '订单'], // 控制 Tag 在 UI 中的排序
],
'basic_auth' => [
'enable' => false,
],
'openapi_doc' => [
'scan_path' => fn() => [
Finder::create()->files()->name('*.php')
->in(app_path('api/controller'))
->exclude(['example']),
],
'modify' => function (OAA\OpenApi $openapi) {
$openapi->info->title = config('app.name') . ' API';
$openapi->info->version = '1.0.0';
$openapi->servers = [
new OA\Server(
url: route_url('/api'),
description: request()->host(),
),
];
if (!$openapi->components instanceof OAA\Components) {
$openapi->components = new OAA\Components([]);
}
$openapi->components->securitySchemes = [
new OA\SecurityScheme(
securityScheme: 'api_key',
type: 'apiKey',
name: 'X-Api-Key',
in: 'header',
),
];
$openapi->security = [['api_key' => []]];
},
'host_forbidden' => [
'ip_white_list_intranet' => true,
],
],
]);
}
}
SwaggerRegister::register();
modify 闭包在每次生成文档时执行,可以读取运行时配置(config()、request()->host() 等),比静态 #[OA\Info] 注解更灵活。
标准接口写法
推荐:@handle 模式(一行同时声明请求和响应)
use OpenApi\Attributes as OA;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[OA\Tag(name: 'users', description: '用户管理')]
final class UserController
{
#[OA\Post(
path: '/users',
summary: '创建用户',
x: [SchemaConstants::X_SCHEMA_REQUEST => UserCreateForm::class . '@handle']
// ^^^^^^^ 自动从 handle() 返回类型推断响应 schema
)]
public function create()
{
return UserCreateForm::fromRequest()->handle()->toResponse();
}
}
@handle 的工作原理:swagger 读取 handle() 方法的返回类型,自动设置为 x-schema-response,无需手动声明。
DTO 类:让类型声明做文档
use OpenApi\Attributes as OA;
use WebmanTech\DTO\BaseRequestDTO;
use WebmanTech\DTO\BaseResponseDTO;
use WebmanTech\DTO\BaseDTO;
use WebmanTech\DTO\Attributes\ValidationRules;
final class UserCreateForm extends BaseRequestDTO
{
public string $name;
#[ValidationRules(min: 1, max: 120)]
public int $age;
public array|null $address = null;
public function handle(): UserCreateFormResult
{
return new UserCreateFormResult(id: 1);
}
}
final class UserCreateFormResult extends BaseResponseDTO
{
public function __construct(
public readonly int $id,
) {}
}
final class UserCreateFormAddressItem extends BaseDTO
{
public string $city;
public string $street;
}
文档注释规范
@example 提供示例值
public string $status;
public int $age;
public array $tags = [];
属性描述写在 docblock 第一行
public string $orderNo;
多个请求 Schema
当一个接口需要合并多个 schema 时:
#[OA\Post(
path: '/orders',
x: [
SchemaConstants::X_SCHEMA_REQUEST => [
CreateOrderForm::class,
CommonPaginationForm::class, // 合并分页参数
]
]
)]
响应包装结构(response_layout)
如果所有接口都有统一的响应结构(如 {code, message, data}),配置 response_layout_class:
#[OA\Schema]
final class ApiResponse
{
public int $code = 0;
public string $message = 'success';
}
'openapi_doc' => [
'response_layout_class' => ApiResponse::class,
'response_layout_data_code' => 'data',
],
配置后,所有接口的 200 响应自动包裹在 ApiResponse 结构中,data 字段填充实际响应 schema。
单个接口可以用 x-response-layout: null 跳过包装:
#[OA\Get(
path: '/health',
x: [SchemaConstants::X_RESPONSE_LAYOUT => null] // 不包装
)]
多文档(多个 Swagger 实例)
Swagger::create()->registerRoute([
'route_prefix' => '/openapi/admin',
'openapi_doc' => [
'scan_path' => [app_path('admin')],
'cache_key' => 'swagger:admin', // 必须指定不同的 cache_key
],
]);
枚举类型
枚举自动提取描述,只需定义枚举类:
enum StatusEnum: string
{
case Active = 'active';
case Inactive = 'inactive';
public function description(): string // 默认调用此方法
{
return match($this) {
self::Active => '启用',
self::Inactive => '禁用',
};
}
}
如果方法名不是 description,通过配置指定:
'openapi_doc' => [
'schema_enum_description_method' => 'label',
],
路由注解(register_route: true 时)
开启 register_route 后,swagger 注解同时作为路由定义,无需在 route.php 中重复写:
#[OA\Post(
path: '/users',
summary: '创建用户',
x: [
SchemaConstants::X_SCHEMA_REQUEST => UserCreateForm::class . '@handle',
SchemaConstants::X_NAME => 'user.create', // 命名路由
SchemaConstants::X_MIDDLEWARE => [AuthMiddleware::class], // 中间件
// SchemaConstants::X_PATH => '/users/{id:\d+}', // 路由 path 与 openapi path 不同时使用
]
)]
安全配置
IP/Host 限制
生产环境禁止外网访问文档:
'host_forbidden' => [
'enable' => true,
'ip_white_list_intranet' => true,
'ip_white_list' => ['1.2.3.4'],
'host_white_list' => ['admin.example.com'],
],
Basic 认证
需要用户名密码才能访问 Swagger UI:
'basic_auth' => [
'enable' => true,
'username' => 'admin',
'password' => 'your-secure-password',
'realm' => 'API Documentation',
],
也可以在 registerRoute 时通过 global_route 配置:
'global_route' => [
'basic_auth' => [
'enable' => true,
'username' => 'admin',
'password' => 'secret',
],
],
两种安全机制(IP 限制 + Basic Auth)可以同时启用,按顺序执行。
自定义中间件
如需更复杂的认证(如基于 auth 包的登录认证),通过 middlewares 配置:
'global_route' => [
'middlewares' => [
new \WebmanTech\Auth\Middleware\Authentication('admin'),
],
],
常见错误
| 错误 | 原因 | 解决 |
|---|
| 文档为空 | 缺少 #[OA\Info] 或 modify 未设置 info | 用 modify 闭包设置 $openapi->info->title 和 version |
| Schema 找不到 | DTO 类不在扫描路径内 | 检查 scan_path 配置 |
@handle 不自动推断响应 | handle() 没有返回类型声明 | 给 handle() 加返回类型 UserCreateFormResult |
| 多实例文档互相覆盖 | 未指定不同 cache_key | 每个实例配置唯一的 cache_key |
| 枚举描述不显示 | 方法名不匹配 | 配置 schema_enum_description_method |