Fabien Potencier
Contributed by Fabien Potencier in #48542

Webhooks are user-defined HTTP callbacks. They allow other services to alert you about external events so you can respond to them. For example, consider the packagist.org website that publishes information about PHP packages. Without webhooks, that site would have to call GitHub, Gitlab, etc. repeatedly to see if the code repositories of your packages changed.

Instead, packagist.org provides some webhooks that GitHub and others can call to send your package details whenever you push new code. This way, the changes are propagated almost immediately and none of these sites waste resources asking other sites if things changed since last time.

Webhooks are so common and convenient that in Symfony 6.3 we're introducing a new Webhook component and a new RemoteEvent component. In Symfony, you define a webhook as a parser + consumer. First, you create a parser able to handle a certain type of webhook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
namespace App\Webhook;

use Symfony\Component\HttpFoundation\ChainRequestMatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher\HostRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\IsJsonRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcher\MethodRequestMatcher;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\RemoteEvent\Exception\ParseException;
use Symfony\Component\Webhook\Client\AbstractRequestParser;
use Symfony\Component\Webhook\Exception\RejectWebhookException;

final class MailerWebhookParser extends AbstractRequestParser
{
    protected function getRequestMatcher(): RequestMatcherInterface
    {
        // these define the conditions that the incoming webhook request
        // must match in order to be handled by this parser
        return new ChainRequestMatcher([
            new HostRequestMatcher('github.com'),
            new IsJsonRequestMatcher(),
            new MethodRequestMatcher('POST'),
        ]);
    }

    protected function doParse(Request $request, string $secret): ?RemoteEvent
    {
        // in this method you check the request payload to see if it contains
        // the needed information to process this webhook
        $content = $request->toArray();
        if (!isset($content['signature']['token'])) {
            throw new RejectWebhookException(406, 'Payload is malformed.');
        }

        // you can either return `null` or a `RemoteEvent` object
        return new RemoteEvent('mailer_callback.event', 'event-id', $content);
    }
}

Then, you create a consumer class able to process the remote event whose name matches the one returned by the parser (in this example, the 'mailer_callback.event' event):

1
2
3
4
5
6
7
8
9
10
11
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer;
use Symfony\Component\RemoteEvent\RemoteEvent;

#[AsRemoteEventConsumer(name: 'mailer_callback.event')]
class MailerCallbackEventConsumer
{
    public function consume(RemoteEvent $event): void
    {
        // Process the event returned by our parser
    }
}

This example showed just the most basic features of the new components, but there's much more. We're still preparing the docs of these components, but meanwhile you can watch for free the Fabien Keynote introducing Webhook and RemoteEvent (in that link you will also find the slides).

Published in #Living on the edge