Giter VIP home page Giter VIP logo

receiver's Introduction

Receiver

Receiver

Receiver is a drop-in webhook handling library for Laravel.

Webhooks are a powerful part of any API lifecycle. Receiver aims to make handling incoming webhooks in your Laravel app a consistent and easy process.

Out of the box, Receiver has built in support for:

Of course, Receiver can receive webhooks from any source using custom providers.

Tests Latest Version on Packagist PHP from Packagist

Table of Contents

Installation

Requires:

  • PHP ^8.1
  • Laravel 9+
composer require hotmeteor/receiver

Optional:

Stripe support requires stripe/stripe-php

Receiving Webhooks

The Basics

Webhooks require an exposed endpoint to POST to. Receiver aims to make this a one-time setup that supports any of your incoming webhooks.

  1. Create a controller and route for the webhooks you expect to receive.
  2. Receive the webhook and handle it, as necessary:
    <?php
    
    namespace App\Http\Controllers\Webhooks;
    
    use App\Http\Controllers\Controller;
    use Illuminate\Http\Request;
    
    class WebhooksController extends Controller
    {
       public function store(Request $request)
       {
           return Receiver::driver('slack')
               ->receive($request)
               ->ok();
       }
    }

The methods being used are simple:

  • Define the driver that should process the webhook
  • receive the request for handling
  • Respond back to the sender with a 200 ok response

Receiving from multiple apps

Maybe you have webhooks coming in from multiple services -- handle them all from one controller with a driver variable from your route.

<?php

namespace App\Http\Controllers\Webhooks;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class WebhooksController extends Controller
{
   public function store(Request $request, string $driver)
   {
       return Receiver::driver($driver)
           ->receive($request)
           ->ok();
   }
}

The provided ReceivesWebhooks trait will take care of this for you.

<?php

namespace App\Http\Controllers\Webhooks;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Receiver\ReceivesWebhooks;

class WebhooksController extends Controller
{
   use ReceivesWebhooks;
}

Note: you'll still need to create the route to this action. Example:

Route::post('/hooks/{driver}', [\App\Http\Controllers\Webhooks\WebhooksController::class, 'store'])
    ->withoutMiddleware(\App\Http\Middleware\VerifyCsrfToken::class);

Advanced Usage

Fallbacks

Receiver allows you to safely handle webhooks for events you do not handle. Add a fallback method before ok โ€“ it takes a callback that is passed the webhook object.

<?php

namespace App\Http\Controllers\Webhooks;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Receiver\Providers\Webhook;

class WebhooksController extends Controller
{
   public function store(Request $request, string $driver)
   {
       return Receiver::driver($driver)
           ->receive($request)
           ->fallback(function(Webhook $webhook) {
               // Do whatever you like here...
           })
           ->ok();
   }
}

Handling Webhooks

The Basics

Now that webhooks are being received they need to be handled. Receiver will look for designated Handler classes for each event type that comes in in the App\Http\Handlers\[Driver] namespace. Receiver does not provide these handlers -- they are up to you to provide as needed. If Receiver doesn't find a matching handler it simplies ignores the event and responds with a 200 status code.

For example, a Stripe webhook handler would be App\Http\Handlers\Stripe\CustomerCreated for the incoming customer.created event.

Each handler is constructed with the event (name of the webhook event) and data properties.

Each handler must also use the Dispatchable trait.

<?php

namespace App\Http\Handlers\Stripe;

use Illuminate\Foundation\Bus\Dispatchable;

class CustomerCreated
{
    use Dispatchable;
    
    public function __construct(public string $event, public array $data)
    {
    }

    public function handle()
    {
        // Your code here
    }
}

Queueing Handlers

Of course, since your app may be receiving a lot of webhooks it might be better practice to queue these handlers. That way your app can efficiently respond back to the service that the webhook was received and requests aren't being blocked as events are handled.

Receiver attempts to dispatch every handled event, so queueing handlers is simply a matter of setting them up like any Laravel queued job:

<?php

namespace App\Http\Handlers\Stripe;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class CustomerCreated implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;
    use SerializesModels;

    public function __construct(public string $event, public array $data)
    {
    }

    public function handle()
    {
        // Your code here
    }
}

Extending Receiver

As mentioned previously, Receiver can handle webhooks from any source. Even though there are a few providers distributed with the package, Receiver can easily be extended to work with other apps.

Adding Custom Providers

The easiest way to add a new provider is to use the included Artisan command:

php artisan receiver:make <name>

This command will generate a new provider with the name you defined. This class will be created in the App\Http\Receivers namespace.

If your provider needs to be able to verify webhook signatures simply add the --verified flag to the command:

php artisan receiver:make <name> --verified

Once you've created your new provider you can simply extend Receiver in your AppServiceProvider so that Receiver can use it:

<?php

namespace App\Providers;

use App\Http\Receivers\MailchimpProvider;
use App\Http\Receivers\MailgunProvider;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // 
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        $receiver = app('receiver');

        $receiver->extend('mailchimp', function ($app) {
            return new MailchimpProvider(
                config('services.mailchimp.webhook_secret')
            );
        });
        
        $receiver->extend('mailgun', function ($app) {
            return new MailgunProvider(
                config('services.mailgun.webhook_secret')
            );
        });
    }
}

Defining Attributes

Receiver needs two pieces of information to receive and handle webhook events:

  • The event name
  • The event data

Since these are found in different attributes or headers depending on the webhook, Receiver makes it simple ways to define them in your provider.

<?php

namespace Receiver\Providers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class CustomProvider extends AbstractProvider
{
    /**
     * @param Request $request
     * @return string
     */
    public function getEvent(Request $request): string
    {
        return $request->input('event.name');
    }
    
    /**
     * @param Request $request
     * @return array
     */
    public function getData(Request $request): array
    {
        return $request->all();
    }
}

The getEvent() method is used to return the name of the webhook event, ie. customer.created.

The getData() method is used to return the payload of data that can be used within your handler. By default this is set to $request->all().

Securing Webhooks

Many webhooks have ways of verifying their authenticity as they are received, most commonly through signatures or basic authentication. No matter the strategy, Receiver allows you to write custom verification code as necessary. Simply implement the verify method in your provider and return true or false if it passes.

A false return will result in a 401 response being returned to the webhook sender.

<?php

namespace Receiver\Providers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class CustomProvider extends AbstractProvider
{
    public function verify(Request $request): bool
    {
        // return result of verification
    }
}

Handshakes

Some webhooks want to perform a "handshake" to check if your endpoint exists and returns a valid response when it's first set up. To facilitate this, implement the handshake method in your provider:

<?php

namespace Receiver\Providers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class CustomProvider extends AbstractProvider
{
    public function handshake(Request $request): array
    {
        // return result of handshake
    }
}

Unlike the verify method, handshake expects an array to be returned, since many times the webhook sender is expecting a specific "challenge" response. The return of the handshake method is sent back to the webhook sender.

Share your Receivers!

Have you created a custom Receiver? Share it with the community in our Receivers Discussion topic!

Credits

Made with contributors-img.

License

The MIT License (MIT). Please see License File for more information.

receiver's People

Contributors

amiranagram avatar chrishardie avatar ewa-podolak avatar garret-gunter-membersy avatar hmaesta avatar hotmeteor avatar jankremlacek avatar xolf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

receiver's Issues

Authenticate postmark webhooks by IP

Is your feature request related to a problem? Please describe.
For the Postmark provider it (annoyingly) authenticates the webhook using one of my app's users via HTTP basic auth. It's kinda silly to have to setup a special user in my app just to authenticate the webhooks. Then I would want to add logic to my login page's controller to exclude the possibility of logging in as that user on the main website for if those credentials where somehow leaked.

Describe the solution you'd like
In my opinion it makes much more sense to authenticate the webhook by IP address instead of the current method. And maybe add a config value to add additional IP's such as localhost or other special purpose IP's for testing.

Describe alternatives you've considered
I have extended receiver with my own Postmark provider to accomplish this for now.

Additional context

Multiple events in single request

I have been evaluating if this package would work with Xero webhooks.

One thing I would like to know is if there is a way in a custom provider to handle multiple events in the one request. This is how Xero handles webhooks by sending an array of events that you then loop through.

Example payload:

{
   "events": [
      {
         "resourceUrl": "https://api.xero.com/api.xro/2.0/Contacts/717f2bfc-c6d4-41fd-b238-3f2f0c0cf777",
         "resourceId": "717f2bfc-c6d4-41fd-b238-3f2f0c0cf777",
         "eventDateUtc": "2017-06-21T01:15:39.902",
         "eventType": "Update",
         "eventCategory": "CONTACT",
         "tenantId": "c2cc9b6e-9458-4c7d-93cc-f02b81b0594f",
         "tenantType": "ORGANISATION"
      }
   ],
   "lastEventSequence": 1,
   "firstEventSequence": 1,
   "entropy": "S0m3r4Nd0mt3xt"

}

Support for fallback handlers when event-specific class doesn't exist

Thanks for this great package. I notice that right now, for any given driver if a webhook is sent with an event/topic that doesn't map to a handler class in that driver's namespace, the code just quietly finishes and returns success.

I wonder if it would be helpful to allow the definition of a fallback class that would be used when no event-specific class is available?

I could imagine implementing this by hardcoding a class name that would be used (e.g. "NoEventHandler.php") or by allowing users of the package to define a fallback class in the receiver definition, e.g. through getFallbackClass(): string or similar.

Stripe Driver Not Recognized?

I've installed the latest version running on Laravel 9 and PHP 8.1.

I'm getting this error message when trying to use the stripe driver:

InvalidArgumentException: Driver [stripe] not supported.

Not sure what I'm doing wrong or if this is a bug.

My implementation:

<?php

namespace App\Http\Controllers\Webhooks;

use Illuminate\Http\Request;
use Receiver\Facades\Receiver;
use App\Http\Controllers\Controller;

class WebhooksController extends Controller
{
    public function store(Request $request)
    {
        Receiver::driver('stripe')
            ->receive($request)
            ->ok();
    }
}

Unclear documentation

In you README, it states:

Simply implement the verify method in your provider and return true or false if it passes.

Likewise, it states:

To facilitate this, implement the handshake method in your provider

However, both of the examples for these two methods show them being on the controller, rather than the provider.

If both of these methods are on the controller, it doesn't make it possible to group the verification and handshake with the provider itself. In any case, the documentation is confusing.

Breaking change in v0.1.15, classes not constructed the same?

The change in v0.1.15 introduced in #18 created a fatal error for me when deployed to production. It may be my code, but here's what I saw:

The webhook event handling stopped happening synchronously, which was succeeding, and was moved to a queued job, which failed.

The code that ran to handle the webhook event is

<?php

namespace App\Http\Handlers\Woocommerce;

class UserCreated extends AbstractUserEvent
{
    public function handle()
    {
        $this->createContactFromUser();
    }
}

and the parent class has this construction:

<?php

namespace App\Http\Handlers\Woocommerce;

use App\Data\WoocommerceCustomer;
use App\Data\WordpressUser;
use App\Helpers\WoocommerceHelpers;
use App\Models\Contact;
use Illuminate\Support\Facades\Log;

abstract class AbstractUserEvent extends AbstractWoocommerceEvent
{
    private WoocommerceHelpers $wooHelper;

    public function __construct(public string $event, public array $data)
    {
        parent::__construct($event, $data);

        // Create a Woo customer object we can work with.
        $this->wooHelper = new WoocommerceHelpers();

        ...
    }

    protected function createContactFromUser(): Contact|null
    {
        $newContact = $this->wooHelper->createLocalFromWpUser($this->user);
        return $newContact;
    }

    ...
}

The error I started getting was:

local.ERROR: Typed property App\Http\Handlers\Woocommerce\AbstractCustomerEvent::$wooHelper must not be accessed before initialization {"exception":"[object] (Error(code: 0): Typed property App\\Http\\Handlers\\Woocommerce\\AbstractCustomerEvent::$wooHelper must not be accessed before initialization at <path>/app/Http/Handlers/Woocommerce/AbstractCustomerEvent.php:51)

So it appears that the __construct method in AbstractUserEvent is not run following this release. Downgrading to 0.1.14 fixed it.

Please let me know if there's any other information that would be helpful to understand or advise on what's happening. Thank you.

Best way to validate $driver string in a request?

Short version: is there a recommended method or function for determining if a $driver string specified in a request is valid?

Longer version: I have a service sending webhooks that wants to get a 200 OK back from a GET request to the webhook endpoint before it will activate the webhook. It doesn't use the "challenge" approach so I don't think the handshake method is going to work here. I'm adding a simple method in the controller that will be reached by a GET route, but I'd like to return 404 when the driver specified isn't valid, as a basic check against typos or an invalid driver being otherwise specified.

It looks like right now this package just relies on the illuminate/support package to throw an exception when the driver doesn't exist, but maybe there's a way to check that more intentionally? Something like Receiver::driverExists($driver);?

Thanks for any suggestions you can offer.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.