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).
Looking forward to using it!
One quick question though: Where is that
string $secret
argument in MailerWebhookParser:: doParse()` coming from?Do you explicitly call this
RequestParser
in yourController
, which allows you to pass arbitrary data, or how is it being used?I'm guessing I'd have to watch the keynote or wait for the docs? :)
This might be the biggest improvement Symfony has made...
Can I suggest some pre-configured signature verification handlers get included and/or that it be abstracted out in such a way that you can easily include classes for this? No sense in having everyone re-implement the same signature verification code for the same providers over and over again.
Can somebody explain, why this can't be achieved with a regular controller and the event dispatcher?
@Christian Stoller First of all, this components will allow you to use ready-made implementations of events for various webhook providers, work with them consistently, and easily replace them when necessary. Like you already work with the mailer, that is integrated with different providers. And if you are writing your own webhook handler, this components do best practices (like asynchronous handling) for you. I believe the main goal of these components is to ultimately free you from writing your own request parsers, allowing you to focus solely on the event consumers.
Great addition!
In the last code example, the class needs to extend
Symfony\Component\RemoteEvent\Consumer\ConsumerInterface
otherwise it will throw an exception:https://github.com/symfony/symfony/blob/0a49ff8c54968c219d4bd4363b44371665b1755a/src/Symfony/Component/RemoteEvent/Messenger/ConsumeRemoteEventHandler.php#L37
@Kai Eichinger: that's a parser-specific config value. You define the parsers in the config under
webhook.routing
and there you can define the secret for this webhook:https://github.com/symfony/symfony/blob/0a49ff8c54968c219d4bd4363b44371665b1755a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php#L2208
Thanks
I think there could be a bug in those examples. The "name" of the RemoteEvent is irrelevant because the name key of the AsRemoteEventConsumer-Attribute has to be the webhook.routing.type from the configuration.
So:
#[AsRemoteEventConsumer(name: 'mailer_callback.event')]
doesnt' work.
If the Hookpath is /webhook/mailer it must be
#[AsRemoteEventConsumer(name: 'mailer')]
to work.