我是通过Zend的前同事JohnHerren在2009年发表的一篇博文首次了解到Webhook的概念的。当时,它们还处于起步阶段;今天,它们无处不在,因为它们为服务提供了一种机制来通知事件的相关方。这节省了流量;该服务直接通知他们,而不是消费者轮询API以获取事件更改。这也意味着消费者不需要设置诸如cronjobs之类的东西;他们设置了一个webhook端点,将其注册到服务提供商,然后他们的应用程序会处理剩下的事情。
事实是,处理webhook通常会导致额外的处理,您应该立即向提供商发送响应,表明您收到了该事件。
如何实现这一点?
卸载处理
我是Mezzio和OpenSwoole1的粉丝可能已经不是什么秘密了。在持久进程中运行PHP迫使我考虑应用程序中的状态,这反过来又通常迫使我成为在编写代码时更加谨慎和明确。除此之外,我还受益于持久缓存、更好的性能等等。
我推入mezzio-swoole(Mezzio的Swoole和OpenSwoole绑定)的一个功能是与swoole任务工作者一起工作的功能。有多种使用该功能的方法,但我最喜欢的是使用PSR-14EventDispatcher派发一个我附加了可延迟侦听器的事件。
那看起来像什么?
假设我有一个GitHubWebhookEvent
,为此我在我的事件调度程序中关联了一个GitHubWebhookListener
2。我会将此事件调度为如下:
/** @var GitHubWebhookEvent $event */ $dispatcher->dispatch($event);
这样做的好处是调度事件的代码不需要知道事件是如何处理的,甚至不需要知道事件的处理时间。它只是调度事件并继续前进。
为了使监听器可延迟,在Mezzio应用程序中,我可以将mezzio-swoole包提供的特殊委托工厂与监听器相关联。这是通过标准Mezzio依赖配置完成的:
use Mezzio\Swoole\Task\DeferredServiceListenerDelegator; return [ 'dependencies' => [ 'delegators' => [ GitHubWebhookListener::class => [ DeferredServiceListenerDelegator::class, ], ], ], ];
这种方法意味着我的侦听器可以有任意数量的依赖项,并连接到容器中,但是当我请求它时,我将返回一个Mezzio\Swoole\Task\DeferredServiceListener
.此类将从侦听器和事件创建一个swoole任务,该任务将执行推迟到任务工作者,从网络工作者中卸载它。
事件状态
任务工作者收到事件的副本,而不是原始实例。您的侦听器在事件实例中所做的任何状态更改都不会反映出来在您的webworker中存在的实例中。因此,您应该仅将不通过事件将状态传达回调度代码的侦听器推迟。
事件状态
任务工作者收到事件的副本,而不是原始实例。您的侦听器在事件实例中所做的任何状态更改都不会反映出来在您的webworker中存在的实例中。因此,您应该仅将不通过事件将状态传达回调度代码的侦听器推迟。
Sharinganeventdispatcherwiththewebserver
mezzio-swoole定义了一个标记接口,
Mezzio\Swoole\Event\EventDispatcherInterface
。该接口用于定义Mezzio\Swoole\SwooleRequestHandlerRunner
使用的事件调度程序服务,用于调度swooleHTTP服务器事件,绕过swoole遵循的“一个事件,一个处理程序”规则。但是,这可能意味着您最终在你的应用程序中有两个不同的调度程序:一个由swooleweb服务器使用,一个由应用程序使用,这意味着你不能委托任务。为了解决这个问题,别名为
Mezzio\Swoole\Event\EventDispatcherInterface
服务到Psr\EventDispatcher\EventDispatcherInterface
服务:use Mezzio\Swoole\Event\EventDispatcherInterface as SwooleEventDispatcher; use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcher; return [ 'dependencies' => [ 'alias' => [ SwooleEventDispatcher::class => PsrEventDispatcher::class, ], ], ];然后确保与事件调度程序一起使用的任何侦听器提供程序都包含以下映射(所有类都在
Mezzio\Swoole\Event
命名空间中):
ServerStartEvent
映射到ServerStartListener
WorkerStartEvent
映射到WorkerStartListener
RequestEvent
映射到StaticResourceRequestListener
RequestEvent
映射到RequestHandlerRequestListener
ServerShutdownEvent
映射到ServerShutdownListener
TaskEvent
映射到TaskInvokerListener
例如,使用我的phly/phly-event-dispatcher包:
/** @var Phly\EventDispatcher\AttachableListenerProvider $provider */ $provider->listen(ServerStartEvent::class, $container->get(ServerStartListener::class)); $provider->listen(WorkerStartEvent::class, $container->get(WorkerStartListener::class)); $provider->listen(RequestEvent::class, $container->get(StaticResourceRequestListener::class)); $provider->listen(RequestEvent::class, $container->get(RequestHandlerRequestListener::class)); $provider->listen(ServerShutdownEvent::class, $container->get(ServerShutdownListener::class)); $provider->listen(TaskEvent::class, $container->get(TaskInvokerListener::class));
通过webhook卸载处理
这意味着您可以为接收负载的webhook编写处理程序,从该负载创建事件,分派事件,并立即返回响应。
作为一个简单的例子,假设webhook事件将只接受其实体中的请求内容:
declare(strict_types=1); namespace App; class WebhookEvent { public function __construct( public readonly string $requestContent, ) { } }
然后我们的webhook将创建一个包含请求内容的事件,发送它,并返回204(空)响应,表示成功:
declare(strict_types=1); namespace App; use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; class AtomHandler implements RequestHandlerInterface { public function __construct( private ResponseFactoryInterface $responseFactory, private EventDispatcherInterface $dispatcher, ) { } public function handle(ServerRequestInterface $request): ResponseInterface { $this->dispatcher->dispatch(new WebhookEvent((string) $request->getBody())); return $this->responseFactory->createResponse(204); } }
GitHub会立即收到204响应,表明我们已经接受了有效负载,并且任务工作者会在有机会时交付有效负载以进行处理。
我喜欢这种方法,因为它使Web逻辑保持最小和简单,同时让我能够使用我可以使用的所有工具来处理Webhook事件。
Validation
您需要确保在进行任何实际处理之前验证您的负载。如果需要,您可以在处理程序中执行此操作,并在需要时返回4xx错误。我的经验然而,大多数使用webhook的服务提供商除了在一系列此类响应之后可能停止发送负载外,不会对此类错误采取任何措施。因此,我通常将验证放入我的侦听器中,我可以在其中记录问题和然后稍后跟进。
Validation
您需要确保在进行任何实际处理之前验证您的负载。如果需要,您可以在处理程序中执行此操作,并在需要时返回4xx错误。我的经验然而,大多数使用webhook的服务提供商除了在一系列此类响应之后可能停止发送负载外,不会对此类错误采取任何措施。因此,我通常将验证放入我的侦听器中,我可以在其中记录问题和然后稍后跟进。
其他注意事项
-
许多服务在发送webhook时会使用共享密钥。这可能用于生成在标头中发送的签名,或者甚至只是标头值表明有效载荷来自他们。我将这种验证放入中间件,因为它(a)在秘密相同的情况下变得可重用,或者我可能为来自同一提供商的不同事件注册了多个webhook。Mezzio使它成为可能在定义路由时添加中间件,确保中间件仅在需要时被触发:
$app->post('/api/github/release', [ GitHubWebhookValidationMiddleware::class, // validation middleware GitHubReleaseWebhookHandler::class, // webhook handler ], 'webhook.github.release');
-
You’llwantto为您的webhook端点优雅地管理错误。即使处理程序中没有太多代码,另一个侦听器也可能引发异常,或者您的某些中间件可能(见上文)。我建议将mezzio-problem-details中间件放在您的webhook中处理程序的管道:
$app->post('/api/github/release', [ \Mezzio\ProblemDetails\ProblemDetailsMiddleware::class, GitHubWebhookValidationMiddleware::class, // validation middleware GitHubReleaseWebhookHandler::class, // webhook handler ], 'webhook.github.release');
-
同样,您的侦听器应该在出现错误时让您知道。b最好的方法是通过日志记录,或通过您可能在应用程序中使用的任何监控API。
脚注
- 1我将在整个文档中将这两个项目统称为“swoole”。
- 2PSR-14定义一个
ListenerProviderInterface
,事件调度程序可以从中选择性地检索与调度事件关联的侦听器。连接这些由应用程序开发人员决定;PSR-14库通常提供这些机制。