Handle Webhooks with Serverless PHP

Did you know you can serverless with PHP? OK, so serverless is clearly not a verb, but since I love serverless tech and have also loved PHP for longer than I'm going to admit (the dates of the earliest posts on this blog might serve as a clue), using these technologies together is definitely my idea of a good time.

I included an example of a serverless endpoint to receive incoming webhooks in my talk at PHPUK last week and I got a few questions about it so I thought I'd share that example in written form. Serverless makes a lot of sense for API endpoints in general - and also for webhook endpoints since they're essentially the same thing. Deploying individual functions to the cloud makes the endpoints independent of one another, and serverless platforms scale on demand so if the API calls or incoming webhooks all arrive at once (the reality of the internet is bursty traffic), everything will still work well.

Serverless PHP Code

If you know how to write a file containing PHP, and run it, then you're all set to work with serverless PHP. All these examples are for an Apache OpenWhisk platform - which is the project behind the IBM Cloud Functions tool that I'm using here. Not all of the serverless platforms support PHP - but any that are using OpenWhisk as their platform will do.

The basics are very, um, basic. You write a function called main() within a file called index.php. You put your PHP code in it. My webhook receiving code simply looks at the data it got, adds some metadata to indicate that this is a new webhook and to record the timestamp - then it writes that information to the database. This projects uses Cloudant on IBM Cloud as its database, which is essentially hosted Apache CouchDB and so I'm using (my own, rather alpha) PHPCouchDB library as a dependency.

Here's the code:

<?php

function main(array $params) : array
{
    $db_url = $params['cloudantURL'];

    $incoming_body = base64_decode($params['__ow_body']);
    $data = json_decode($incoming_body, true);

    echo "Saving data ...\n";
    $server = new \PHPCouchDB\Server(["url" => $db_url]);
    $db = $server->useDb(["name" => "incoming"]);

    $meta = ["received" => time(), "status" => "new"]; 
    $db->create(["data" => $data, "meta" => $meta]);
    return ["body" => "OK"];
}

This action is intended to be used with --web raw which you'll see in a moment. This setting instructs OpenWhisk not to parse the incoming arguments and lets us parse them ourselves by base64 decoding the body data, and then JSON decoding it (probably with more error checking than I have here!). To get the values parsed for you, use --web true. Personally I prefer the raw version so that I know where the values came from and can also log the body for debugging if I need to.

This code saves the data to CouchDB (called "Cloudant" on IBM Cloud), using a PHP CouchDB library that I'm developing (so it's awesome but somewhat incomplete at this point). CouchDB is a document database so it's more than happy to store my nested JSON data, let me search on it, and so on. It's also great for this use case because it has a feed of database changes so I can write to the database and acknowledge the incoming webhook - and have a separate process react to that new database record and do whatever processing is required.

Deploying a Serverless PHP Function

Since I'm deploying to IBM Cloud, my commands start with bx wsk but for other Apache OpenWhisk installations the command will start with wsk - after this, the commands are identical.

Since I have the PHPCouchDB dependency in my code, the first thing I need to do is zip up all the code that my app needs: in this case it's index.php where the code lives, plus the vendor/ folder. The command looks like this:

zip -r hook.zip index.php vendor

(you might like to add -q as well to suppress the chatty output from this command)

Now we'll deploy that zip file that we created as our new function. I'll name the function hook and deploy it like this:

bx wsk action update hook --kind php:7.1 --web raw hook.zip

OpenWhisk will quietly create the action even though this is technically the update command, which is quite nice, it means the same commands always work regardless of the current state of the action.

This command says: "update the hook action" and specifies that we should use PHP 7.1 to run the code. It also enables web access for this action but by setting the --web raw field we tell openwhisk not to try decoding the values automatically.

This action expects to find settings in the incoming function parameter, so let's set those too:

bx action update hook --param cloudantURL https://username:[email protected]

Sending Webhooks to the Serverless PHP Function

Once the function is created, it already has a URL we can use because we set the --web flag, so let's find out what that is:

bx wsk action get hook --url

This returns the URL which we can copy and then use. For example, you could pass it to a GitHub webhook setup form, or use it with Zapier. Since I'm old school, I'll just use curl.

curl -X POST -H "Content-Type: application/json" [URL from before] -d '{"user":"Lorna", "message": "I love code"}'

Since all our original function from the start of this post does is write incoming data to the database, we can send whatever we like. Hopefully this has given you a starting point for all the moving parts you need to create your own HTTP endpoint out of serverless PHP. The code is taken from a wider sample app which you can find here: https://github.com/ibm-watson-data-lab/guestbookIf you have questions, comments or examples to share, I'd love to see this in the comments!

Leave a Reply

Please use [code] and [/code] around any source code you wish to share.