The PHP language is a true and good alternative for Serverless applications. PHP is a fast and flexible programming language, and there are many business treasures inside PHP applications, business logic running well for years inside company codebases worldwide.
We don't need to look at PHP as a language that could not run inside a modernized stack. We can move some of this code without total refactoring to Serverless applications, benefiting from an already proven successful code. And we know we all have flows suitable to run as a lambda function.
And not only legacy code. New features are also perfect candidates to be run in PHP and lambdas due to the team's experience, consistency of the technology stack, speed, etc. PHP has served the world well and will remain operating well. PHP is alive.
Table of contents:
- The series
- Functions
- Code
- Requirements
- The lambda
- Wrap-up
The Serie
I am starting a series as a walkthrough for PHP into Serverless, specifically to run as lambdas functions.
We will use Bref, a composer package, to deploy PHP applications to AWS.
Bref (which means "brief" in french) comes as an open source Composer package and helps you deploy PHP applications to AWS and run them on AWS Lambda.
https://bref.sh/docs/
Bref relies on the Serverless framework and AWS access keys to deploy applications.
https://bref.sh/docs/installation.html
The Serverless framework is excellent, but I am more of a fan of AWS CDK. Mainly because it is designed to use an imperative programming framework that speeds up the required infrastructure with excellent constructs on different levels (reasonable defaults), and its output can be run against a test framework (predictability).
There are already some CDK constructs for PHP, but, as far as I see, they are intended to be used by Web Apps lambdas (i.e. using frameworks such as Laravel and Symfony). However, the purpose of this series is to run Event-Driven functions, so I will start using pure CDK constructs.
Functions
As a walkthrough, we will digest the series in affordable bites, starting from simple functions that we will improve as the series continues and we use more AWS resources.
Code
Let's start our PHP lambda function. First, it will begin as an HTTP-based lambda, expecting a request and returning a response. Then, it will execute a trivial piece of computing code: it will return fibonacci.
Get the part 1 source-code in GitHub.
Requirements
(optional) There is a Dockerfile and a docker-compose.yml file for your convenience if you prefer to use docker. It will require you to set AWS environment variables for use by the container.
The lambda
You can check the complete source code for part 1, and we will highlight essential parts from the CDK code, as the PHP code has a few different things from what we are used to code.
The stack creator
In our case, it will create the serverless Stack and related infrastructure, i.e., IAM, lambda function, and URL. If anything else we need, it would be defined and requested by this class.
bin/cdk-stack.ts
export class CdkStack extends Stack {
// Get Bref layer ARN from https://runtimes.bref.sh/
public static brefLayerFunctionArn = 'arn:aws:lambda:us-east-1:209497400698:layer:php-82:16';
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const layer = LayerVersion.fromLayerVersionArn(this, 'php-layer', CdkStack.brefLayerFunctionArn);
const getLambda = new LambdaFunction(this, 'get', {
layers: [layer],
handler: 'get.php',
runtime: Runtime.PROVIDED_AL2,
code: Code.fromAsset(join(__dirname, `../assets/get`)),
functionName: 'part1-get',
});
const fnUrl = getLambda.addFunctionUrl({authType: FunctionUrlAuthType.NONE});
new CfnOutput(this, 'TheUrl', {
// The .url attributes will return the unique Function URL
value: fnUrl.url,
});
}
}
Highlights
The bref php layer:
public static brefLayerFunctionArn = 'arn:aws:lambda:us-east-1:209497400698:layer:php-82:16';
Where you point your entry point and source code:
handler: 'get.php',
runtime: Runtime.PROVIDED_AL2,
code: Code.fromAsset(join(__dirname, `../assets/get`)), // get.php file inside the zip file located at this path
Using AWS Lambda built-int function URL (we will change to API Gateway later if needed):
const fnUrl = getLambda.addFunctionUrl({authType: FunctionUrlAuthType.NONE});
Output
You would see CDK outputting the lambda function URL you will use to run your application. Something like:
Outputs:
CdkStack.TheUrl = https://6eoftivwkq4ht65d2h2fwlmsga0vnpfs.lambda-url.us-east-1.on.aws/
The handler
The PHP entry point has usually named a handler to the code. Its responsibility would be to forward the request to a controller or service that will perform the business rules and prepare the response to be returned. This is an HTTP-based lambda; the response should be an HTTP-valid response.
Obs.: You can note by the words above that any existing code that fits in the lambda computing model can be the controller or service to be called by the handler. Theoretically, you only need to create the handler compatible with the lambda environment, instantiate your controller or service, pass whatever it requires as an argument, and then return the expected response.
php/handlers/get.php
<?php
return function ($request) {
$int = (int) ($request['queryStringParameters']['int'] ?? random_int(1, 300));
$responseBody = [
'response' => 'OK. Time: ' . time(),
'now' => date('Y-m-d H:i:s'),
'int' => $int,
'result' => fibonacci($int),
];
$response = new \Symfony\Component\HttpFoundation\JsonResponse($responseBody);
return (new \Bref\Event\Http\HttpResponse($response->getContent(), $response->headers->all()))->toApiGatewayFormatV2();
};
Highlights
All handlers receive a request object. This is how to access /?int=myValue
query string param.
$int = (int) ($request['queryStringParameters']['int'] ?? random_int(1, 300));
The call to the function fibonacci()
is how we would call any other controller or service.
'result' => fibonacci($int),
Using the Symfony Response to validate and prepare a valid HTTP response:
$response = new \Symfony\Component\HttpFoundation\JsonResponse($responseBody);
AWS API Gateway requires a certain Response shape. To be sure to have a valid API Gateway response:
return (new \Bref\Event\Http\HttpResponse($response->getContent(), $response->headers->all()))->toApiGatewayFormatV2();
And that is it. You can now use your lambda function URL as in the output of the CDK stack above and call it with or without the query string param ?/int=
.
➜ curl https://6eoftivwkq4ht65d2h2fwlmsga0vnpfs.lambda-url.us-east-1.on.aws/
{"response":"OK. Time: 1674612343","now":"2023-01-25 02:05:43","int":273,"result":5.05988662735923e+56}%
➜ curl https://6eoftivwkq4ht65d2h2fwlmsga0vnpfs.lambda-url.us-east-1.on.aws/\?int\=500
{"response":"OK. Time: 1674612353","now":"2023-01-25 02:05:53","int":500,"result":1.394232245616977e+104}%
➜ curl https://6eoftivwkq4ht65d2h2fwlmsga0vnpfs.lambda-url.us-east-1.on.aws/\?int\=500
{"response":"OK. Time: 1674612356","now":"2023-01-25 02:05:56","int":500,"result":1.394232245616977e+104}%
The test
We can predict the resources we create via CDK and check if those resources are as expected. The output of the CDK is a CloudFormation template, which we can put under test. That is solid, as unexpected behaviour or changes will fail in our CI pipeline test step.
test/cdk.test.ts
test('Lambda created', () => {
const app = new cdk.App();
// WHEN
const Stack = new Cdk.CdkStack(app, 'MyTestStack');
// THEN
const template = Template.fromStack(stack);
template.hasResourceProperties('AWS::Lambda::Function', {
Layers: [Cdk.CdkStack.brefLayerFunctionArn]
});
});
Highlights
We are checking if there is a lambda function and if that function is using the expected specific bref layer:
template.hasResourceProperties('AWS::Lambda::Function', {
Layers: [Cdk.CdkStack.brefLayerFunctionArn]
});
Wrap-up
We have created our Stack and our first simple HTTP-based PHP lambda function using CDK (with tests). Next, we will improve our lambda to use more AWS resources and communication with more complex application services.