Serverless PHP Applications on Digital Ocean Functions
Digital Ocean Functions is a serverless compute product Digital Ocean recently launched. It supports PHP as one of the runtimes, along with Node.js, Python, and Go.
PHP's Request-Response model has always been akin to modern serverless functions, in that PHP and serverless functions are often short-lived actions, and that they are invoked in command-line, and more commonly, with an HTTP request. Each of these short-lived requests do not share data in-between unless they explicitly store data in a persistent media (such as on files or a database), which make them ideal to scale up and down depending on the demand.
Traditionally, running a PHP web application required running a web server that accepts requests, passes them to PHP (php-fpm
server API for example), and returns the response from PHP back to the client. Apache, Nginx, and Caddy are some of the web servers software that can act as the "server". With Serverless function platforms, the "server" is managed by the platform provider (Digital Ocean Functions in this case).
Digital Ocean Functions competes with existing Serverless platforms such as Google Cloud Functions, Amazon Lambda, and Oracle Cloud Functions.
Apache Open Whisk is a free and open-source serverless platform that also supports PHP as a language run-time. Digital Ocean Functions uses Open Whisk to power their underlying infrastructure.
Getting Started with Digital Ocean Functions
Once a Digital Ocean account is created, or logged into the dashboard, head to the Functions section to create the first Digital Ocean Function.
Functions created from the "Functions" section are meant for testing purposes. Once a function is fully tested out, they can be deployed with the App Platform.
Pricing for PHP Functions
The pricing page shows a summary of the billing rates for functions. Digital Ocean Functions are billed by the memory usage multiplied by the duration of each invocation of the function. In a PHP context, the billable GiB-seconds can be calculated by the memory each PHP request consumes (which can be obtained with memory_get_usage()
), times the duration of the request.
For example, if the PHP function invocation consumes 128 MiB of memory, and takes 100 ms to process, that consumes roughly 0.0125
GiB-Seconds (30/1024 * 100/1000). Digital Ocean's free tier offers up to 90,000 GiB-Seconds a month at no charge. This is roughly equivalent to invoking said PHP function for over 7 million times a month.
Launch a test Serverless PHP Function
Head over to the Functions section in the dashboard, and click "Launch" to launch the first function.
The functions listed in the "Functions" section are meant to be testing, until they are deployed with the App Platform. In a production application, the application source code is taken from a GitHub repository, and built and deployed automatically when new code is pushed to that repository.
For the purpose of the first example, create a new function with the "Create Function" under "Prototype Serverless Functions" section:
The next screen allows selecting the runtime (Php:8.0
) and asks for a name for the function. This name will be part of the URL, but because they are prototype functions, they will be invoked at a namespace assigned to the Digital Ocean account.
Once the function is created, it is possible to modify the PHP source code of the serverless function. The "Settings" tab provides controls to edit the memory and duration limits, to edit function-specific environment variables, and to modify how the HTTP requests are handled.
Developing Serverless PHP Function
Digital Ocean Functions is based on Apache Open Whisk, and many concepts used in Open Whisk work on Digital Ocean Functions as well.
Each Function must have a function that serves as the entry-point, and that function must return an array containing the response body. The return value may additionally contain a statusCode
for the HTTP response code, and a PHP stdClass
object of HTTP response headers.
On PHP applications that are deployed on the App Platform, the code is fetched from a GitHub/GitLab repository, from Digital Ocean container registry, or from a Docker image. For Prototype functions, the source is directly entered at the "Source" tab of each function.
Here is a minimal yet functional "Hello World" example:
function main(): array {
return [
'body' => 'Hello World'
];
}
If statusCode
is not provided, a standard "200 OK" response is sent to the client. Additional response headers can be added to from the code itself, or from CORS settings when the application is deployed with the App Platform.
Here is a minimal example that the function returns an error repose along with an HTTP status code and additional HTTP headers:
function main(): array {
return [
'body' => 'The requested resource is not found.',
'statusCode' => 404,
'headers' => (object)
[
'Cache-Control' => 'no-cache',
],
];
}
For prototype functions, the PHP source code can be edited and executed immediately.
Once executed, the output is shown directly below the source code:
PHP Runtime Environment
As of now, Digital Ocean's PHP runtime for Serverless Functions is configured as below:
PHP Version: 8.0 Available extensions: Libxml, OpenSSL, Sqlite3, Zlib, Curl, Dom, FTP, Iconv, Mbstring, PDO (Sqlite, PgSQL, MySQL), Session, Readline, SimpleXML, Phar, Tokenizer, XML, BCMath, GD, Intl, MongoDB, MySQLi, Soap, Sodium, Zip, Opcache
A few notable observations include:
max_execution_time
andmemory_limit
PHP INI values being set to unlimited.- File uploads and
allow_url_fopen
enabled. - No functions disabled from INI settings.
error_reporting
value set toE_ALL
.
Digital Ocean may add new PHP versions as they see fit, and the list of extensions may change in the future as well.
HTTP Response Format
Each Digital Ocean Function should return a response value that may contain a body, an HTTP status code, and a list of HTTP responses.
For PHP functions, this means the invoked function should return a PHP associative array with body
, statusCode
, and headers
.
function main(): array {
return [
'body' => 'Response body goes here',
'statusCode' => 200,
'headers' => (object)
[
'Cache-Control' => 'no-cache',
'Foo' => 'Bar',
],
];
}
body
: The response body as a string.statusCode
: The HTTP response code as an integer.headers
: A PHPstdClass
object with the HTTP header names as object property names, and the header value as the property value.
Values emitted to the stdout
(such as echo
calls) will not be sent to the user.
HTTP Request Format
A serverless PHP can be invoked from various ways, such as calling it from CLI (using doctl
), a REST API call, or via an HTTP request. HTTP request provide the ease of calling serverless functions from anywhere, and can be used with third party applications that only accept an HTTP endpoint.
The HTTP endpoint of the Serverless function is displayed above the function source code region. Any HTTP requests to this URL also invokes the function.
Digital Ocean Functions inherit Open Whisk's behavior that the invoked function receives the HTTP request information in the arguments passed to the function.
HTTP request information is only available if the function is invoked over HTTP/HTTPS. The following values will not be present if the function is invoked with the REST API or via
doctl
.
For each function invoked over HTTP, the following parameters are made available in addition to the application parameters passed in the URL or in the POST request body.
__ow_method
: The HTTP method of the request, in lower case. e.gpost
,get
.__ow_headers
: An associative array of HTTP headers from the HTTP request.__ow_path
: The path of the request. For requests made to the exact URL endpoint of the function, this value will be an empty string. If there is a URL path after the endpoint, that additional path will be stored here.
Digital Ocean Functions parse the HTTP request body (URL parameters or parameters of a POST request), but there is a setting under the "Settings" tab to opt-in to Raw HTTP, which leaves it up the function to parse the request body. With Raw HTTP enabled, the two following keys will be available in the parameters array in addition to the values listed above:
__ow_query
: The HTTP query string of the request. e.g.name=test&username=test
.__ow_body
: Contents of the raw HTTP request body, as a string.
Deploying PHP Serverless Functions
Prototype functions used in the section above are meant to be used as a testing ground until they are deployed on the App platform. Applications deployed on the App platform, support custom domain names, resource management, automatic deployment on Git commits, and more.
The rest of this article guides through an example application called "qr-show", a simple application that encodes a given string in a QR code.
project.yml
File and the Git Repository
The app starts with a project.yml
file, which is greatly explained in Digital Ocean docs. The following is the bare minimum to get started with PHP serverless functions:
In an empty or existing Git repository, place this project.yml
file at the root, and have the changes pushed to a GitHub/GitLab repository.
project.yml
packages:
- name: qr
actions:
- name: qrShow
runtime: php:default
In the example above, project.yml
file describes a serverless function at qr/qrShow
path, using php:default
runtime. By default, this means the function will be invoked when requesting https://example.com/qr/qrShow
URL, using the php:default
runtime.
Digital Ocean docs explains the syntax and more parameters of the project.yml
file in detail.
Creating Resources
To deploy a PHP serverless function on Digital Ocean App Platform, head over to the Apps page in the dashboard, and click "Create".
The next step offers an overview of the current resource allocations and an estimation of costs:
The next step offers controls to set environment variables for the app itself and for the serverless function, followed by a choice of data center location and the name of the application. Proceed with the form, and create the resources.
Application code
After creating the compute resources on Digital Ocean, it is now time to create/update application code to suit Digital Ocean Functions.
The following is an overview of the example QR code application that the article guides through:
├── packages
│ └── qr
│ └── qrShow
│ ├── build.sh
│ ├── composer.json
│ ├── composer.lock
│ └── index.php
└── project.yml
PHP Source Files
PHP file(s) for the function shall be placed in packages/qr/qrShow
directory. Digital Ocean Functions automatically assumes the main entry file to be packages/qr/qrShow/index.php
, and calls the main()
function defined in this file.
For the QR code example, here is a simple file that makes use of library to generate the QR code as an SVG string, and returned to the caller:
packages/qr/qrShow/index.php
<?php
use splitbrain\phpQRCode\QRCode;
function main(array $args): array {
if (!isset($args['message']) || !is_string($args['message'])) {
$response['statusCode'] = 400;
$response['body'] = 'No "message" parameter provided.';
return $response;
}
return ['body' => QRCode::svg('hello world')];
}
build.sh
File
In the same directory as the index.php
file, place a build.sh
file that Digital Ocean Functions automatically execute when deploying. This function can be used to install Composer dependencies, run tests, etc.
packages/qr/qrShow/build.sh
#!/bin/bash
set -e
composer install --no-interaction --ansi
composer ci # Run a composer script named "ci", that runs tests, QA, etc.
composer.json
and composer.lock
Digital Ocean Functions can be used to automatically install dependencies of the PHP serverless function. The application composer.json
file can be placed in the same directory as index.php
and build.sh
files. The build.sh
file in this example is set to run composer install
to install the application dependencies at deploy time.
To ensure the deployed application uses the exact same set of dependencies everywhere, make sure to commit the composer.lock
file to the repository as well.
packages/qr/qrShow/composer.json
{
"require": {
"php": "^8.0.0",
"splitbrain/php-qrcode": "^0.1"
}
}
Databases, Persistent File storage, Domain names
Digital Ocean offers several other features such as custom domain names, databases, static file storage, and other products that can be integrated with each other.
The Settings tab of each application provides fine control over the alert policies, custom domains, and environment variables.
Using the "Create" button, it is possible to create a new managed database instance that acts as a persistent database storage for serverless functions.
Logs and Insights
The Insights, Activity, and Runtime Logs sections provide detailed information on the performance, latency, and frequency information of the serverless functions, activity logs (when builds were triggered), and detailed logs to debug any failures or notices on builds.
Digital Ocean Functions is a serverless compute product from Digital Ocean that provides PHP as a run-time. It offers 90,000 GiB-seconds of compute resources for free every month, and PHP applications that are meant to be short-lived, with no or small requirement for persistent storage might be an ideal fit to deploy to Digital Ocean Functions as an inexpensive and scalable platform.