Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Efficient Webhook Management with API Platform ...

Efficient Webhook Management with API Platform and Symfony’s Webhook Component

Webhooks provide a convenient solution for businesses to notify third parties of events, but their integration can be complex.

The purpose of this talk is to explore the fundamentals of webhooks and present practical strategies for their effective management. By utilizing API Platform alongside Symfony's Webhook component, we aim to demonstrate how to seamlessly implement and optimize webhook handling for enhanced performance and integration.

a_guilhem

June 07, 2024
Tweet

More Decks by a_guilhem

Other Decks in Technology

Transcript

  1. Allison Guilhem ➔ Lead Developer at Les-Tilleuls.coop ➔ Contributor to

    Symfony - API Platform @Alli_g83 Allison E.Guilhem
  2. 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
  3. 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
  4. Callback: OpenApi 3.0 ➔ HTTP Requests in response to a

    previous HTTP request ➔ Examples: Request to generate a heavy report
  5. ➔ 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
  6. 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
  7. ➔ 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
  8. ➔ Very common in emails ➔ Automatic reminders for meetings

    ➔ Payment confirmations ➔ Package tracking notifications ➔ Stripe, Github, MailGun etc… Webhook : a truly handy mechanism
  9. ➔ 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
  10. Use case scenario API HTTP requests for stock update: registering

    a webhook for x event Notify each time an event is triggered
  11. Stock de l’ API ➔ Under the guise of an

    API Resource #[ApiResource] ➔ Well documented ➔ Queryable ➔ Manageable access based on specific permission - easily customisable
  12. 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 », …
  13. Webhook registration: endpoint #[ApiResource()] class StockWebhook { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column]

    private ?int $id = null; #[ORM\Column(length: 255)] private ?string $name = null; …. }
  14. 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
  15. 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 {}
  16. Via an attribute ➔ JSON obtained in accordance with OpenAPI

    3.1 specifications ➔ Possibility of nested resource documentation ➔ Reference to schemas
  17. ➔ Symfony 6.3 ➔ composer require symfony/remote-event ➔ composer require

    symfony/webhook Symfony Webhook Component And RemoteEvent Component
  18. Using Messenger #[ApiResource(operations: [ new Post(messenger:true, output: false, status: 202)]

    class Stock { … } <?php // api/src/Handler/StockHandler namespace App\Handler; use App\Entity\Stock; use Symfony\Component\Messenger\Attribute\A sMessageHandler; #[AsMessageHandler] final class StockHandler { public function __invoke(Stock $stock) { // do something with the resource } }
  19. 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 { … }
  20. Dispatching Webhooks Where && key Webhook Name Webhook Id Payload

    Reactions to a specific action via Messenger or a state processor etc… React && dispatch Dispatch
  21. $subscriber = new Subscriber( $urlCallback, $secret ); $event = new

    Event( ‘name.event, ‘1’, […]); $this->bus->dispatch(new SendWebhookMessage( $subscriber, $event )); Dispatching Webhooks
  22. Dispatching Webhooks Configuration headers - body - headers signature Request

    sent via Symfony HttpClient Component Fully customizable! HANDLER
  23. 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()); }
  24. 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’
  25. 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
  26. 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 } }
  27. WebhookController final class WebhookController { public function __construct( /** @var

    array<string, array{parser: RequestParserInterface, secret: string}> $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’
  28. 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
  29. #[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’
  30. Consuming Webhooks Parse and , if present, validate signature Optionally

    Base: Name - id - payload Configuration entry name
  31. 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