Skip to content

Commit

Permalink
[WEBHOOK]: update doc
Browse files Browse the repository at this point in the history
  • Loading branch information
alli83 committed Jun 19, 2024
1 parent 79b3725 commit 38750f0
Showing 1 changed file with 298 additions and 27 deletions.
325 changes: 298 additions & 27 deletions webhook.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ Webhook

The Webhook component was introduced in Symfony 6.3.

The Webhook component is used to respond to remote webhooks to trigger actions
in your application. This document focuses on using webhooks to listen to remote
events in other Symfony components.
Essentially, webhooks serve as event notification mechanisms, typically via HTTP POST requests, enabling real-time updates.

The Webhook component is used to respond to remote webhooks in order to trigger actions
in your application. Additionally, it can assist in dispatching webhooks from the provider side.

This document provides guidance on utilizing the Webhook component within the context of a full-stack Symfony application.

Installation
------------
Expand All @@ -16,8 +19,89 @@ Installation
$ composer require symfony/webhook
Consuming Webhooks
------------------

Consider an example of an API where it's possible to track the stock levels of various products.
A webhook has been registered to notify when certain events occur, such as stock depletion for a specific product.

During the registration of this webhook, several pieces of information were included in the POST request,
including the endpoint to be called upon the occurrence of an event, such as stock depletion for a certain product:

.. code-block:: json
{
"name": "a name",
"url": "something/webhook/routing_name"
"signature": "..."
"events": ["out_of_stock_event"]
....
}
From the perspective of the consumer application, which receives the webhook, three primary phases need to be anticipated:

1) Receiving the webhook

2) Verifying the webhook and constructing the corresponding Remote Event

3) Manipulating the received data.

Symfony Webhook, when used alongside Symfony Remote Event, streamlines the management of these fundamental phases.

A Single Entry Endpoint: Receive
--------------------------------

Through the built-in :class:`Symfony\\Component\\Webhook\\Controller\\WebhookController`, a unique entry point is offered to manage all webhooks
that our application may receive, whether from the Twilio API, a custom API, or other sources.

By default, any URL prefixed with ``/webhook`` will be routed to this :class:`Symfony\\Component\\Webhook\\Controller\\WebhookController`.
Additionally, you have the flexibility to customize this URL prefix and rename it according to your preferences.

.. code-block:: yaml
# config/routes/webhook.yaml
webhook:
resource: '@FrameworkBundle/Resources/config/routing/webhook.xml'
prefix: /webhook # or possible to customize
Additionally, you must specify the parser service responsible for analyzing and parsing incoming webhooks.
It's crucial to understand that the :class:`Symfony\\Component\\Webhook\\Controller\\WebhookController` itself remains provider-agnostic, utilizing
a routing mechanism to determine which parser should handle incoming webhooks for analysis.

As mentioned earlier, incoming webhooks require a specific prefix to be directed to the :class:`Symfony\\Component\\Webhook\\Controller\\WebhookController`.
This prefix forms the initial part of the URL following the domain name.
The subsequent part of the URL, following this prefix, should correspond to the routing name chosen in your configuration.

The routing name must be unique as this is what connects the provider with your
webhook consumer code.

.. code-block:: yaml
# config/webhook.yaml
# e.g https://example.com/webhook/my_first_parser
framework:
webhook:
routing:
my_first_parser: # routing name
service: App\Webhook\ExampleRequestParser
# secret: your_secret_here # optionally
At this point in the configuration, you can also define a secret for webhooks that require one.

All parser services defined for each routing name of incoming webhooks will be injected into the :class:`Symfony\\Component\\Webhook\\Controller\\WebhookController`.


A Service Parser: Verifying and Constructing the Corresponding Remote Event
---------------------------------------------------------------------------

It's important to note that Symfony provides built-in parser services.
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.

Usage in Combination with the Mailer Component
----------------------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

When using a third-party mailer provider, you can use the Webhook component to
receive webhook calls from this provider.
Expand Down Expand Up @@ -94,8 +178,6 @@ component routing:
};
In this example, we are using ``mailer_mailgun`` as the webhook routing name.
The routing name must be unique as this is what connects the provider with your
webhook consumer code.

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

When done, add a :class:`Symfony\\Component\\RemoteEvent\\RemoteEvent` consumer
Usage in Combination with the Notifier Component
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The usage of the Webhook component when using a third-party transport in
the Notifier is very similar to the usage with the Mailer.

Currently, the following third-party SMS transports support webhooks:

============ ==========================================
SMS service Parser service name
============ ==========================================
Twilio ``notifier.webhook.request_parser.twilio``
Vonage ``notifier.webhook.request_parser.vonage``
============ ==========================================

A custom Parser
~~~~~~~~~~~~~~~

However, if your webhook, as illustrated in the example discussed, originates from a custom API,
you will need to create a parser service that extends :class:`Symfony\\Component\\Webhook\\Client\\AbstractRequestParser`.

This process can be simplified using a command:

.. code-block:: terminal
$ php bin/console make:webhook
.. tip::

Starting in `MakerBundle`_ ``v1.58.0``, you can run ``php bin/console make:webhook``
to generate the request parser and consumer files needed to create your own
Webhook.

Depending on the routing name provided to this command, which corresponds, as discussed earlier,
to the second and final part of the incoming webhook URL, the command will generate the parser service responsible for parsing your webhook.

Additionally, it allows you to specify which RequestMatcher(s) from the HttpFoundation component should be applied to the incoming webhook request.
This constitutes the initial step of your gateway process, ensuring that the format of the incoming webhook is validated before proceeding to its thorough analysis.

Furthermore, the command will create the RemoteEvent consumer class implementing the :class:`Symfony\\Component\\RemoteEvent\\Consumer\\ConsumerInterface`, which manages the remote event returned by the parser.

Moreover, this command will automatically update the previously discussed configuration with the webhook's routing name.
This ensures that not only are the parser and consumer generated, but also that the configuration is seamlessly updated::

// src/Webhook/ExampleRequestParser.php
final class ExampleRequestParser extends AbstractRequestParser
{
protected function getRequestMatcher(): RequestMatcherInterface
{
return new ChainRequestMatcher([
new IsJsonRequestMatcher(),
new MethodRequestMatcher('POST'),
new HostRequestMatcher('regex'),
new ExpressionRequestMatcher(new ExpressionLanguage(), new Expression('expression')),
new PathRequestMatcher('regex'),
new IpsRequestMatcher(['127.0.0.1']),
new PortRequestMatcher(443),
new SchemeRequestMatcher('https'),
]);
}

/**
* @throws JsonException
*/
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?RemoteEvent
{
// Adapt or replace the content of this method to fit your need.
// e.g Validate the request against $secret and/or Validate the request payload
// and/or Parse the request payload and return a RemoteEvent object or throw an exception

return new RemoteEvent(
$payload['name'],
$payload['id'],
$payload,
);
}
}


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.
Depending on the specific product and the severity of the stock outage, your application can trigger different remote events.

For instance, you might define ``HighPriorityStockRefillEvent``, ``MediumPriorityStockRefillEvent`` and ``LowPriorityStockRefillEvent``.


By implementing the :class:`Symfony\\Component\\RemoteEvent\\PayloadConverterInterface` and its :method:`Symfony\\Component\\RemoteEvent\\PayloadConverterInterface::convert` method, you can encapsulate all the business logic
involved in creating the appropriate remote event. This converter will be invoked by your parser.

For inspiration, you can refer to :class:`Symfony\\Component\\Mailer\\Bridge\\Mailgun\\RemoteEvent\\MailGunPayloadConverter`::

// src/Webhook/ExampleRequestParser.php
final class ExampleRequestParser extends AbstractRequestParser
{
protected function getRequestMatcher(): RequestMatcherInterface
{
...
}

/**
* @throws JsonException
*/
protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?RemoteEvent
{
// Adapt or replace the content of this method to fit your need.
// e.g Validate the request against $secret and/or Validate the request payload
// and/or Parse the request payload and return a RemoteEvent object or throw an exception

try {
return $this->converter->convert($content['...']);
} catch (ParseException $e) {
throw new RejectWebhookException(406, $e->getMessage(), $e);
}
}
}

// src/RemoteEvent/ExamplePayloadConverter.php
final class ExamplePayloadConverter implements PayloadConverterInterface
{
public function convert(array $payload): AbstractPriorityStockRefillEvent
{
...

if (....) {
$event = new HighPriorityStockRefillEvent($name, $payload['id]', $payload])
} elseif {
$event = new MediumPriorityStockRefillEvent($name, $payload['id]', $payload])
} else {
$event = new LowPriorityStockRefillEvent($name, $payload['id]', $payload])
}

....

return $event;
}
}

From this, we can see that the Remote Event component is highly beneficial for handling webhooks.
It enables you to convert the incoming webhook data into validated objects that can be efficiently manipulated and utilized according to your requirements.

Remote Event Consumer: Handling and Manipulating The Received Data
------------------------------------------------------------------

It is important to note that when the incoming webhook is processed by the :class:`Symfony\\Component\\Webhook\\Controller\\WebhookController`, you have the option to handle the consumption of remote events asynchronously.
Indeed, this can be configured using a bus, with the default setting pointing to the Messenger component's default bus.
For more details, refer to the :doc:`Symfony Messenger </components/messenger>` documentation


Whether the remote event is processed synchronously or asynchronously, you'll need a consumer that implements the :class:`Symfony\\Component\\RemoteEvent\\Consumer\\ConsumerInterface`.
If you used the command to set this up, it was created automatically

.. code-block:: terminal
$ php bin/console make:webhook
Otherwise, you'll need to manually add it with the ``AsRemoteEventConsumer`` attribute which will allow you to designate this class as a consumer implementing :class:`Symfony\\Component\\RemoteEvent\\Consumer\\ConsumerInterface`,
making it recognizable to the Remote Event component so it can pass the converted object to it.
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```.

In the :method:`Symfony\\Component\\RemoteEvent\\Consumer\\ConsumerInterface::consume` method,
you can access your object containing the event data that triggered the webhook, allowing you to respond appropriately.

For example, you can use Mercure to broadcast updates to clients of the hub, among other actions ...::

// src/Webhook/ExampleRequestParser.php
#[AsRemoteEventConsumer('my_first_parser')] # routing name
final class ExampleWebhookConsumer implements ConsumerInterface
{
public function __construct()
{
}

public function consume(RemoteEvent $event): void
{
// Implement your own logic here
}
}


If you are using it alongside other components that already include built-in parsers,
you will need to configure the settings (as mentioned earlier) and also create your own consumer.
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.

Usage in Combination with the Mailer Component
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

You can add a :class:`Symfony\\Component\\RemoteEvent\\RemoteEvent` consumer
to react to incoming webhooks (the webhook routing name is what connects your
class to the provider).

Expand All @@ -122,7 +389,7 @@ events::
use Symfony\Component\RemoteEvent\RemoteEvent;

#[AsRemoteEventConsumer('mailer_mailgun')]
class WebhookListener implements ConsumerInterface
class MailerWebhookConsumer implements ConsumerInterface
{
public function consume(RemoteEvent $event): void
{
Expand All @@ -148,19 +415,7 @@ events::
}

Usage in Combination with the Notifier Component
------------------------------------------------

The usage of the Webhook component when using a third-party transport in
the Notifier is very similar to the usage with the Mailer.

Currently, the following third-party SMS transports support webhooks:

============ ==========================================
SMS service Parser service name
============ ==========================================
Twilio ``notifier.webhook.request_parser.twilio``
Vonage ``notifier.webhook.request_parser.vonage``
============ ==========================================
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For SMS webhooks, react to the
:class:`Symfony\\Component\\RemoteEvent\\Event\\Sms\\SmsEvent` event::
Expand Down Expand Up @@ -189,13 +444,29 @@ For SMS webhooks, react to the
}
}

Creating a Custom Webhook
-------------------------

.. tip::
Providing Webhooks
------------------

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

For instance, you can utilize the specific :class:`Symfony\\Component\\Webhook\\Messenger\\SendWebhookMessage` and
:class:`Symfony\\Component\\Webhook\\Messenger\\SendWebhookHandler` provided to dispatch the message either synchronously or asynchronously using the Symfony Messenger component.

The SendWebhookMessage takes a :class:`Symfony\\Component\\Webhook\\Subscriber` as its first argument, which includes the destination URL and the mandatory secret.
If the secret is missing, an exception will be thrown.

As a second argument, it expects a :class:`Symfony\\Component\\RemoteEvent\\RemoteEvent` containing the webhook name, the ID, and the payload, which is the substantial information you wish to communicate.

The :class:`Symfony\\Component\\Webhook\\Messenger\\SendWebhookHandler` configures the headers, the body of the request, and finally sign the headers before making an HTTP request to the specified URL using Symfony's HttpClient component::

$subscriber = new Subscriber($urlCallback, $secret);

$event = new Event(‘name.event, ‘1’, […]);

$this->bus->dispatch(new SendWebhookMessage($subscriber, $event));


However, you also have the flexibility to define your own message, handler, or custom mechanism, and process it either synchronously or asynchronously.

.. _`MakerBundle`: https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html

0 comments on commit 38750f0

Please sign in to comment.