Slide 1

Slide 1 text

Don't call us, we will call you Fabien Potencier

Slide 2

Slide 2 text

Don't call us, we will call you Fabien Potencier

Slide 3

Slide 3 text

Don't call us, we will call you Fabien Potencier

Slide 4

Slide 4 text

https://symfony.com/components | https://symfony.com/stats/downloads Let's make it 200? That's almost 15 billion dls ~240 dl/second Full-stack PHP compat Frontend 3rd party 
 providers Most of 
 the code is here!

Slide 5

Slide 5 text

Provide fl exible low-level abstractions 
 Build higher-level abstractions Still on-going I want more of these! 
 Productivity boosts

Slide 6

Slide 6 text

RateLimiter Lock Login Throttling Cache 3.3 3.1 5.2 5.2 security: firewalls: default: # default limits to 5 login attempts per minute, # the number can be configured via "max_attempts" login_throttling: ~ # define your own RateLimiter login_throttling: limiter: login Great high-level features Throttling Login Attempts

Slide 7

Slide 7 text

Twig Great high-level features Sending Emails Serializer 2.0 + Dotenv 3.3 + Messenger 4.1 + Mime 4.3 + HttpClient 4.3 + Mailer 4.3 =>

Slide 8

Slide 8 text

Con fi guration Event Dispatcher Messenger Twig Dotenv Web Pro fi ler Speci fi c PHPUnit constraints ... An Ecocsystem with a Shared Experience It's also about the processes: 
 BC promise 
 Release cycle Security management 
 Consistency across the board ...

Slide 9

Slide 9 text

Incremental Improvements from the Community Mailer Examples ✓ More providers ✓ DKIM ✓ Draft Emails ✓ mailer:test command ✓ Tags and Metadata Currently supported by 
 MailChimp, Mailgun, Mailpace*, Postmark, 
 Sendgrid, Sendinblue, Amazon SES* The power of the community Based on real-world needs

Slide 10

Slide 10 text

Building flexible high-level abstractions 
 on top of solid low-level ones What's next?

Slide 11

Slide 11 text

Clicks, Bounces, Spam, and more…

Slide 12

Slide 12 text

email_webhooks: resource: '@MailerBundle/Resources/config/routing/webhooks.xml' prefix: /_email Webhooks: Mount the “universal” controller

Slide 13

Slide 13 text

class WebhookFailureHandler implements MessageHandlerInterface { private $logger; public function __construct(LoggerInterface $logger) { $this->logger = $logger; } public function __invoke(FailureWebhook $failure) { $this->logger->error('APP: New email failure', ['type' => $failure->getType(), 'email' => $failure->getEmail()]); } } Webhooks: Do something…

Slide 14

Slide 14 text

routing: 'Symfony\Component\Mailer\Webhook\FailureWebhook': amqp Webhooks: … sync or async Webhooks were tied 
 to Mailer Very specialized 
 webhook events

Slide 15

Slide 15 text

Time to revisit?

Slide 16

Slide 16 text

Does Webhooks handling 
 deserve to become a 
 Symfony Component? Do I have a need myself? Do I feel some pain?

Slide 17

Slide 17 text

Our Symfony Corp 
 "vanity" Slack channel It leverages several components 
 like HTTP Client, Noti fi er And some custom code 
 for Stripe Webhooks Slack support here was key 
 to creating the Notifier component Second use case for Webhooks...

Slide 18

Slide 18 text

Does Webhooks handling deserve 
 to become a Symfony Component? Are there any best practices 
 that could be abstracted and enforced? Is it generic enough to build a 
 useful and reusable abstraction? Could Symfony leverage it 
 in some components? Would it be useful 
 for many of our users?

Slide 19

Slide 19 text

Does Webhooks handling deserve 
 to become a Symfony Component? ✓ Support Mailer and Noti fi er ✓ Consume and send ✓ Replay payloads in tests ✓ Prevent production replay ✓ Promote dataless noti fi cations ✓ Check payload signatures ✓ ... Abstract and reuse Implement 
 common "best practices" Fully featured

Slide 20

Slide 20 text

Webhook Will land in 6.3 
 as an experimental component

Slide 21

Slide 21 text

A Webhook 
 is a noti fi cation 
 from one system 
 to another system 
 of some state change A successful payment in Stripe Twilio sent an SMS successfully A SaaS provider 
 or an internal system Your application It allows to react to changes from external systems What are Webhooks?

Slide 22

Slide 22 text

A system makes 
 real-time HTTP requests 
 to other servers, 
 sending events as JSON payloads Very common, but not universal language agnostic What are Webhooks? Broadcast

Slide 23

Slide 23 text

Well understood universal concept with support from many providers But not standardized Payload is freeform What are Webhooks? Security is provider dependent

Slide 24

Slide 24 text

Symfony Webhooks 
 Support in Practice

Slide 25

Slide 25 text

A Single HTTP Entry Point 
 for Consuming Webhooks webhook: resource: ../vendor/symfony/webhook/Controller/ type: attribute prefix: /webhook Prefix of 
 your choice Similar to 
 HTTP fragments 
 Web Profiler ...

Slide 26

Slide 26 text

framework: webhook: routing: emails: service: mailer.webhook_request_parser.mailgun secret: '%env(MAILGUN_WEBHOOK_SECRET)%' Webhooks Routing Your provider Similar to 
 Messenger Routing https://somwhere.com/webhook/emails

Slide 27

Slide 27 text

#[AsRemoteEventConsumer(name: 'emails')] class MailerEventConsumer implements ConsumerInterface { /** * @param AbstractMailerEvent $event */ 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()), }); } } Mailer Webhook Consumer Logic independent 
 from the provider!

Slide 28

Slide 28 text

framework: messenger: routing: Symfony\Component\RemoteEvent\Messenger\ConsumeRemoteEventMessage: async 👌Webhook Best Practice 
 Fast HTTP Response For later...

Slide 29

Slide 29 text

protected function getRequestMatcher(): RequestMatcherInterface { return new ChainRequestMatcher([ new MethodRequestMatcher('POST'), new IsJsonRequestMatcher(), ]); } 1 Incoming HTTP Request Validation return new ChainRequestMatcher([ new MethodRequestMatcher('POST'), // https://postmarkapp.com/support/article/800-ips-for-firewalls#webhooks // localhost is added for testing new IpsRequestMatcher(['3.134.147.250', '50.31.156.6', '50.31.156.77', '18.217.206.57', '127.0.0.1']), new IsJsonRequestMatcher(), ]); Enforce provider security Reuses HttpFoundation infrastructure Used by Security

Slide 30

Slide 30 text

if ( !isset($content['signature']['timestamp']) || ... ) { throw new RejectWebhookException(406, 'Payload is malformed.'); } -- private function validateSignature(array $signature, string $secret): void { if (!hash_equals($signature['signature'], hash_hmac('sha256', $signature['timestamp'].$signature['token'], $secret)) throw new RejectWebhookException(406, 'Signature is wrong.'); } } 2 Signature & Payload Validation Enforce provider security

Slide 31

Slide 31 text

final class MailgunPayloadConverter implements PayloadConverterInterface { /** @return AbstractMailerEvent */ public function convert(array $payload): Event { if (in_array($payload['event'], ['failed', 'accepted', 'rejected', 'delivered'])) { // ... } else { $event = match ($payload['event']) { 'clicked' => MailerEngagementEvent::CLICK, // ... default => throw new RejectWebhookException(406, sprintf('Not supported event "%s".', $payload['event'])) }; $wh = new MailerEngagementEvent($event, $payload['id'], $payload); } if (!$date = \DateTimeImmutable::createFromFormat('U.u', $payload['timestamp'])) { throw new RejectWebhookException(406, sprintf('Invalid date "%s".', $date)); } $wh->setRecipientEmail($payload['recipient']); $wh->setTags($payload['tags']); // ... 3 Payload Parsing & Conversion Where the standardization happens!

Slide 32

Slide 32 text

namespace Symfony\Component\RemoteEvent\Event\Mailer; abstract class AbstractMailerEvent extends Event { private \DateTimeImmutable $date; private string $email = ''; private array $metadata = []; private array $tags = []; public function getDate(): \DateTimeImmutable { return $this->date; } public function getRecipientEmail(): string { return $this->email; } public function getMetadata(): array { return $this->metadata; } public function getTags(): array { return $this->tags; } } Mailer 
 Event Supports common 
 provider features

Slide 33

Slide 33 text

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'; private string $reason = ''; public function getReason(): string { return $this->reason; } } final class MailerEngagementEvent extends AbstractMailerEvent { public const OPEN = 'open'; public const CLICK = 'click'; public const SPAM = 'spam'; public const UNSUBSCRIBE = 'unsubscribe'; } Mailer 
 Event Fined grained 
 Provider agnostic

Slide 34

Slide 34 text

Webhooks for Noti fi er? Can we do the same 
 for SMS, Chat, ...?

Slide 35

Slide 35 text

namespace Symfony\Component\RemoteEvent\Event\Sms; final class SmsEvent extends Event { public const FAILED = 'failed'; public const DELIVERED = 'delivered'; private string $sms = ''; public function getRecipientSms(): string { return $this->sms; } } SMSEvent Provider agnostic

Slide 36

Slide 36 text

#[AsRemoteEventConsumer(name: 'sms')] class SmsEventConsumer implements ConsumerInterface { public function consume(Event $event): void { $phone = $event->getRecipientSms(); error_log(match ($event->getName()) { SmsEvent::DELIVERED => sprintf('SMS delivered to %s', $phone), SmsEvent::FAILED => sprintf('SMS failed for %s', $phone), default => sprintf('Receive unhandled SMS event %s', $event->getName()), }); } } $sms = new SmsMessage('+1411111111', 'A new login was detected!'); $sms->options((new TwilioOptions())->webhookUrl('https://somewhere.com/webhook/sms')); $texter->send($sms); SMS Webhook Consumer framework: webhook: routing: sms: service: Symfony\...\TwilioRequestParser Logic independent 
 from the provider! Routing /webhook/sms Plumbing specific 
 to provider

Slide 37

Slide 37 text

🙋 I need your help ‣ Implement support for more providers ‣ Validate event names and common payload items ‣ Write tests ‣ Write documentation Next Steps? I will submit the Pull Request 
 after the 6.2 release

Slide 38

Slide 38 text

class Event { public function __construct( private string $name, private string $id, private array $payload, ){ } public function getName(): string { return $this->name; } public function getId(): string { return $this->id; } public function getPayload(): array { return $this->payload; } } name: user.new id: 1337 name: invoice.paid id: evt_1M2yqF2Gz5TJuMKBAlWnwTKe name: complained id: -Agny091SquKnsrW2NEKUA name: bounced id: 00000000-0000-0000-0000-000000000000 Your own internal 
 application Events Implement your own 
 Webhooks!

Slide 39

Slide 39 text

$subscriber = new Subscriber($userDefinedUrl, $userDefinedSecret); $event = new Event('user.new', '1', [ 'id' => 1, 'name' => 'Fabien Potencier', 'email' => '[email protected]', ]); $this->bus->dispatch(new SendWebhookMessage($subscriber, $event)); Sends the Webhook via HTTP async What about providing Webhooks to other applications? Application defined Payload

Slide 40

Slide 40 text

> POST /webhook/users HTTP/1.1 Host: 127.0.0.1:8000 X-Event: user.new X-Id: 1 X-Signature: sha256=5331fa2b02cd43616c9d8c44cd... Content-Type: application/json Accept: */* Content-Length: 22 User-Agent: Symfony HttpClient/Curl Accept-Encoding: gzip {"id":1, "name":"Fabien Potencier", ...} POST HTTP request Payload signature JSON payload HTTP Transport Abstraction Default implementation, fully customizable

Slide 41

Slide 41 text

#[AsRemoteEventConsumer(name: 'users')] class SomeEventConsumer implements ConsumerInterface { public function consume(Event $event): void { error_log('Receive an internal and generic event '.var_export($event, true)); } } Some other 
 Symfony application(s) Consumers can be anything ... including Symfony itself for SOA/micro-services/multi-apps/...

Slide 42

Slide 42 text

class Event { public function __construct( private string $name, private string $id, private array $payload, ){ } public function getName(): string { return $this->name; } public function getId(): string { return $this->id; } public function getPayload(): array { return $this->payload; } } Alternative transports: Queues for internal systems APIs ... A Webhook is just 
 one possible transport for an event - via HTTP

Slide 43

Slide 43 text

Let's decouple 
 Remote Events and Webhooks vs local events from EventDispatcher Webhook events are Remote Events Like Mime & Mailer

Slide 44

Slide 44 text

RemoteEvent Will land in 6.3 
 as an experimental component

Slide 45

Slide 45 text

namespace Symfony\Component\RemoteEvent; class Event { public function __construct( private string $name, private string $id, private array $payload, ){ } public function getName(): string { return $this->name; } public function getId(): string { return $this->id; } public function getPayload(): array { return $this->payload; } } RemoteEvent 
 in Symfony

Slide 46

Slide 46 text

Webhooks vs API? RemoteEvent Component Shared infrastructure ‣ RemoteEvent class structure ‣ Payload converters ‣ Consumer code push Webhook: A system makes real-time HTTP requests 
 to other servers, the payload is an event API: A system API endpoint that can be polled on-demand; 
 It returns all events that have occurred since last request pull Only useful if the provider supports 
 Webhooks and an API for events

Slide 47

Slide 47 text

Event API Resource waste (polling) Not real-time Webhooks vs Event API: which one to use? Many providers provide both Good providers 
 mitigate those Webhooks Uncertain security Downtime might lose events No ordering guarantees Needs some tunnel to test locally Frequent HTTP rq 
 without new events Depends on provider retry strategy Expose a web server 
 Security depends on the provider Don't call us 
 We will call you Matters sometimes

Slide 48

Slide 48 text

Webhooks vs Event API? Use both? Subscribe to real-time noti fi cations via a webhook 
 Use the API to retrieve the event That's dataless notifications

Slide 49

Slide 49 text

Webhooks vs Event API? Depends on your use case... Vanity notifications 
 on Slack on paid 
 invoices from Stripe Stripe invoices 
 for accounting Event API Webhooks

Slide 50

Slide 50 text

🙋 I need your help ‣ Payload replay for tests ‣ Logs / Audit trail ‣ Production Replay prevention ‣ Dataless noti fi cations ‣ Support for CRM, payment gateways, ...? ‣ ... Next Steps? The main concepts 
 and low-level infrastructure 
 are well-defined or wait a few years :)

Slide 51

Slide 51 text

https://symfony.com/sponsor Sponsor Symfony Thank you!