HOWTO · PHP
在 PHP 中建立一個 Webhook
本教學說明如何建立 PHP 端點、接收並驗證 JSON POST 請求,以及在本機測試端點。
本頁內容
Webhook 是一種 HTTP 請求:某項服務會在事件發生時,將請求傳送至另一個系統上的 URL。在此範例中,PHP 接收包含 JSON payload 的 POST 請求。端點是接收請求的一方;JSON 則是資料格式。在本機寫入檔案並不會建立 webhook,不過接收端可以在接受請求後記錄或儲存資料。
PHP Webhook 接收端點的用途
傳送服務會向你設定的公開 URL 發出 HTTP 請求,例如 https://example.com/webhook.php。網頁伺服器會將請求導向 PHP 指令碼。指令碼會檢查請求方法、讀取原始本文、解析 payload,並傳回 HTTP 狀態碼,讓傳送端知道是否已接受這次傳送。
Webhook 傳送端通常使用 POST,但 payload 格式和驗證機制取決於服務本身。本教學使用 JSON,並以包含事件名稱和訂單 ID 的簡單事件為例。若要接收其他服務的請求,請依照該服務文件所記載的欄位名稱與簽章機制設定。使用者提交的表單適合一般網頁輸入,但無法示範 webhook 的伺服器對伺服器傳送流程。
建立 PHP Webhook 接收端點
將程式碼存成 webhook.php,並放在由 PHP 提供服務的文件根目錄中。透過 HTTP 收到請求時,程式會從 php://input 讀取請求本文。直接從命令列執行時,程式會使用內建的範例本文,因此即使沒有傳送端,也能檢視解析流程。命令列方式僅供本機示範,不能取代對實際 HTTP 路由和伺服器設定的測試。
本節中的程式碼以及顯示的標籤或輸出仍為英文。
<?php
$isCli = PHP_SAPI === 'cli';
$method = $isCli ? 'POST' : ($_SERVER['REQUEST_METHOD'] ?? '');
$rawBody = $isCli
? '{"event":"order.created","order_id":1042}'
: file_get_contents('php://input');
if (!$isCli) {
header('Content-Type: application/json; charset=utf-8');
}
$respond = static function (int $status, array $data) use ($isCli): void {
if (!$isCli) {
http_response_code($status);
}
echo json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), PHP_EOL;
};
if ($method !== 'POST') {
if (!$isCli) {
header('Allow: POST');
}
$respond(405, ['error' => 'POST required']);
exit;
}
$payload = json_decode($rawBody);
if (json_last_error() !== JSON_ERROR_NONE || !($payload instanceof stdClass)) {
$respond(400, ['error' => 'Expected a JSON object']);
exit;
}
$respond(200, [
'received' => true,
'event' => $payload->event ?? null,
'order_id' => $payload->order_id ?? null,
]);
{
"received": true,
"event": "order.created",
"order_id": 1042
}
讀取並驗證 JSON Payload
php://input 提供原始請求本文,因此程式可以直接解碼 JSON,不必先將內容當成表單資料處理。使用 json_decode() 的預設第二個引數時,頂層的 JSON 物件會以 PHP stdClass 執行個體傳回;頂層的 JSON 陣列則會以 PHP 陣列傳回。json_last_error() 可區分有效的 JSON 與格式錯誤的輸入,而 instanceof stdClass 檢查只接受物件。因此,[] 和其他陣列不會進入成功回應,字串和數字等純量 JSON 值也會被排除。
範例也限定只能使用 POST;若使用其他方法,便會回傳 405。本文格式錯誤,或頂層值的形狀不符時,則會收到 400。成功回應只會回傳一小組已知欄位;缺少的欄位會顯示為 null。請依傳送服務調整這份結構,並在使用資料前驗證必要欄位及其類型。不要假設語法正確的 JSON 一定可信或完整。
在本機測試端點
在包含 webhook.php 的目錄中,使用 php -S 127.0.0.1:8000 啟動 PHP 開發伺服器。接著在另一個終端機中,使用 curl -i -X POST -H 'Content-Type: application/json' --data '{"event":"order.created","order_id":1042}' http://127.0.0.1:8000/webhook.php,將 POST 請求傳送至 http://127.0.0.1:8000/webhook.php。命令中的請求本文是可選取的文字;-i 會一併顯示 HTTP 回應標頭,讓你查看狀態碼以及 JSON 回應本文。
執行 php webhook.php 時,指令碼的命令列模式會提供相同的範例 payload;這會測試解碼與回應建構,但不會測試 HTTP 標頭、路由或傳送端的網路請求。若要完整檢查本機 HTTP 流程,請使用開發伺服器和 curl 請求。內建伺服器僅供開發使用,不應作為正式環境的 webhook 主機對外公開。
記錄、顯示或儲存 Payload
開發期間,只檢視診斷傳送問題所需的欄位。避免將完整 payload 列印到公開回應或瀏覽器頁面,因為其中可能含有個人資料、憑證或其他機密。如果要寫入診斷記錄,請將記錄放在公開文件根目錄之外、限制存取權,並設定保留期限。將值寫入逐行記錄檔之前,請先清理內容,以免使用者提供的資料偽造額外的記錄項目。
若要長期處理資料,請先驗證事件和必要欄位,再將接受的值交給應用程式的一般儲存層。寫入資料庫是依應用程式需求而定的後續步驟;此接收端不會連接 SQL,也不會保存範例 payload。可以考慮記錄傳送端提供的事件 ID,並加上唯一性限制,以免重試造成同一筆傳送被處理多次。只有在傳送端預期收到確認的工作已完成或安全地排入佇列後,才回傳成功。
Webhook 接收端的正式環境注意事項
請將傳送端設定為使用 HTTPS URL,並依照傳送端文件所述的方法驗證簽章或驗證 token。如果簽章涵蓋原始請求本文,請在解碼或變更本文之前,以完全相同的位元組驗證簽章。將密鑰存放在原始碼管理之外,安全地比對簽章;若服務提供時間戳記或重放防護,也要拒絕過期的簽署請求。僅靠保密 URL 並不是可靠的驗證方式。
請在網頁伺服器或應用程式層設定請求大小上限,因為解碼過大的本文可能耗用大量記憶體。檢查內容類型和傳送端文件所述的 payload 結構,並在將任何值用於查詢、檔案路徑、命令或下游請求之前先行驗證。避免記錄授權標頭、簽章或完整的敏感 payload。請回傳明確的狀態碼:永久性的格式錯誤請求不應被回覆為成功;暫時性的伺服器錯誤則應讓傳送端的重試政策能正常運作。
最後,請了解服務供應商的傳送行為。有些傳送端若未及時收到成功回應便會重試,因此接收端應保持快速,並以冪等方式處理資料。若工作耗時較長,請先驗證事件並將它排入佇列,再依照服務供應商的要求確認收到。請監控傳送失敗,並以管理其他公開應用程式路由時相同的操作標準保護此端點。