Serverless PHP: How to implement serverless functions in PHP using OpenWhisk

In the following you will learn how to implement so called dockerized OpenWhisk actions using PHP.

Background

PHP is a server-side scripting language primarily designed for web development but also used as a general-purpose programming language. Since its birth in 1994, PHP has experienced an impressive growth and has meanwhile become one of top programming languages (e.g. according to TIOBE, PYPL, or Redmonk).

On the other side OpenWhisk is a serverless, aka Function-as-a-Service (FaaS), platform that supports a plurality of first-class programming languages like JavaScript/NodeJS, Swift, Java, and Python that can be used to implement serverless functions.

But what if you want to implement a function in a language not natively supported, e.g. because you already have a lot of code you simply want to reuse (and thus migrate) or because your developers are experts in such a language — like PHP? 
Fortunately, OpenWhisk provides a solution for this: It allows you to write your serverless functions (aka actions) by implementing them as Docker images that only need to follow a few conventions. This allows to code in (almost) any programming language!

When using dockerized actions your code is compiled into an executable binary and embedded into a Docker image. The binary program interacts with the system by taking input from stdin and replying through stdout.
While we will now focus on writing dockerized actions in PHP, you can learn more about the basic concepts of dockerized actions here.

In the following we assume that you have Docker as well as the OpenWhisk CLI properly installed locally.

Implementing your serverless function

First, let’s implement a simple serverless function (aka action) in PHP containing the business logic you want to execute in response to an event:

<?php
function main($args) {
if (array_key_exists("name", $args)) {
$greeting = "Hello " . $args["name"] . "!";
} else {
$greeting = "Hello stranger!";
}
return array(
"greeting" => $greeting
);
}
?>

If invoked without parameters the function simply responds with Hello stranger!. If invoked with the parameter name it responds with Hello <value>!, where <value> corresponds to the value associated with the parameter name.

Store the function in a file called action.php.

Implementing your router

Next, we need a so called router (aka proxy). The router is an application which implements the HTTP API used to handle platform requests, e.g. invoke the function with certain parameters.

The router needs to implement the required /init and /run routes to interact with the OpenWhisk invoker service.
The /init endpoint is called after the container is started and serves the purpose of initializing the function. It is only relevant for language-specific functions where it is used to specify the code to be executed. For dockerized actions it is most often okay to simply respond with status code 200.
The /post endpoint is called upon function invocation. Parameters are handed over via JSON under the key value.

Hence, let’s implement our router in PHP like this:

<?php
$ACTION_SRC = 'action.php';
switch ($_SERVER["REQUEST_URI"]) {
case "/init":
// Nothing to return.
header('Content-Length: 3');
echo "OK\n";
return true;
    case "/run":
// Load action code.
require $ACTION_SRC;
        // Load action params.
$post_body = file_get_contents('php://input');
$data = json_decode($post_body, true);
        // Run.
$res = main($data["value"]);
        // Return.
$res_json = json_encode($res) . "\n";
$res_json_length = strlen($res_json);
        header('Content-Length: ' . $res_json_length);
header('Content-Type: application/json');

echo $res_json;
return true;
    default:
return true;
}
?>

As part of the router’s /post endpoint implementation the function’s code is loaded — as String — from action.php. This String is then being converted to PHP code using PHP’s eval function.
Next, parameters — later on handed over via wsk action invoke --param <name> <value>— are being extracted.
Finally, the function’s main method is invoked with the extracted parameters.

Store the function in a file called router.php.

Creating and uploading the Docker image

Next, we need to create and upload our docker image containing the previously implemented function and router.

Hence, to create the image, let’s first define a Dockerfile like this:

FROM php:latest
ADD router.php /
ADD action.php /
EXPOSE 8080
CMD [ "php", "-S", "0.0.0.0:8080", "/router.php" ]

Next, build your Docker image like this:

$ docker build -t phpaction .
Sending build context to Docker daemon 4.608kB
Step 1/5 : FROM php:latest
---> b3b517804b73
Step 2/5 : ADD router.php /
---> 1ebfb70e0755
Removing intermediate container d580262a49ce
Step 3/5 : ADD action.php /
---> 302efdf6f957
Removing intermediate container 03453a054dfc
Step 4/5 : EXPOSE 8080
---> Running in af1dd11518ec
---> 43ee9b1ecc33
Removing intermediate container af1dd11518ec
Step 5/5 : CMD php -S 0.0.0.0:8080 /router.php
---> Running in bcd8abb61981
---> 45712cddd6c5
Removing intermediate container bcd8abb61981
Successfully built 45712cddd6c5
Successfully tagged phpaction:latest

Verify that your image has been succesfully created like this:

$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
phpaction latest 45712cddd6c5 About a minute ago 370MB

Currently, as a prerequisite, you also need to have a Docker Hub account to which your images can be uploaded (for the instructions that follow we assume that the Docker user ID is jon and the password doe).

To upload your image to Docher Hub proceed as follows:

First, tag your image using the image ID shown above followed by userID/repositoryname:

$ docker tag 45712cddd6c5 jon/phpaction

Then, push the image to Docker hub:

$ docker push jon/phpaction
The push refers to a repository [docker.io/jon/phpaction]
bc1b3900046a: Pushed
a5aeaa284683: Pushed
927a7e45e37f: Mounted from jon/phpaction
ff0e05b98c9a: Mounted from jon/phpaction
f9d1a1b5bd76: Mounted from jon/phpaction
0a18de4af8d9: Mounted from jon/phpaction
bfef89b95d28: Mounted from jon/phpaction
b36f60db3b2f: Mounted from jon/phpaction
8d4d1ab5ff74: Mounted from jon/phpaction
latest: digest: sha256:5c25189be2de806605bb76a3a44dd9f6ffb6c0a44a66c8f1867f6505b91c2e56 size: 2200

Creating and invoking your dockerized serverless function

Now, let’s create your dockerized function like this:

$ wsk action create --docker phpaction jon/phpaction
ok: created action phpaction

Finally, invoke it like this:

$ wsk action invoke -br phpaction
{
"greeting": "Hello stranger!"
}

Or like this:

$ wsk action invoke -br phpaction --param name Andreas
{
"greeting": "Hello Andreas!"
}

Et voila!

Please note that the latest code snippets can always be found here:
https://github.com/nauerz-ibm/openwhisk/tree/master

Please also note that this article is based on the preliminary work by Philippe Suter: https://www.slideshare.net/psuter/openwhisk-deep-dive-the-action-container-model