Slide 1

Slide 1 text

E ffi cient Webhook Management with API Platform and Symfony’s Webhook Component

Slide 2

Slide 2 text

Allison Guilhem ➔ Lead Developer at Les-Tilleuls.coop ➔ Contributor to Symfony - API Platform @Alli_g83 Allison E.Guilhem

Slide 3

Slide 3 text

1. What is a Webhook Efficient Webhook Management with API Platform and Symfony Webhook Component 2. A standardized management of webhooks via a use case scenario

Slide 4

Slide 4 text

Webhook ➔ A HTTP based Callback function ➔ A request from one system to another to notify that something has occurred ➔ Real-time notifications between applications ➔ Webhooks can indirectly affect the browser by using the receiving server to update the browser: Mercure, SSE, Websockets etc… ➔ API? Reverse API? Event-driven Api? ➔ Pull Architecture API - Push Architecture API

Slide 5

Slide 5 text

Webhook

Slide 6

Slide 6 text

Webhook - Callback

Slide 7

Slide 7 text

Callback: OpenApi 3.0 ➔ HTTP Requests in response to a previous HTTP request ➔ Examples: Request to generate a heavy report

Slide 8

Slide 8 text

Webhook - Callback

Slide 9

Slide 9 text

➔ It behaves similarly to the callback described in OpenAPI 3.0 and fits within a Path ➔ It doesn‘t necessarily stem from a previous HTTP request but rather from a triggered event ➔ It describes HTTP requests in both directions independently ➔ A callback in OpenAPI is a type of Webhook but not every Webhook fits the specific definition outlined in OpenAPI 3.0 Webhook - Callback

Slide 10

Slide 10 text

Webhook Independence: The Recognition

Slide 11

Slide 11 text

OpenApi 3.1 ➔ New Top-Level Element « webhooks » to describe the Webhooks that are registered and managed out- of-band webhooks: newPet: post: requestBody: description: Information about a new pet in the system content: application/json: schema: $ref: "#/components/schemas/Pet" responses: "200": description: Return a 200 status to indicate that the data was received successfully

Slide 12

Slide 12 text

➔ Registering the Webhook (endpoint) ➔ Triggering an Event ➔ Payload: data from the triggering event delivered to your webhook once the event is triggered ➔ Payload URL: the location where the payload will be sent Webhook: 4 important points

Slide 13

Slide 13 text

➔ Very common in emails ➔ Automatic reminders for meetings ➔ Payment confirmations ➔ Package tracking notifications ➔ Stripe, Github, MailGun etc… Webhook : a truly handy mechanism

Slide 14

Slide 14 text

A standardized management of webhooks via a use case scenario

Slide 15

Slide 15 text

➔ API handling inventory management ➔ Client applications requiring updates on certain stock changes (e.g stock depletion, updates) ➔ Polling technique has shown its limitations Use case scenario

Slide 16

Slide 16 text

Use case scenario API HTTP requests for stock update: registering a webhook for x event Notify each time an event is triggered

Slide 17

Slide 17 text

Stock de l’ API ➔ Under the guise of an API Resource #[ApiResource] ➔ Well documented ➔ Queryable ➔ Manageable access based on specific permission - easily customisable

Slide 18

Slide 18 text

Webhook Registration ➔ Webhook Registration ➔ Post request to an endpoint or via an interface { "name": « a cute name », "url": « something/here », « content_type": « a content. Type », "signature": « something", "events": [ « out_of_ stock_event», « add_stock_ event» ], "status": « enabled », …

Slide 19

Slide 19 text

Webhook registration: endpoint #[ApiResource()] class StockWebhook { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\Column(length: 255)] private ?string $name = null; …. }

Slide 20

Slide 20 text

What about the Webhook Documentation?

Slide 21

Slide 21 text

class OpenApiFactory implements OpenApiFactoryInterface { public function __construct(private OpenApiFactoryInterface $decorated){} public function __invoke(array $context = []): OpenApi { $openApi = $this->decorated->__invoke($context); $pathItem = $openApi->getPaths()->getPath(‘/api/stock_webhook'); $paths = $openApi->getPaths()->getPaths(); $filteredPaths = new Paths(); foreach ($paths as $path => $pathItem) { if ($path === '/api/stock_webhook') { continue; } $filteredPaths->addPath($path, $pathItem); } $openApi = $openApi->withPaths($filteredPaths); $webhooks = new \ArrayObject(); $webhooks['test'] = $pathItem; return new OpenApi( $openApi->getInfo(), $openApi->getServers(), $openApi->getPaths(), $openApi->getComponents(), $openApi->getSecurity(), [], null, null, $webhooks ); } ➔ Via the decoration mechanism

Slide 22

Slide 22 text

Via an attribute

Slide 23

Slide 23 text

Via an attribute #[ApiResource(operations: [ new Post(openapi: new Webhook( name:’a cute name’, pathItem: new PathItem( post: new Operation( summary: 'Something here', description: 'Something else here for example', )))) ])] class OutOfStockEvent {} #[ApiResource(operations: [ new Post(openapi: new Webhook(name:’a cute name’)) ])] class OutOfStockEvent {}

Slide 24

Slide 24 text

Via an attribute ➔ JSON obtained in accordance with OpenAPI 3.1 specifications ➔ Possibility of nested resource documentation ➔ Reference to schemas

Slide 25

Slide 25 text

Let's dispatch our Webhook

Slide 26

Slide 26 text

➔ Symfony 6.3 ➔ composer require symfony/remote-event ➔ composer require symfony/webhook Symfony Webhook Component And RemoteEvent Component

Slide 27

Slide 27 text

Using Messenger #[ApiResource(operations: [ new Post(messenger:true, output: false, status: 202)] class Stock { … }

Slide 28

Slide 28 text

State processors class StockPostProcessor implements ProcessorInterface { public function __construct( #[Autowire(service: ‘api_platform.doctrine.orm.state.persist_pr ocessor’] private ProcessorInterface $persistProcessor) {} /** {@inheritDoc} */ public function process($data, Operation $operation, array $uriVariables = [], array $context = []) { // what you need + dispatch the webhook } } #[Post(processor: StockPostProcessor::class)] class Stock { … }

Slide 29

Slide 29 text

Dispatching Webhooks Where && key Webhook Name Webhook Id Payload Reactions to a specific action via Messenger or a state processor etc… React && dispatch Dispatch

Slide 30

Slide 30 text

$subscriber = new Subscriber( $urlCallback, $secret ); $event = new Event( ‘name.event, ‘1’, […]); $this->bus->dispatch(new SendWebhookMessage( $subscriber, $event )); Dispatching Webhooks

Slide 31

Slide 31 text

Dispatching Webhooks Configuration headers - body - headers signature Request sent via Symfony HttpClient Component Fully customizable! HANDLER

Slide 32

Slide 32 text

Dispatching Webhooks public function send(Subscriber $subscriber, RemoteEvent $event): void { $options = new HttpOptions(); $this->headers->configure($event, $subscriber->getSecret(), $options); $this->body->configure($event, $subscriber->getSecret(), $options); $this->signer->configure($event, $subscriber->getSecret(), $options); $this->client->request('POST', $subscriber->getUrl(), $options- >toArray()); }

Slide 33

Slide 33 text

Let‘s consume our Webhook Retrieve Verify Process

Slide 34

Slide 34 text

Consuming Webhooks ➔ Single entry point: WebhookController ➔ Prefix of your choice ➔ A routing mechanism ➔ Built-in parsers (Sendgrid - mailjet - mailgun - brevo - Twilio- - vonage etc…) webhook: resource: '@FrameworkBundle/Resources/config/ routing/webhook.xml' prefix: /webhook framework: webhook: routing: email: # path: /webhook/email service: ‘mailer.webhook.request_parser.postmark‘ secret: ‘%env(POSTMARK_WEBHOOK_SECRET’

Slide 35

Slide 35 text

Consuming Webhooks - built-in mechanisms example Parse and , if present, validate signature Optionally as for MailGun or Mailjet - SMSEvent - MailerDeliveryEvent - MailerEngement Event Base: Name - id - payload

Slide 36

Slide 36 text

Custom parsers: php bin/console make:webhook (makerBundler v1.58.0) final class CustomRequestParser extends AbstractRequestParser { protected function getRequestMatcher(): RequestMatcherInterface { return new ChainRequestMatcher([…]); } /** * @throws JsonException */ protected function doParse(Request $request, #[\SensitiveParameter] string $secret): ?RemoteEvent { // TODO: Adapt or replace the content of this method to fit your need. return new RemoteEvent( $payload['name'], $payload['id'], $payload, ); } } #[AsRemoteEventConsumer(‘custom')] final class CustomWebhookConsumer implements ConsumerInterface { public function __construct() { } public function consume(RemoteEvent $event): void { // Implement your own logic here } }

Slide 37

Slide 37 text

WebhookController final class WebhookController { public function __construct( /** @var array $parsers */ private readonly array $parsers, private readonly MessageBusInterface $bus, ) { } public function handle(string $type, Request $request): Response { if (!isset($this->parsers[$type])) { return new Response('No webhook parser found for the type given in the URL.', 404, ['Content-Type' => 'text/plain']); } // call the parser and return the remoteEvent $this->bus->dispatch(new ConsumeRemoteEventMessage($type, $event)); return $parser->createSuccessfulResponse(); } } framework: webhook: routing: custom: # path: /webhook/custom service: App\Service\MyCustomServiceParser secret: ‘mysecret’

Slide 38

Slide 38 text

Parsers final class CustomRequestParser 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'), ]); } ➔ Parse method will validate the request with requestMatchers + « substantially » parse the request

Slide 39

Slide 39 text

#[AsRemoteEventConsumer(‘custom')] final class CustomWebhookConsumer implements ConsumerInterface { public function __construct() { } public function consume(RemoteEvent $event): void { // Implement your own logic here } } Consuming Webhooks framework: webhook: routing: custom: # path: /webhook/custom service: App\Service\MyCustomServiceParser secret: ‘mysecret’

Slide 40

Slide 40 text

Consuming Webhooks Parse and , if present, validate signature Optionally Base: Name - id - payload Configuration entry name

Slide 41

Slide 41 text

So … in the end … To call, or to be called, that is the question: Sometimes, register and await, no need for haste, But when you wait, ensure your team stands tall, With symfony Webhook and API Platform 's embrace. Standardization, security, a fortress's wall, In good management, find your trusted guide. Clear documentation, a beacon for all, With these, success shall never subside

Slide 42

Slide 42 text

Thank you! les-tilleuls.coop [email protected] @coopTilleuls