HOWTO · PHP

在 PHP 中创建一个 Webhook

本教程介绍如何创建 PHP webhook 端点、验证 JSON POST 请求,并在本地测试端点。

本页内容

Webhook 是一种 HTTP 请求:发生事件时,一个服务会向另一系统上的 URL 发送该请求。在此示例中,PHP 接收包含 JSON 负载的 POST 请求。端点是接收方;JSON 是数据格式。写入本地文件并不会创建 webhook,不过接收端在接受请求后可以记录或存储数据。

PHP Webhook 端点的作用

发送服务会向你配置的公开 URL 发出 HTTP 请求,例如 https://example.com/webhook.php。Web 服务器会将请求路由到 PHP 脚本。脚本会检查请求方法、读取原始请求正文、解析负载,并返回 HTTP 状态码,告知发送方是否接受了此次投递。

Webhook 发送方通常使用 POST,但负载格式和身份验证机制取决于具体服务。本教程使用 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 负载

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 向 http://127.0.0.1:8000/webhook.php 发送 POST 请求。命令中的请求正文是可选取的文本;-i 会包含 HTTP 响应头,便于同时检查状态码和 JSON 响应正文。

运行 php webhook.php 时,脚本的命令行模式会提供相同的示例负载。这会执行解码和响应构造,但不会测试 HTTP 标头、路由或发送方的网络请求。要完整检查本地 HTTP 流程,请使用开发服务器和 curl 请求。内置服务器供开发使用,不应作为生产环境的 webhook 主机对外开放。

记录、显示或存储负载

在开发期间,只检查诊断投递问题所需的字段。不要在公开响应或浏览器页面中打印完整负载,因为其中可能包含个人数据、凭据或其他机密信息。如果要写入诊断记录,请将其保存在公开文档根目录之外,限制访问权限,并设置保留期限。将值写入逐行日志前应先进行清理,避免输入数据伪造额外的日志条目。

若要持久化处理,请先验证事件和必需字段,再将已接受的值交给应用程序常规的数据存储层。写入数据库属于应用程序特定的后续步骤;此接收端不会连接 SQL,也不会持久化示例负载。可以考虑记录发送方提供的事件 ID,并为其设置唯一性约束,以避免重试导致同一投递被处理多次。只有在发送方要求确认的工作已完成或已安全排入队列后,才返回成功。

Webhook 接收端的生产环境注意事项

为发送方配置 HTTPS URL,并按照发送方文档所述的方法验证签名或身份验证令牌。如果签名涵盖原始请求正文,请在解码或修改正文之前,使用完全相同的字节进行验证。将机密保存在源代码管理之外,安全地比较签名;如果服务商支持时间戳或重放防护,也应拒绝过期的签名请求。仅靠不公开的 URL 不是可靠的身份验证方式。

在 Web 服务器或应用程序层设置请求大小上限,因为解码任意大的正文会消耗内存。检查内容类型和发送方文档说明的负载结构,并在将任何值用于查询、文件路径、命令或下游请求之前进行验证。避免记录授权标头、签名或完整的敏感负载。返回明确的状态码:对于永久性格式错误的请求,不应确认成功;对于暂时性的服务器故障,则应允许发送方的重试策略正常工作。

最后,了解服务商的投递行为。有些发送方在未及时收到成功响应时会重试,因此接收端应保持快速,并确保处理过程具备幂等性。对于耗时较长的任务,可以先验证事件并将其加入队列,再按服务商要求确认。监控投递失败,并像保护其他公开应用路由一样妥善维护此端点。