Fabien Potencier
Contributed by Fabien Potencier in #48542

In Symfony 6.3 we've introduced two new components called Webhook and RemoteEvent. A webhook is a notification from one system (e.g. a payment processor) to another system (e.g. your application) of some state change (e.g. some order was paid).

Many third-party mailing services provide webhook support to notify you about the different events related to emails (sent, opened, bounced, etc.) Same for notification services like SMS, which provide webhooks to notify events like message sent, sending failed, etc.

Most webhooks use standard HTTP and JSON to send their information. However, they are not standardized: security is provider-dependent and payload is free-form. That's why in Symfony 6.3, we're standardizing the webhooks of the most common mailer/notification services so your application doesn't have to deal with those internal details.

The rest of the article shows an example focused on the Mailer integration, but the same applies to the Notifier integration. Imagine that you need to log when your emails "bounce" (they haven't reached their destination) and when people unsubscribe from your emails.

If you use for example Mailgun, first you configure a webhook in that service pointing to a URL in your site (e.g. https://example.com/webhook/emails). Then, you add the following configuration in your Symfony project:

1
2
3
4
5
6
framework:
    webhook:
        routing:
            emails:
                service: '...'
                secret: '%env(MAILGUN_WEBHOOK_SECRET)%'

Finally, create the consumer of this webhook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
use Symfony\Component\RemoteEvent\Attribute\AsRemoteEventConsumer;
// ...

#[AsRemoteEventConsumer(name: 'emails')]
class MailerEventConsumer implements ConsumerInterface
{
    public function consume(Event $event): void
    {
        $email = $event->getRecipientEmail();

        error_log(match ($event->getName()) {
            MailerDeliveryEvent::BOUNCE => sprintf('Email to %s bounced (%s)', $email, $event->getReason()),
            MailerEngagementEvent::UNSUBSCRIBE => sprintf('Unsubscribe from %s', $email),
            default => sprintf('Receive unhandled email event %s', $event->getName()),
        });
    }
}

And that's all. If you change your mailer provider (in this or another project) you can reuse the exact same code for the consumer; you'll only need to update the configuration. This is possible because Symfony does the following:

  1. It runs some "request parsers" that check that the incoming payload is not malformed, contains all the needed data, verifies the signatures, etc.
  2. It runs some "payload converters" so the payload of each service is mapped into a standard payload format.

The key here is the standardization. Symfony maps the incoming payloads and events into common structures that you can use in your application to abstract from the provider details.

For example, no matter how each provider names their events. When using this feature, you only have to deal with the following common event names:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
namespace Symfony\Component\RemoteEvent\Event\Mailer;

final class MailerDeliveryEvent extends AbstractMailerEvent
{
    public const RECEIVED = 'received';
    public const DROPPED = 'dropped';
    public const DELIVERED = 'delivered';
    public const DEFERRED = 'deferred';
    public const BOUNCE = 'bounce';
}

final class MailerEngagementEvent extends AbstractMailerEvent
{
    public const OPEN = 'open';
    public const CLICK = 'click';
    public const SPAM = 'spam';
    public const UNSUBSCRIBE = 'unsubscribe';
}

Symfony 6.3 provides out-of-the-box webhook support for Mailgun, Postmark and Twilio. Now we need you, the Symfony community, to help us provide integration for the rest of mailing/notification services. Also, consider talking with your company about sponsoring Symfony components and sponsoring Symfony third-party integrations.

Published in #Living on the edge