Serverless computing, also known as functions as a service (FaaS), allows developers to host code snippets in the cloud and execute them in response to external events or triggers. This approach offers a few important advantages. It lets you slice and dice your code into small, decoupled functions that each serve a distinct purpose, resulting in more streamlined architecture. It often lowers costs because although it’s still pay-per-use, you’re only paying for the execution time and memory consumed by your functions and not for any associated storage or networking costs. And finally, it’s very scalable, making it possible to support higher workloads easily.

I’ve been developing applications for the cloud for many years now, and one of the most frequent questions I’ve heard from clients is, “We’re happy to pay for the server, but who’s going to maintain it?” It’s easy to spin up a cloud server or cluster for an application, but keeping those servers up-to-date and secure is less easy. I started looking into serverless applications as a potential solution, because they have almost no administrative overhead related to server hosting or maintenance, and they can easily scale up (or down) as needed.

This beginner tutorial uses IBM Cloud Functions. I chose it for developing serverless applications, because it is based on Apache OpenWhisk and it comes with all the necessary tools and support for my favorite programming language, PHP (in addition to Python, Ruby, Swift, Node.js, Go, Java, .NET and Docker).

It took me some time, and quite a few experiments, to understand how to use PHP to develop serverless applications (and I’m still learning). I hope to save you that time and effort, by walking you through some common use cases I encountered when using serverless PHP code with Cloud Functions.

Learning objectives

This tutorial is suitable for intermediate PHP developers who are interested in learning about serverless PHP, or transitioning existing PHP applications to the IBM Cloud Functions platform. After completing this tutorial, you’ll know how to complete the following tasks:

  • Use the IBM Cloud Functions CLI to provision new PHP actions.
  • Invoke PHP actions over HTTP.
  • Integrate PHP actions with third-party REST APIs and IBM Cloud services.
  • Automatically execute PHP actions on a pre-defined schedule.

Prerequisites

Before starting, make sure you have the following tools and skills:

NOTE: Any application that uses the Cloudant service and IBM Cloud Functions must comply with the corresponding Terms of Service at IBM Cloud Terms and Notices. Some examples in this article also make use of third-party APIs and services, such as OpenWeatherMap and Slack, and your use of these APIs and services are subject to separate terms of use, attribution requirements, quotas and rate limits.

Estimated time

It should take you approximately two hours to complete this tutorial.

Steps

This tutorial covers the following steps:

  1. Provision and invoke a simple PHP action.
  2. Use parameters with actions.
  3. Integrate actions with third-party APIs.
  4. Read and write data using a Cloudant database on IBM Cloud.
  5. Use sequences to chain actions together.
  6. Automatically invoke actions.
  7. Build an example application.

This article covers a number of different broad topics, which cannot all be covered in depth in a single tutorial. You are encouraged to learn more by clicking the links provided in each section.

Step 1: Provision and invoke a simple PHP action

Slim is a lightweight PHP framework that adds structure to your code and makes it easy to integrate other components and libraries. In this tutorial, you use the Slim micro-framework as the basis for the application.

  1. Create a simple PHP action by placing the code below in a text file in your working directory. Name the file info.php:

     <?php
    
     function main(array $args) : array
     {
       return ['version' => phpversion()];
     }
    
  2. Provision the new action using the following command:

     ibmcloud fn action create mypackage/info info.php --web true
    

    Packages provide a way to organize actions into related groups. The previous command creates a package named mypackage and, within it, an action named info. The action code is retrieved from the info.php file. The additional --web parameter makes the action available for invocation at an HTTP URL. Learn more about packages at Organizing actions in packages.

  3. On the IBM Cloud Functions dashboard, select the Actions menu and then select the newly-added action from the action list. On the resulting page, select the Endpoints section to obtain the HTTP URL for the action:

    Screen capture of the Cloud Functions Web Action page

    You can also obtain the base URL with the following command. At invocation time, add .json (for example) to the retrieved URL as a suffix to indicate the desired content type for the response:

     ibmcloud fn action get mypackage/info --url
    
  4. Test the action by using the RESTer extension, cURL, or a similar tool to send a GET request to the action’s HTTP URL. Confirm that you see a JSON document listing the PHP version, as shown in the following output:

     {
       "version": "7.3.0"
     }
    

    The following image displays the same request and response in RESTer:

    Screen capture of RESTer

    You can also invoke the action using the command line tool:

     ibmcloud fn action invoke --result mypackage/info –result
    

    Find out more about actions at Managing actions and see another basic PHP “Hello world” example in the OpenWhisk documentation.

Step 2: Use parameters with actions

Functions like the one in the previous section are good for illustration, but they have little practical utility. Most of the time, you need your functions to accept one or more input arguments and perform calculations or processing on those inputs.

  1. To see how to use parameters with actions, create a file named convert-temp.php with the following code:

     <?php
     function main(array $args) : array
     {
       if (!isset($args['temp_c'])) {
           throw new Exception("Input temperature (Celsius) not supplied");
       }
       $temp_c = $args['temp_c'];
       $temp_f = ($temp_c * 9/5) + 32;
       return ['temp_c' => $temp_c, 'temp_f' => $temp_f];
     }
    

    This is a simple function that accepts an input temperature in Celsius and returns the corresponding value in Fahrenheit. Notice the $args array in the function signature. This array holds the input provided to the function. You can provide the input through the command-line tool, the Cloud Functions dashboard or, if the function is enabled for Web access, as part of the HTTP request.

  2. Provision this function as a PHP action:

     ibmcloud fn action create mypackage/convert-temp convert-temp.php --web true
    
  3. Invoke the action with the required input:

     ibmcloud fn action invoke mypackage/convert-temp --param temp_c 38 --result
    

    You should receive a response with the converted value, as shown in the following output:

     {
       "temp_c": "38",
       "temp_f": 100.4
     }
    

    If you decide to invoke the function over HTTP, for example with a request to http://ACTION-URL?temp_c=38, the request and response is displayed, as shown in the following screen capture:

    Screen capture of RESTer

  4. You can also define default parameters for your action with the –param keyword. Define a default parameter for the previous PHP action by executing the following command:

     ibmcloud fn action update mypackage/convert-temp convert-temp.php --web true --param temp_c 1
    
  5. Generate an HTTP request for the action endpoint, this time without providing any input. In this case, the default temperature is used, and you should receive the following response:

     {
       "temp_c": "1",
       "temp_f": 33.8
     }
    

    Note that invoking an action with parameters through the CLI when the action already has default parameters assigned results in an error. In this case, it is necessary to either update the action to remove the default parameter or invoke the action using its HTTP endpoint instead. Find out more about working with parameters in the OpenWhisk documentation at apache/incubator-openwhisk.

Step 3: Integrate actions with third-party APIs

One of the coolest things about PHP is Composer, a dependency manager for PHP. Composer makes it easy to plug components and libraries into your PHP application, allowing you to add new functionality with minimal effort.

The PHP runtime on IBM Cloud Functions ships with two Composer packages by default: guzzlehttp/guzzle and ramsey/uuid. Guzzle is an HTTP client written in PHP that makes it easy to integrate your PHP actions with third-party APIs. Learn more about the built-in Composer packages at PHP actions.

A trivial example is to build a PHP function that accepts a sentence as input and returns it in “Yoda-speak”, using the free Yoda Translator API through Guzzle. This public API is rate-limited to 60 API calls a day and 5 API calls an hour.

  1. Create a file named translate.php with the following PHP code:

     <?php
     use GuzzleHttp\Client;
     use GuzzleHttp\Exception\ClientException;
    
     function main(array $args) : array
     {
       // check for input text
       if (!isset($args['text'])) {
         throw new Exception('No input provided');
       }
    
       // create an HTTP client
       $c = new Client([
         'base_uri' => 'https://api.funtranslations.com',
         'timeout'  => 6000,
       ]);
    
       // generate POST request and return output
       $response = $c->post('/translate/yoda.json', ['body' => json_encode(['text' => $args['text']])]);
       $data = json_decode($response->getBody());
    
       return [$data];
     }
    

    Internally, this function uses Guzzle to generate a POST request to the API endpoint and returns the response to the caller.

  2. Provision the PHP action using the following command:

     ibmcloud fn action create mypackage/translate translate.php --web true
    
  3. Invoke the action:

     ibmcloud fn action invoke mypackage/translate --result --param text "You must go far away"
    

    You should see something like the following response:

     {
         "0": {
             "contents": {
                 "text": "You must go far away",
                 "translated": "Far away, you must go",
                 "translation": "yoda"
             },
             "success": {
                 "total": 1
             }
         }
     }
    

Step 4: Read and write data using a Cloudant database on IBM Cloud

If you’re interested in reading and writing from or to persistent data storage, Cloud Functions comes with ready-made functionality to integrate with the Cloudant database service. Cloudant is a document-oriented database that offers an HTTP REST API to create, modify, and delete databases and documents.

  1. To see how this works, log in to your IBM Cloud account and, from the service catalog, under Databases, select the Cloudant service. Review the description of the service and click Create to start it. Select IAM as the authentication method and the free Lite plan.

    A new Cloudant database service is initialized and a service information page opens. Note the name of the service instance at the top of the page.

  2. Under Service credentials, click the New credential button. Then, click View credentials button and note the name of the credentials key for the instance, as shown in the following screen capture:

    Screen capture of Cloudant service credentials

  3. Use the Cloud Functions CLI to import and bind the ready-to-use Cloudant package (written in Node.js) in your account:

     ibmcloud fn package bind /whisk.system/cloudant cloudant
    
  4. Bind the package to the Cloudant database service and make the corresponding credentials available, remembering to replace the INSTANCE-NAME and CREDENTIALS-KEY placeholders in the following command with the service instance name and credentials key name noted previously.

     ibmcloud fn service bind cloudantnosqldb cloudant --instance INSTANCE-NAME --keyname 'CREDENTIALS-KEY'
    
  5. Confirm that the bound package appears in the list of packages and actions in the Cloud Functions dashboard, as shown in the following screen capture:

    Screen capture of Cloud Functions dashboard

  6. You can now use the actions in the Cloudant package to read and write data to your Cloudant database instance. By convention, you use a POST request to create a new document, a PUT request to update a document, and a DELETE request to delete a document. The IBM Cloud Functions Cloudant package takes care of generating and transmitting these different types of requests for you.

    Begin by creating a new database named mydb:

     ibmcloud fn action invoke cloudant/create-database --param dbname mydb --result
    
  7. Create an example document in the database:

     ibmcloud fn action invoke cloudant/create-document --param dbname mydb --param doc '{"user":"joe", "role":"teacher", "status":"active"}' --result
    

    You should see a response like the following, which confirms that the document was successfully created:

     {
         "id": "9970e0d1fd447908626ed22dd044ec5e",
         "ok": true,
         "rev": "1-9432474275a934f3fced4e2c91b6f23d"
     }
    
  8. Navigate to the IBM Cloudant dashboard and confirm that you can see the document:

    Screen capture of IBM Cloudant dashboard

    Similar functions exist to retrieve documents, list all documents, delete documents, create and delete attachments, and perform other common operations. You can find more information in the Cloudant package documentation.

    Note that the PHP runtime included with Cloud Functions comes with a number of built-in extensions, such as curl, gd, intl, mbstring and mysqli. These extensions mean that your PHP actions can also read and write data from and to a MySQL or MariaDB database without requiring any additional dependencies. See the complete list of available extensions in the PHP actions section of the System details and limits documentation.

Step 5: Use sequences to chain actions together

The Cloudant package described in the previous section does most of the heavy lifting when you’re working with Cloudant databases on IBM Cloud, but it has its own conventions for parameter input. In many cases, you might prefer to have a custom wrapper function around it (or example, to offer a simpler API to end-users or to perform input validation or pre-processing before writing to the database or displaying database content).

With Cloud Functions, you can chain multiple actions together, with the return value of one serving as the input parameters to the next. Consider the following example, which creates a simple wrapper around the Cloudant package’s create-document action and then chains the two together in a sequence.

  1. Create the following PHP function in a file named prepare-user-doc.php:

     <?php
     function main(array $args) : array
         {
       // check for inputs
       if (!isset($args['user'])) {
         throw new Exception('User name not found');
       }
    
       if (!isset($args['role'])) {
         throw new Exception('Role not found');
       }
    
       // create document
       $doc = [
         'user' => $args['user'],
         'role' => $args['role'],
         'status' => 'active'
       ];
    
       // return values in format expected by cloudant/create-document
       return ['dbname' => 'mydb', 'doc' => json_encode($doc)];
     }
    

    This function receives two parameters (a user name and a role), and it then creates a JSON document containing those parameters and adding one more static value. It returns an array with two keys, one for the Cloudant database name and the other for the JSON document content. The important point to note is that the keys of the return array match the input parameters expected by the create-document action of the Cloudant package.

  2. Provision this function as a PHP action:

     ibmcloud fn action create mypackage/prepare-user-doc prepare-user-doc.php
    
  3. Then, create a sequence chaining together the new PHP action with the existing Cloudant action:

     ibmcloud fn action create mypackage/create-user --sequence mypackage/prepare-user-doc,cloudant/create-document
     `
    
  4. Invoke the following sequence, providing only the user name and role as parameters:

     ibmcloud fn action invoke mypackage/create-user --param user "john" --param role "student" --result
    

    You should see a response like this:

         {
         "id": "3f8cbd00ef0add52d3c95c37a7fa60a5",
         "ok": true,
         "rev": "1-192ebdcc7939e12be1dac7d11f6dff5e"
     }
    
  5. Go to the Cloudant dashboard and confirm that the document exists and looks like the following screen capture:

    Screen capture of IBM Cloudant dashboard

As you can see, sequences provide an easy way to chain actions together. Sequences, coupled with the many pre-installed packages available in Cloud Functions, makes it easy to quickly build powerful functions with minimal time and effort.

Step 6: Automatically invoke actions

In the previous sections, you learned how you can invoke actions in multiple ways: through the IBM Cloud Functions CLI, through the IBM Cloud Functions online dashboard, and through an HTTP request. But all of these invocations are manual. What if you need to invoke actions automatically?

IBM Cloud Functions has you covered with the following ways to automatically invoke actions:

  • Triggers, which are similar to event declarations and which carry a data payload
  • Rules, which connect triggers with actions
  • Alarms, which automatically activate triggers

Here’s a simple explanation of how it works:

  1. When an alarm “rings”, it activates a trigger.
  2. The rule associated with the trigger invokes the connected action.
  3. The trigger’s data payload is transferred to the action as input parameter(s).
  4. The action runs.

Learn more about triggers and rules at Responding to events with triggers and rules.

Complete the following steps to see how triggers and rules work.

  1. Create a trigger with an alarm that rings every minute using the following command. Note that the trigger payload that contains the parameters expected by the cloudant/create-document action:

     ibmcloud fn trigger create trigger-write-db --feed /whisk.system/alarms/interval --param minutes 1 --param trigger_payload '{"dbname":"mydb", "doc":"{\"hello\":\"world\"}"}'
    
  2. Add a rule that associates the trigger with the action:

     ibmcloud fn rule create rule-write-db trigger-write-db cloudant/create-document
    

    With this arrangement, a new document is written to your Cloudant database every minute (until the time when you delete the trigger and rule). Verify this by checking your Cloudant database after a few minutes. You should see a list of auto-created documents, as shown in the following screen capture:

    Screen capture of Cloudant database

    This example used a simple periodic alarm. Cloud Functions also supports other types of alarms, such as one-time alarms and more complex periodic alarms with cron-type syntax. Learn more in the Alarm package documentation.

Step 7: Build an example application: Slack weatherbot

In the previous sections, you saw the Cloudant package for Cloud Functions in action.

Cloud Functions also has a number of other ready-to-use packages, including packages for Slack, GitHub, IoT, The Weather Company, and various IBM Watson services. You can find a complete list of available packages at Getting started and Using installable packages.

To wrap up the tutorial, this final example builds on the concepts you already learned to create a Slack “weatherbot”, which periodically posts a weather forecast for your city in your Slack workspace. It combines the OpenWeatherMap API with the Slack package for Cloud Functions and also uses action sequences, triggers, and alarms to deliver the expected result.

To try this example, you need to complete the following prerequisites:

  • You must have access to a Slack workspace and have a Slack application with an incoming webhook. You can sign up for a free Slack workspace on the Slack website, then create a Slack application and follow the instructions to define an incoming webhook. The following screen capture shows an example of what you should see at the end of the process:

    Screen capture of Slack webhook URLs

  • You must obtain an API key (free) from the OpenWeatherMap website.

After you have the OpenWeatherMap API key and the Slack incoming webhook URL, complete the following steps:

  1. Add the Slack package to your Cloud Functions workspace using the following command. This command defines the Slack webhook URL, Slack user name, and Slack channel as package-level parameters. Replace the SLACK-HOOK-URL, SLACK-USERNAME and SLACK-CHANNEL placeholders with the Slack webhook URL, your Slack user name, and the name of the Slack channel where you want to post the weather forecast (for example, #general).

     ibmcloud fn package bind /whisk.system/slack slack --param url SLACK-HOOK-URL --param username SLACK-USERNAME --param channel SLACK-CHANNEL
    
  2. Test the integration by invoking the package’s post action with a sample message using the following command. Learn more in the Slack package documentation.

     ibmcloud fn action invoke slack/post --result --param text "Hello from IBM Cloud Functions!"
    

    Confirm that you see the message in the Slack channel, as shown in the following screen capture:

    Screen capture of Slack message from Cloud Functions

  3. The OpenWeatherMap API exposes a /forecast API endpoint, which provides a 5-day weather forecast with data at 3-hourly intervals. Read more about it in the OpenWeatherMap API documentation and review the following screen capture, which displays a sample response from the API:

    Screen capture of OpenWeatherMap API sample response

  4. Create the following PHP function to generate a weather forecast for the next 12 hours for a specified city:

     <?php
     use GuzzleHttp\Client;
     use GuzzleHttp\Exception\ClientException;
    
     function main(array $args) : array
     {
       // check for input text
       if (!isset($args['city'])) {
         throw new Exception('Invalid city name');
       }
    
       if (!isset($args['key'])) {
         throw new Exception('No API key provided');
       }
    
       // create client
       $c = new Client([
         'base_uri' => 'https://openweathermap.org/api',
         'timeout'  => 6000,
       ]);
    
       // send request
       $response = $c->get('/data/2.5/forecast?APPID=' . $args['key'] . '&q=' . $args['city']);
       $data = json_decode($response->getBody(), true);
    
       // iterate over next 4 forecast readings
       // obtain average forecasted temperature
       $forecastedTempSum = 0;
       for ($x = 0; $x < 4; $x++) {
         $forecastedTempSum += $data['list'][$x]['main']['temp'];
       }
       $averageTemp = round(($forecastedTempSum / 4) - 273.15);
    
       // return text message containing average temperature value
       return ['text' => 'Hello! The average temperature in ' . ucwords($args['city']) . ' for the next 12 hours or will be ' . $averageTemp . ' C.'];
     }
    

    This function expects a city name as an input parameter (together with an API key, which is defined at deploy-time). It uses the Guzzle HTTP client to connect to the OpenWeatherMap API endpoint, retrieve a forecast for the specified city and then calculate the average forecasted temperature for the next 12 hours by averaging the first 4 readings. The average is then converted to Celsius from Kelvin. The function ends by generating a text message suitable for posting in Slack.

    Note that since third-party service credentials like API keys and passwords don’t change very often, it’s a good idea to store these credentials as default parameters (as suggested in the GitHub thread at github.com/serverless/) and simply reference them by name in your function code. If you want to use credentials for certain IBM Cloud services (such as Cloudant, discussed earlier in this article), you can also use the ibmcloud fn service bind command to directly bind service credentials to your package. Learn more at Simplify binding your IBM Cloud services to serverless Functions.

    Provision the PHP action using the following command and replace the placeholder API-KEY with your OpenWeatherMap API key:

     ibmcloud fn action create mypackage/forecast weather.php --param key API-KEY
    
  5. Create a sequence that connects the previous PHP action with the Slack message posting action:

     ibmcloud fn action create mypackage/slack-weatherbot --sequence mypackage/forecast,slack/post
    
  6. Create a trigger, rule and alarm to invoke the sequence every 12 hours. The trigger payload contains the input parameter expected by the first action in the sequence, the city name, which you can update to reflect your city:

     ibmcloud fn trigger create trigger-slack-weatherbot --feed /whisk.system/alarms/interval --param minutes 1200 --param trigger_payload '{"city":"mumbai"}'
    
     ibmcloud fn rule create rule-slack-weatherbot trigger-slack-weatherbot mypackage/slack-weatherbot
    

    Your weatherbot is now live and should shortly begin posting messages to the named Slack channel. The following image shows an example of the output in Slack:

    Screen capture of Slack response

Summary

This tutorial discussed specific tools, technologies, and APIs. Although these tools and APIs might change and evolve over time, the general principles remain valid and should serve as guidance for other projects you undertake. Here are some tips to keep in mind:

  • Serverless code and decoupled functions enable lightweight applications that are easy to test and maintain. This approach typically produces performance and cost benefits and eliminates the need to maintain server infrastructure.
  • Built-in packages reduces development and testing time and make it possible to quickly assemble standalone components into production-ready applications.
  • A secure, stable and extensible HTTP client library simplifies integration with any third-party REST API.

This tutorial explained the basics of provisioning PHP code as standalone functions on IBM Cloud Functions. It also covered integrating data from third-party APIs, scheduling actions and integrating with Cloudant and Slack for data storage and messaging respectively. After working through all the code examples in this article, you should have a working knowledge of how to use PHP with IBM Cloud Functions.