Skip to content

Commit af22c0a

Browse files
committed
[WEBHOOK]: update doc
1 parent 79b3725 commit af22c0a

File tree

1 file changed

+296
-27
lines changed

1 file changed

+296
-27
lines changed

webhook.rst

Lines changed: 296 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ Webhook
55

66
The Webhook component was introduced in Symfony 6.3.
77

8+
Essentially, webhooks serve as event notification mechanisms, typically via HTTP POST requests, enabling real-time updates.
9+
810
The Webhook component is used to respond to remote webhooks to trigger actions
9-
in your application. This document focuses on using webhooks to listen to remote
10-
events in other Symfony components.
11+
in your application. Additionally, it can assist in dispatching webhooks from the provider side.
12+
13+
This document provides guidance on utilizing the Webhook component within the context of a full-stack Symfony application.
1114

1215
Installation
1316
------------
@@ -16,8 +19,87 @@ Installation
1619
1720
$ composer require symfony/webhook
1821
22+
23+
Consuming Webhooks
24+
------------------
25+
26+
Consider an example of an API where it's possible to track the stock levels of various products.
27+
A webhook has been registered to notify when certain events occur, such as stock depletion for a specific product.
28+
29+
During the registration of this webhook, several pieces of information were included in the POST request,
30+
including the endpoint to be called upon the occurrence of an event, such as stock depletion for a certain product:
31+
32+
.. code-block:: json
33+
{
34+
"name": "a name",
35+
"url": "something/webhook/routing_name"
36+
"signature": "..."
37+
"events": ["out_of_stock_event"]
38+
....
39+
}
40+
41+
42+
From the perspective of the consumer application, which receives the webhook, three primary phases need to be anticipated:
43+
44+
1) Receiving the webhook
45+
46+
2) Verifying the webhook and constructing the corresponding Remote Event
47+
48+
3) Manipulating the received data.
49+
50+
Symfony Webhook, when used alongside Symfony Remote Event, streamlines the management of these fundamental phases.
51+
52+
A Single Entry Endpoint: Receive
53+
--------------------------------
54+
55+
Through the built-in :class:`WebhookController`, a unique entry point is offered to manage all webhooks
56+
that our application may receive, whether from the Twilio API, a custom API, or other sources.
57+
58+
By default, any URL prefixed with ``/webhook`` will be routed to this :class:`WebhookController`.
59+
Additionally, you have the flexibility to customize this URL prefix and rename it according to your preferences.
60+
61+
.. code-block:: yaml
62+
63+
# config/routes/webhook.yaml
64+
webhook:
65+
resource: '@FrameworkBundle/Resources/config/routing/webhook.xml'
66+
prefix: /webhook # or possible to customize
67+
68+
Additionally, you must specify the parser service responsible for analyzing and parsing incoming webhooks.
69+
It's crucial to understand that the :class:`WebhookController` itself remains provider-agnostic, utilizing
70+
a routing mechanism to determine which parser should handle incoming webhooks for analysis.
71+
72+
As mentioned earlier, incoming webhooks require a specific prefix to be directed to the :class:`WebhookController`.
73+
This prefix forms the initial part of the URL following the domain name.
74+
The subsequent part of the URL, following this prefix, should correspond to the routing name chosen in your configuration.
75+
76+
The routing name must be unique as this is what connects the provider with your
77+
webhook consumer code.
78+
79+
.. code-block:: yaml
80+
# config/webhook.yaml
81+
# e.g https://example.com/webhook/routing_name
82+
83+
framework:
84+
webhook:
85+
routing:
86+
my_first_parser: # routing name
87+
service: App\Webhook\ExampleRequestParser
88+
# secret: your_secret_here # optionally
89+
90+
At this point in the configuration, you can also define a secret for webhooks that require one.
91+
92+
All parser services defined for each routing name of incoming webhooks will be injected into the :class:`WebhookController`.
93+
94+
95+
A Service Parser: Verifying and Constructing the Corresponding Remote Event
96+
---------------------------------------------------------------------------
97+
98+
It's important to note that Symfony provides built-in parser services.
99+
In such cases, configuring the service name and optionally the required secret in the configuration is sufficient; there's no need to create your own parser.
100+
19101
Usage in Combination with the Mailer Component
20-
----------------------------------------------
102+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21103

22104
When using a third-party mailer provider, you can use the Webhook component to
23105
receive webhook calls from this provider.
@@ -94,8 +176,6 @@ component routing:
94176
};
95177
96178
In this example, we are using ``mailer_mailgun`` as the webhook routing name.
97-
The routing name must be unique as this is what connects the provider with your
98-
webhook consumer code.
99179

100180
The webhook routing name is part of the URL you need to configure at the
101181
third-party mailer provider. The URL is the concatenation of your domain name
@@ -106,7 +186,195 @@ For Mailgun, you will get a secret for the webhook. Store this secret as
106186
MAILER_MAILGUN_SECRET (in the :doc:`secrets management system
107187
</configuration/secrets>` or in a ``.env`` file).
108188

109-
When done, add a :class:`Symfony\\Component\\RemoteEvent\\RemoteEvent` consumer
189+
Usage in Combination with the Notifier Component
190+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
191+
192+
The usage of the Webhook component when using a third-party transport in
193+
the Notifier is very similar to the usage with the Mailer.
194+
195+
Currently, the following third-party SMS transports support webhooks:
196+
197+
============ ==========================================
198+
SMS service Parser service name
199+
============ ==========================================
200+
Twilio ``notifier.webhook.request_parser.twilio``
201+
Vonage ``notifier.webhook.request_parser.vonage``
202+
============ ==========================================
203+
204+
A custom Parser
205+
~~~~~~~~~~~~~~~
206+
207+
However, if your webhook, as illustrated in the example discussed, originates from a custom API,
208+
you will need to create a parser service that extends :class:`AbstractRequestParser`.
209+
210+
This process can be simplified using a command:
211+
212+
.. code-block:: terminal
213+
214+
$ php bin/console make:webhook
215+
216+
.. tip::
217+
218+
Starting in `MakerBundle`_ ``v1.58.0``, you can run ``php bin/console make:webhook``
219+
to generate the request parser and consumer files needed to create your own
220+
Webhook.
221+
222+
.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html
223+
224+
225+
Depending on the routing name provided to this command, which corresponds, as discussed earlier,
226+
to the second and final part of the incoming webhook URL, the command will generate the parser service responsible for parsing your webhook.
227+
228+
Additionally, it allows us to specify which RequestMatcher(s) from the HttpFoundation component should be applied to the incoming webhook request.
229+
This constitutes the initial step of our gateway process, ensuring that the format of the incoming webhook is validated before proceeding to its thorough analysis.
230+
231+
Furthermore, the command will create the :class:`RemoteEventConsumer`, which manages the remote event returned by the parser.
232+
233+
Moreover, this command will automatically update the previously discussed configuration with the webhook's routing name.
234+
This ensures that not only are the parser and consumer generated, but also that the configuration is seamlessly updated::
235+
236+
// src/Webhook/ExampleRequestParser.php
237+
final class ExampleRequestParser extends AbstractRequestParser
238+
{
239+
protected function getRequestMatcher(): RequestMatcherInterface
240+
{
241+
return new ChainRequestMatcher([
242+
new IsJsonRequestMatcher(),
243+
new MethodRequestMatcher('POST'),
244+
new HostRequestMatcher('regex'),
245+
new ExpressionRequestMatcher(new ExpressionLanguage(), new Expression('expression')),
246+
new PathRequestMatcher('regex'),
247+
new IpsRequestMatcher(['127.0.0.1']),
248+
new PortRequestMatcher(443),
249+
new SchemeRequestMatcher('https'),
250+
]);
251+
}
252+
253+
/**
254+
* @throws JsonException
255+
*/
256+
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?RemoteEvent
257+
{
258+
// Adapt or replace the content of this method to fit your need.
259+
// e.g Validate the request against $secret and/or Validate the request payload
260+
// and/or Parse the request payload and return a RemoteEvent object or throw an exception
261+
262+
return new RemoteEvent(
263+
$payload['name'],
264+
$payload['id'],
265+
$payload,
266+
);
267+
}
268+
}
269+
270+
271+
Now, imagine that in your case, you receive a notification of a product stock outage, and the received JSON contains details about the affected product and the severity of the outage.
272+
Depending on the specific product and the severity of the stock outage, your application can trigger different remote events.
273+
274+
For instance, you might define ``HighPriorityStockRefillEvent``, ``MediumPriorityStockRefillEvent`` and ``LowPriorityStockRefillEvent``.
275+
276+
277+
By implementing the :class:`PayloadConverterInterface` and its :method:`Symfony\\Component\\RemoteEvent\\PayloadConverterInterface::convert` method, you can encapsulate all the business logic
278+
involved in creating the appropriate remote event. This converter will be invoked by your parser.
279+
280+
For inspiration, you can refer to :class:`MailGunPayloadConverter`::
281+
282+
// src/Webhook/ExampleRequestParser.php
283+
final class ExampleRequestParser extends AbstractRequestParser
284+
{
285+
protected function getRequestMatcher(): RequestMatcherInterface
286+
{
287+
...
288+
}
289+
290+
/**
291+
* @throws JsonException
292+
*/
293+
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?RemoteEvent
294+
{
295+
// Adapt or replace the content of this method to fit your need.
296+
// e.g Validate the request against $secret and/or Validate the request payload
297+
// and/or Parse the request payload and return a RemoteEvent object or throw an exception
298+
299+
try {
300+
return $this->converter->convert($content['...']);
301+
} catch (ParseException $e) {
302+
throw new RejectWebhookException(406, $e->getMessage(), $e);
303+
}
304+
}
305+
}
306+
307+
// src/RemoteEvent/ExamplePayloadConverter.php
308+
final class ExamplePayloadConverter implements PayloadConverterInterface
309+
{
310+
public function convert(array $payload): AbstractPriorityStockRefillEvent
311+
{
312+
...
313+
314+
if (....) {
315+
$event = new HighPriorityStockRefillEvent($name, $payload['id]', $payload])
316+
} elseif {
317+
$event = new MediumPriorityStockRefillEvent($name, $payload['id]', $payload])
318+
} else {
319+
$event = new LowPriorityStockRefillEvent($name, $payload['id]', $payload])
320+
}
321+
322+
....
323+
324+
return $event;
325+
}
326+
}
327+
328+
From this, we can see that the Remote Event component is highly beneficial for handling webhooks.
329+
It enables us to convert the incoming webhook data into validated objects that can be efficiently manipulated and utilized according to our requirements.
330+
331+
Remote Event Consumer: Handling and Manipulating The Received Data
332+
------------------------------------------------------------------
333+
334+
It is important to note that when the incoming webhook is processed by the :class:`WebhookController`, you have the option to handle the consumption of remote events asynchronously.
335+
Indeed, this can be configured using a bus, with the default setting pointing to the Messenger component's default bus.
336+
For more details, refer to the :doc:`Symfony Messenger </components/messenger>` documentation
337+
338+
339+
Whether the remote event is processed synchronously or asynchronously, you'll need a consumer that implements the :class:`ConsumerInterface`.
340+
If you used the command to set this up, it was created automatically
341+
342+
.. code-block:: terminal
343+
344+
$ php bin/console make:webhook
345+
346+
Otherwise, you'll need to manually add it with the ``AsRemoteEventConsumer`` attribute which will allow you to designate this class as a :class:`ConsumerInterface`,
347+
making it recognizable to the Remote Event component so it can pass the converted object to it.
348+
Additionally, the name passed to your attribute is critical; it must match the configuration entry under routing that you specified in the ``webhook.yaml`` file, which in your case is ``my_first_parser```.
349+
350+
In the :method:`Symfony\\Component\\RemoteEvent\\Consumer\\ConsumerInterface::consume` method,
351+
you can access your object containing the event data that triggered the webhook, allowing you to respond appropriately.
352+
353+
For example, you can use Mercure to broadcast updates to clients of the hub, among other actions ...::
354+
355+
// src/Webhook/ExampleRequestParser.php
356+
#[AsRemoteEventConsumer('my_first_parser')] # routing name
357+
final class ExampleWebhookConsumer implements ConsumerInterface
358+
{
359+
public function __construct()
360+
{
361+
}
362+
363+
public function consume(RemoteEvent $event): void
364+
{
365+
// Implement your own logic here
366+
}
367+
}
368+
369+
370+
If you are using it alongside other components that already include built-in parsers,
371+
you will need to configure the settings (as mentioned earlier) and also create your own consumer.
372+
This is necessary because it involves your own business logic and your specific reactions to the remote event(s) that may be received from the built-in parsers.
373+
374+
Usage in Combination with the Mailer Component
375+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
376+
377+
You can add a :class:`Symfony\\Component\\RemoteEvent\\RemoteEvent` consumer
110378
to react to incoming webhooks (the webhook routing name is what connects your
111379
class to the provider).
112380

@@ -122,7 +390,7 @@ events::
122390
use Symfony\Component\RemoteEvent\RemoteEvent;
123391

124392
#[AsRemoteEventConsumer('mailer_mailgun')]
125-
class WebhookListener implements ConsumerInterface
393+
class MailerWebhookConsumer implements ConsumerInterface
126394
{
127395
public function consume(RemoteEvent $event): void
128396
{
@@ -148,19 +416,7 @@ events::
148416
}
149417

150418
Usage in Combination with the Notifier Component
151-
------------------------------------------------
152-
153-
The usage of the Webhook component when using a third-party transport in
154-
the Notifier is very similar to the usage with the Mailer.
155-
156-
Currently, the following third-party SMS transports support webhooks:
157-
158-
============ ==========================================
159-
SMS service Parser service name
160-
============ ==========================================
161-
Twilio ``notifier.webhook.request_parser.twilio``
162-
Vonage ``notifier.webhook.request_parser.vonage``
163-
============ ==========================================
419+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
164420

165421
For SMS webhooks, react to the
166422
:class:`Symfony\\Component\\RemoteEvent\\Event\\Sms\\SmsEvent` event::
@@ -189,13 +445,26 @@ For SMS webhooks, react to the
189445
}
190446
}
191447

192-
Creating a Custom Webhook
193-
-------------------------
194448

195-
.. tip::
449+
Providing Webhooks
450+
------------------
196451

197-
Starting in `MakerBundle`_ ``v1.58.0``, you can run ``php bin/console make:webhook``
198-
to generate the request parser and consumer files needed to create your own
199-
Webhook.
452+
Symfony Webhook and Symfony Remote Event, when combined with Symfony Messenger, are also useful for APIs responsible for dispatching webhooks.
200453

201-
.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html
454+
For instance, you can utilize the specific :class:`SendWebhookMessage` and :class:`SendWebhookHandler` provided, dispatching the message either synchronously or asynchronously using the Symfony Messenger component.
455+
456+
The SendWebhookMessage takes a :class:`Subscriber` as its first argument, which includes the destination URL and the mandatory secret.
457+
If the secret is missing, an exception will be thrown.
458+
459+
As a second argument, it expects a :class:`RemoteEvent` containing the webhook name, the ID, and the payload, which is the substantial information you wish to communicate.
460+
461+
The :class:`SendWebhookHandler` handles configuration of headers, body of the request and finally sign the headers before making an HTTP request to the specified URL using Symfony's HttpClient component::
462+
463+
$subscriber = new Subscriber($urlCallback, $secret);
464+
465+
$event = new Event(‘name.event, ‘1’, […]);
466+
467+
$this->bus->dispatch(new SendWebhookMessage($subscriber, $event));
468+
469+
470+
However, you have the flexibility to define your own message, handler, or custom mechanism, and process it either synchronously or asynchronously.

0 commit comments

Comments
 (0)