php 生成器 — 其技能
此文件是您已导出的 php 生成器(generators/php/)的设计文档:
要修改生成器,请先编辑此技能,然后使代码与之匹配 — 对 generators/php/ 的更改如果没有此文件中的对应说明,则视为不完整。
其输出内容
一个自包含的 <stem>.php 文件:使用构造函数属性提升的模型类,一个包含每个操作的类型化方法的 Client 类,以及嵌入式运行时。PHP ≥ 8.1,通过 cURL 扩展进行 HTTP 通信 — 零 Composer 依赖。命名空间源自 API 标题(identifierFor(title, pascal) — 例如 CafeOrders)。
必须保持的设计决策
- 模型是
final class,使用构造函数属性提升,必需参数在前,可选参数可为 null (= null)。每个类在编译时生成 hydration 方法:fromArray(array $data): self和toArray(): array(内联使用网络名称;序列化时跳过 null) — 无反射。omit模式通过其基类进行 hydration/序列化。类型为区分联合(DISCRIMINATED union)的属性或响应通过联合的unmarshalX调度器进行 hydration,因此消费者可以使用instanceof进行类型收窄;其类型包含array,因为具有未知鉴别器的值将保留为原始数组。任何其他联合将保留为解码后的 JSON,因此其类型与 JSON 一致:类成员为array,枚举成员为其底层标量,日期成员为string。 - 文件通过 PHPStan 级别 5。 它最终位于用户自己的项目中,因此其分析器会读取它。操作表是一个 PHPStan 完全了解的常量,因此方法仅在表中有对应条目时读取
$op['security'],否则传递[]。 writeOnly属性是可选的,即使在规范中标记为必需:请求和响应共享同一个类,而响应永远不会携带该属性。它为 nullable (= null),因此fromArray可以在没有该属性的情况下 hydration 响应,而toArray在请求中仍会发送它。Client类不是final— PHP 测试套件会模拟具体类 (createMock(Client::class)),而final会迫使每个消费者创建一个包装器接口。模型类保持final。- 每个参数都是独立的参数,因此它们的名称与方法自身声明的参数(
$body、$headers、$idempotencyKey)共享一个命名空间。 使用uniqueIdentifiers(..., { taken: … })构建它们:OpenAPI 允许一个操作在两个位置使用相同的名称(路径中的id和查询中的id),而 PHP 会直接拒绝重复的参数定义。网络名称保持不变,因此请求不变。 - 操作映射是一个类常量
Client::OPERATIONS,通过self::OPERATIONS[...]读取,而不是文件级别的const。OPcache 预加载会将类与函数保留在共享内存中,但会丢弃文件级常量,并且一旦Client类存在,自动加载器就不会再次包含该文件 — 因此文件级映射在第一次请求后的每次请求中都将是未定义的。类常量随类一起存储。 - 命名: 类使用 PascalCase,属性/方法使用 camelCase,通过
identifierFor(..., RESERVED_WORDS.php)实现;保留字后添加下划线。 - 枚举是原生支持的枚举(string/int);其他标量保留为别名。区分联合是基于
match的unmarshalX(array $data)调度器。每个分支像处理该类型的属性一样 hydration 其映射目标,因此没有自己类的目标(纯oneOf)保持为原始数组;allOf 被展平。 - 联合类型在 PHP 8.1 可以表达的地方保留其类型。 标量、枚举、类或数组的联合变为原生联合类型(
int|string、string|array),而不是塌缩为mixed— 富列表过滤是常见情况,丢失其类型就失去了类型化 SDK 的意义。仅当某个成员没有自己的 PHP 类型(内联对象、交集、unknown)时才回退到mixed,因为mixed不能出现在联合内部。可空性在联合中表示为|null(PHP 禁止混用?和|),对于单一类型表示为?T。 - 错误: 异常是错误模式(
ApiError/TimeoutError继承\RuntimeException);errorMode不会改变输出(生成器声明errorModes: ['throw'],因此result会快速失败)。 - 日期:
dateType: Date将format: date/date-time类型化为\DateTimeImmutable;hydration 使用new \DateTimeImmutable(...),序列化使用\DateTimeInterface::ATOM(date-time)或'Y-m-d'(date),包括查询参数。 - 方法参数: 必需路径参数为位置参数,JSON 请求体紧随其后,可选查询参数为可空的命名参数(NAMED arguments),然后是
?array $headers,以及修改方法上的?string $idempotencyKey。 - 非 JSON 成功响应体(PDF、图片、八位字节流)返回原始体作为
string— 二进制下载绝不能降级为void。 - PHPDoc 承载签名无法表达的信息。 PHP 的
array和\Generator会擦除其元素类型,因此 docblock 需要声明:集合返回使用@return Customer[],<op>Pages()/<op>Items()使用@return \Generator<int, Customer>。静态分析和阅读器依赖这些注释;没有注解的 hydration 返回值看起来是未类型化的。 - 响应头: 声明了成功响应头的操作会获得一个
<op>WithHeaders()变体,返回一个Envelope(data、headers— 强制转换为 int/bool/string,键名使用 camelCase,缺失或无法解析的值被省略 — 以及status)。没有声明头的操作不会获得变体,基础方法保持仅返回体(PHP 无法根据标志改变返回类型)。 - 服务器: 当描述声明服务器时,会生成一个
Servers类,每个服务器对应一个静态方法;服务器变量成为命名的字符串参数,默认使用规范的默认值(Servers::production(organizationId: 'org_x')),因此模板化的基础 URL 无需手动字符串构建。客户端的固定默认值保持为servers[0],并替换变量默认值。 - 流式请求体直接传递。 对于非类型化的
multipart/form-data操作(来自工具包的isUntypedMultipartBody:既不是对象也不是$ref的模式)或二进制操作(isBinaryContentType),请求体类型为mixed,由运行时处理(multipartBody/binaryBody):流resource通过 cURL 分块上传(CURLOPT_UPLOAD+CURLOPT_INFILE,方法由CURLOPT_CUSTOMREQUEST保持),从不读入字符串,无论重试策略如何,仅尝试一次,因为已消费的流无法重播。CURLOPT_INFILESIZE仅在流可寻址且fstat返回数组(从当前位置开始的剩余字节数)时设置;php://input、管道和不支持stream_stat的包装器使用分块传输。任一操作的string类型请求体直接发送,不进行json_encode;二进制操作的其他值仍会json_encode;非类型化 multipart 操作的array类型请求体保持toMultipart路径。调用者提供的Content-Type优先级高于描述或 multipart 边界的,在send中不区分大小写匹配,因此该标头永远不会发送两次;此规则适用于所有操作。整个请求的 cURL 超时仍然适用(timeout: null禁用)。类型化的 multipart 请求体不受影响。 - 对等特性: 认证、带有
Retry-After和抖动退避的重试、每次尝试的 cURL 超时、中间件可调用对象、分页(<op>Pages()/<op>Items()作为\Generator)、SSE(通过 curl_multi 泵的iterSse)、multipart。 - 运行时在此文件夹的
runtime/runtime.php中手动编写(通过php -l检查),并在准备时嵌入。curl_close从不调用(自 PHP 8.5 起已弃用,自 8.0 起为无操作)。在--runtime module模式下,它作为一个runtime.php文件编写,客户端通过require_once引入,其命名空间被重写为客户端的命名空间,因此一个命名空间跨越两个文件。 onSettled每个发送的调用仅触发一次。send()在重试循环之后,在抛出或返回之前,依次调用每个Config->onSettled可调用对象,然后调用每个Config::$registeredOnSettled可调用对象,传递operationId、status(无响应时为 0)、timedOut、attempts和durationMs。每个钩子都会执行。对于成功调用,抛出第一个钩子错误;对于失败调用,原始错误优先,钩子错误写入error_log。SSE 流在send()外部打开,因此不触发任何钩子。- 遥测在生成时可选择启用。 使用
client.telemetry: on时,生成器还会在客户端文件旁边写入<client>.telemetry.php(来自此文件夹中的telemetry/telemetry.php,在准备时嵌入),两种运行时模式均可,并使用客户端的命名空间并嵌入生成器版本。客户端文件不变。应用程序需要引入它,并在Config->onSettled中传递new UsageTelemetry(...)。SDK 所有者改为调用register(),该方法将实例添加到Config::$registeredOnSettled,因此每个客户端都会统计其调用次数;UsageTelemetry::flushRegistered()刷新每个已注册的实例。PHP 没有计时器:发现窗口已过期的调用会先发送,而关闭函数会发送打开的窗口。常量存在于类上,因为 OPcache 预加载会丢弃文件级常量。类名对每个 SDK 都是保留的,因为该文件与模型共享命名空间。 - 仅使用中性工具包编写 — 否则 dogfooding 保护会失败。
从基于服务的 SDK 迁移
-
按资源划分的服务(
$client->customers()->get($id))映射为以 operationId 命名的扁平方法($client->getCustomer($id));可选的查询参数保持其命名参数风格(filter:、sort:、limit:)。 -
公开分页响应头(
getTotalItems()、getLimit())的集合包装器映射到<op>WithHeaders()信封(->headers['paginationTotal']);简单的迭代映射到<op>Items()/<op>Pages()生成器。 -
公开字段错误的专用验证异常类映射到
catch (ApiError $e)+$e->status === 422+ 解码后的$e->body。 -
会话/bearer token 流程映射到
auth: ['bearer' => $tokenProvider],并使用可调用对象 — 每个请求解析一次,因此刷新无需重建客户端。 -
它自身生成文档。 使用
client.docs(或--docs)时,docs钩子写入<stem>.php.md:安全方案,然后是每个操作的一个章节,包含其参数、请求体、响应类型和行为说明。调用代码片段来自此生成器自身的sample钩子,因此页面只能显示旁边 SDK 的语法,布局来自创作工具包中的renderReferencePage— 可通过@redocly/client-generator从导出的副本访问。页面上的分页由paginationRuleFor决定,与该生成器解析分页时使用的辅助函数相同。
修改循环
- 编辑此技能:描述新的行为或决策。
- 使
generators/php/与之匹配。 - 运行
redocly generate-client并检查生成输出的git diff— 生成的文件从不手动编辑。
更新的内置版本可通过 redocly eject-generator php --update 合并。