介绍mezzio-swoole中的webhooks

78 阅读3分钟

我是通过Zend公司的前同事John Herren在2009年的一篇博文第一次接触到webhooks的概念的。 当时,它们还处于起步阶段;今天,它们已经无处不在了,因为它们为服务提供了一种机制来通知相关方事件。 这节省了流量;消费者不需要轮询API的事件变化,而是由服务直接通知他们。 这也意味着消费者不需要设置像cronjob这样的东西;而是设置一个webhook端点,在服务提供者那里注册,他们的应用程序会处理其余的事情。

问题是,处理一个webhook往往会导致额外的处理,而且你被期望向提供者发送一个即时响应,表明你收到了该事件。

你怎样才能做到这一点呢?

卸载处理

是Mezzio和OpenSwoole1的粉丝,这可能已经不是什么秘密了。 在一个持久化进程中运行PHP,迫使我考虑应用程序中的状态,这反过来又迫使我在编码时更加谨慎和明确。 除此之外,我还得到了持久化缓存、更好的性能等方面的好处。

我在mezzio-swoole(Mezzio的Swoole和OpenSwoole绑定)中推动的一个功能是与swoole任务工作者一起工作的功能。 使用该功能的方法有很多,但我最喜欢的是使用PSR-14 EventDispatcher来分配一个事件,并为其附加可延迟的监听器。

那是什么样子的呢?

假设我有一个GitHubWebhookEvent ,我在我的事件调度器中为它关联了一个GitHubWebhookListener2。我将按如下方式调度这个事件:

/** @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任务,它将执行推迟到任务工作者,从Web工作者那里卸载。

事件状态

任务工作者收到的是事件的副本,而不是原始实例。 你的监听器在事件实例中的任何状态变化都不会反映在Web工作者中存在的实例中。 因此,你应该只推迟那些不通过事件将状态传回给调度代码的监听器。

与Web服务器共享一个事件调度器

mezzio-swoole定义了一个标记接口,Mezzio\Swoole\Event\EventDispatcherInterface 。这个接口用来定义一个由Mezzio\Swoole\SwooleRequestHandlerRunner 消费的事件调度器服务,目的是调度swoole HTTP服务器的事件,绕过swoole遵循的 "一个事件,一个处理器 "的规则。然而,这可能意味着你的应用程序中最终会有两个不同的调度器:一个由swoole web服务器使用,一个由应用程序使用,这意味着你不能委托任务。

为了解决这个问题,把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));

通过webhooks卸载处理

这意味着你可以为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 响应,表明我们已经接受了有效载荷,而任务工作者会在有机会时将有效载荷交付给我们来处理。

我喜欢这种方法,因为它使网络逻辑保持最小和最简单,同时让我有能力用我掌握的所有工具来处理webhook事件:

验证

你要确保在做任何实际处理之前验证你的有效载荷。 如果需要,你可以在处理程序中这样做,并在需要时返回一个4xx错误。 然而,我的经验是,大多数使用webhooks的服务提供商对这样的错误不做任何处理,除了在一系列这样的响应之后可能停止发送有效载荷。 因此,我通常把验证放在我的监听器中,在那里我可以记录问题并在之后跟进它们。

其他考虑

  • 许多服务在发送webhooks时,会使用一个共享的秘密。 这可能被用来生成一个签名,在头中发送,甚至只是一个头值,表明有效载荷来自他们。 我把这种验证放到中间件中,因为它(a)在秘密相同的情况下可以重复使用,或者我可能有多个webhooks为同一供应商的不同事件注册。 Mezzio使得在定义路由时可以添加中间件,确保中间件只在需要时被触发:

    $app->post('/api/github/release', [
        GitHubWebhookValidationMiddleware::class, // validation middleware
        GitHubReleaseWebhookHandler::class,       // webhook handler
    ], 'webhook.github.release');
    
  • 你想优雅地管理你的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');
    
  • 同样,当有错误时,你的监听器应该让你知道。 最好的方法是通过日志,或通过你在应用中可能使用的任何监控API来做到这一点。

脚注

  • 1在整个文档中,我把这两个项目统称为 "swoole"。
  • 2PSR-14定义了一个ListenerProviderInterface ,事件派发者可以选择从其中检索与派发事件相关的监听器。连接这些监听器取决于应用开发者;PSR-14库通常提供这些机制。