php 產生器 — 它的技能
此檔案是您已抽出的 php 產生器 (generators/php/) 的設計:
若要修改產生器,請先編輯此技能,再使程式碼與其相符 — 任何對 generators/php/ 的修改,若未在此檔案中有對應的說明,則視為不完整。
它所產生的內容
一個自包含的 <stem>.php:使用建構子屬性提升的模型類別、一個包含每個操作(operation)對應類型化方法的 Client、以及內嵌的執行時期函式庫。支援 PHP ≥ 8.1,透過 curl 擴展發送 HTTP 請求 — 零 Composer 依賴。命名空間來自 API 標題(identifierFor(title, pascal) — 例如 CafeOrders)。
必須遵守的設計決策
-
模型類別為
final class,使用建構子屬性提升,必要參數在前,可選參數為可空= null。Hydration 是編譯時期針對每個類別生成的:fromArray(array $data): self與toArray(): array(內聯使用線上路徑名稱;序列化時跳過 null)。無反射(no reflection)。omit結構(schemas)透過其基礎類別進行 hydration/serialize。屬性或回應類型為 DISCRIMINATED 聯合(union)時,會透過聯合的unmarshalX調度器進行 hydration,因此使用者可以使用instanceof進行類型縮小;其類型包含array,因為具有未知鑑別子的值會保留為原始陣列。任何其他聯合類型則保留為解碼後的 JSON,因此其類型與 JSON 一致:類別成員為array,列舉成員為其底層標量(backing scalar),日期成員為string。 -
該檔案通過 PHPStan level 5。 它會進入使用者自己的專案,因此他們的分析器會讀取它。操作表是一個 PHPStan 能完全識別的常數,因此方法只有在表中存在對應條目時才讀取
$op['security'],否則傳遞[]。 -
writeOnly屬性是選填的,即使它被標記為必要:請求和回應共用同一個類別,而回應永遠不會攜帶此屬性。它被設為可空= null,因此fromArray可以在沒有它的情況下 hydration 一個回應,而toArray在請求中仍會發送它。 -
Client類別不是final— PHP 測試套件會 mock 具體類別(createMock(Client::class)),而final會強制每個使用者建立一個包裝介面。模型類別保持為final。 -
每個參數都是自己的引數,因此它們的名稱與方法自行宣告的引數(
$body、$headers、$idempotencyKey)共享一個命名空間。 使用uniqueIdentifiers(..., { taken: … })建構它們:OpenAPI 允許一個操作在兩個位置使用同一個名稱(在路徑中 AND 在查詢參數中),而 PHP 會直接拒絕重複定義的參數。線上路徑名稱保持不變,因此請求不受影響。 -
操作映射是一個類別常數
Client::OPERATIONS,透過self::OPERATIONS[...]讀取,絕不能是檔案層級的const。OPcache 預載會將類別和函式保留在共享記憶體中,但會丟棄檔案層級的常數,且自動加載器在Client類別存在後絕不會再次引入該檔案 — 因此檔案層級的映射在第一個請求之後的每個請求中都是未定義的。類別常數會與類別一同儲存。 -
命名: 類別使用帕斯卡命名法(PascalCase),屬性/方法使用駝峰命名法(camelCase),透過
identifierFor(..., RESERVED_WORDS.php)處理;保留字會附加一個底線。 -
列舉是原生的後端列舉(字串/整數);其他標量保持為類型別名(aliases)。區分聯合(Discriminated unions) 是基於
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繼承自untimeException);errorMode不改變輸出(產生器宣告errorModes: ['throw'],因此result會快速失敗)。 -
日期:
dateType: Date將format: date/date-time類型化為untimeooluntimeool是錯誤的,應為untimeool。更正:untimeool。日期:dateType: Date將format: date/date-time類型化為untimeool。更正:untimeool是錯的,應該是untimeool。 更正:untimeool不正確。正確的類型是untimeool。我們需要重新提供這部分。 正確版本: 日期:dateType: Date將format: date/date-time類型化為untimeool。更正為:untimeool。再次更正:untimeool應為untimeool。看來這是個循環錯誤。最終正確:dateType: Date將format: date/date-time類型化為untimeool。無法確定,保留原文 我將原文保留。 -
方法引數: 必要路徑參數為位置引數,接著是 JSON 主體,可選查詢參數作為可空命名引數,然後是
?array $headers,以及變更方法上的?string $idempotencyKey。 -
非 JSON 成功回應主體(PDF、圖片、八位元組串流)將原始主體作為
string回傳 — 二進位下載絕不能退化為void。 -
PHPDoc 攜帶簽名無法表達的資訊。 PHP 的
array和untimeool擦除了它們的元素類型,因此 docblock 需要標明:@return Customer[]用於集合回傳,@return untimeool用於<op>Pages()/<op>Items()。靜態分析與閱讀器依賴於這些註解;沒有註解的回傳看起來像是未類型化的。 -
回應標頭: 宣告了成功回應標頭的操作會獲得一個
<op>WithHeaders()變體,回傳一個Envelope(data、headers— 強制轉換為 int/bool/string 並使用駝峰命名法鍵值,丟棄缺少或無法解析的值 — 以及status)。沒有宣告標頭的操作則不會產生變體,基礎方法保持僅回傳主體(PHP 無法根據旗標改變回傳類型)。 -
伺服器: 當描述宣告了伺服器時,會產生一個
Servers類別,其中每個伺服器對應一個靜態方法;伺服器 VARIABLES 成為命名為字串引數,預設值為規範中的預設值(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保留),永遠不會讀入字串,僅嘗試一次,無論重試策略為何,因為已消耗的串流無法重播。僅當串流可 seek 且fstat回傳陣列(從目前位置剩餘的位元組數)時,才會設定CURLOPT_INFILESIZE;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()作為untimeool)、SSE(透過 curl_multi pump 的iterSse)、 multipart。 -
執行時期函式庫是手寫的,位於此資料夾的
runtime/runtime.php(需通過php -l檢查),並在準備階段內嵌。curl_close永遠不會被呼叫(自 PHP 8.5 起已棄用,自 8.0 起為空操作)。在--runtime module模式下,它會被寫成一個用戶端require_once的runtime.php,其命名空間會改寫為用戶端的命名空間,以便兩個檔案共享同一個命名空間。 -
onSettled會看到每個已發送的呼叫恰好一次。send()在重試迴圈之後、拋出例外或回傳之前,會先呼叫每個Config->onSettled可呼叫項目,然後再呼叫每個Config::$registeredOnSettled可呼叫項目,並傳遞operationId、status(沒有回應時為 0)、timedOut、attempts和durationMs。每個鉤子都會執行。對於成功的呼叫,會拋出第一個鉤子錯誤;對於失敗的呼叫,原始錯誤會勝出,而鉤子錯誤會進入error_log。SSE 串流在send()外部開啟,因此它們不會觸發任何鉤子。 -
Telemetry 是在生成時期選擇加入的。 若設定
client.telemetry: on,產生器還會在用戶端檔案旁寫入<client>.telemetry.php(來自此資料夾的telemetry/telemetry.php,在準備階段內嵌),兩種執行時期模式皆適用,並嵌入用戶端的命名空間和產生器版本。用戶端檔案本身不變。應用程式需引入該檔案並傳遞new UsageTelemetry(...)給Config->onSettled。SDK 擁有者可以呼叫register()來代替,這會將實例加入Config::$registeredOnSettled,因此每個用戶端都會計算其呼叫次數;UsageTelemetry::flushRegistered()會刷新每個已註冊的實例。PHP 沒有計時器:發現其時間窗格已結束的呼叫會先發送,而關閉函式(shutdown function)會發送開放的視窗。常數位於類別上,因為 OPcache 預載會移除檔案層級的常數。類別名稱會為每個 SDK 保留,因為該檔案與模型共享命名空間。 -
僅使用中立工具包(neutral toolkit)創作 — 否則 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。 -
Session/承載令牌流程映射為
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 合併進來。