Bref is a composer package that helps you deploy your PHP applications as AWS Lambda functions. It leverages the Serverless Framework to provision and deploy such applications, and provides support for common frameworks like Laravel, Symfony, and Slim PHP.
In a previous article, we explored how we can use the Bref PHP framework to forward errors from our Twilio application to a Slack channel. In this article, we will revisit Bref by building a service that verifies users’ phone numbers using the Twilio Verify API.
Prerequisites
To complete this tutorial, you will need:
- Composer and npm installed on your computer.
- A Twilio Account
- A root AWS Account (to create an IAM user)
- cURL or Postman installed to test our application endpoints
- The Serverless CLI
Create a Twilio Verify Service
To get started, head over to your Twilio Verify Console and create a new Verify Service. Take note of the generated Service SID. Twilio lets you reuse the same service across different channels such as Email and SMS, so feel free to use an existing one if already available.
Create Project and Install Dependencies
Create a new folder for the project and install the required dependencies with the commands below:
$ mkdir bref-twilio-verify && cd bref-twilio-verify
$ composer require bref/bref twilio/sdk vlucas/phpdotenv slim/slim slim/psr7
The composer
command above will install the Bref serverless framework and the Twilio helper library phpdotenv
to help us use environment variables declared in a .env
file. Additionally, it installs the core of the Slim PHP framework and some of its dependencies. That way, you can use minimal features of the framework (such as routing) without bringing in the extras that come with using the full framework.
Initialize a bref
project in the current directory with ./vendor/bin/bref init
and select the HTTP application runtime to set up an HTTP endpoint. This enables your code to process HTTP requests using the AWS API Gateway. Initializing bref generates two additional files in the working directory:
serverless.yml
which contains information about the application and the configuration details used to provision and deploy it to the specified cloud provider (AWS).index.php
the application entry point. By default, it’s the Bref splash screen.
Next, create a .env
file in the project root to hold your Twilio credentials with:
$ touch .env
Add the Twilio Verify SID generated earlier, as well your Twilio credentials to the .env
file as shown below:
TWILIO_VERIFY_SID=VAXXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_AUTH_TOKEN=40XXXXXXXXXXXXXXXXXXXXXXXX
TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXX
Setup Application Routes with Slim
Slim is a minimal framework for developing PHP applications, whose lightness makes it more fitting for this use case. Specifically, we will use it to power the application routes which comprise three endpoints:
GET /
: An index endpoint for performing a health check.POST /send
: Receives a user’s phone number (in E.164 format) as a request parameter and sends the verification token to the phone number. It also responds with a “Token sent” message if successful.POST /verify
: Receives a user’s phone number (also in E.164 format) and the received token which is then used to verify the number. Its response depends on if the token is valid or not.
To implement the endpoints described above, first, create an app
directory in the project root folder and move the generated index.php
file there. Next, set up the Slim framework by replacing the content of the index.php
file (using your favorite IDE) with the code block below:
<?php
use App\JsonParserMiddleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Twilio\Rest\Client;
require_once __DIR__."/../vendor/autoload.php";
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__."/../");
$dotenv->load();
$app = AppFactory::create();
$app->addMiddleware(new JsonParserMiddleware());
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorHandler = $errorMiddleware->getDefaultErrorHandler();
$errorHandler->forceContentType("application/json");
The code above loads the .env
file we created earlier and adds a JsonParserMiddleware
to parse incoming JSON requests. Also, it forces the default Slim error handler to send back application errors as a JSON response.
Complete your implementation by appending the code below to your index.php
file:
$app->get('/', function (Request $request, Response $response) use ($app) {
$payload = json_encode(["message" => "I am alive!"]);
$response->getBody()->write($payload);
return $response
->withHeader("Content-type", "application/json")
->withStatus(200);
});
/*
* POST /send
* @param string phone E.164 formatted phone number of the recipient
*/
$app->post("/send", function (Request $request, Response $response) {
$data = $request->getParsedBody();
$payload = [];
$statusCode = 200;
if ($data['phone'] == null) {
$payload['error'] = "Phone is required";
return response($response, $payload, 422);
}
$verifier = getVerifyService();
try {
$attempt = $verifier->verifications->create($data['phone'], 'sms');
$payload['message'] = "Token sent";
$payload['sid'] = $attempt->sid;
} catch (\Twilio\Exceptions\TwilioException $e) {
$payload['error'] = $e->getMessage();
$statusCode = 400;
}
return response($response, $payload, $statusCode);
});
/*
* POST /verify
* @param string phone E.164 formatted phone number of the recipient
* @param string token The received token as entered by the recipient
*/
$app->post("/verify", function (Request $request, Response $response) {
$data = $request->getParsedBody();
$payload = [];
$statusCode = 200;
if ($data['phone'] == null || $data['token'] == null) {
$payload['error'] = "Phone and token fields are required";
return response($response, $payload, 422);
}
$verifier = getVerifyService();
try {
$attempt = $verifier->verificationChecks->create(
$data['token'],
['to' => $data['phone']]
);
if ($attempt->valid) {
$payload['message'] = "Verified!";
} else {
$payload['message'] = $attempt->status;
$payload['data'] = $attempt;
}
} catch (\Twilio\Exceptions\TwilioException $e) {
$statusCode = 500;
$payload['error'] = $e->getMessage();
}
return response($response, $payload, $statusCode);
});
function response(Response $response, array $payload, $statusCode = 200) {
$response->getBody()->write(json_encode($payload));
return $response
->withHeader("Content-type", "application/json")
->withStatus($statusCode);
}
function getVerifyService() {
$token = getenv("TWILIO_AUTH_TOKEN");
$sid = getenv("TWILIO_ACCOUNT_SID");
$verifySid = getenv("TWILIO_VERIFY_SID");
$twilio = new Client($sid, $token);
return $twilio->verify->v2->services($verifySid);
}
$app->run();
Now it is time to set up the JsonParserMiddleware imported earlier. Create a new JsonParserMiddleware.php
file in the same app
folder with the following content:
<?php
namespace App;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
class JsonParserMiddleware implements MiddlewareInterface
{
public function process(Request $request, RequestHandler $handler): Response
{
$contentType = $request->getHeaderLine('Content-Type');
if (strstr($contentType, 'application/json')) {
$contents = json_decode(file_get_contents('php://input'), true);
if (json_last_error() === JSON_ERROR_NONE) {
$request = $request->withParsedBody($contents);
}
return $handler->handle($request);
} else {
$response = new \Slim\Psr7\Response();
$payload = ['error' => "Bad request"];
$response->getBody()->write(json_encode($payload));
return $response
->withHeader("Content-type", "application/json")
->withStatus(400);
}
}
}
In the code above, we defined a JsonParserMiddleware class that sends back any request whose Content-Type
isn’t of type application/json
with a “Bad request” response. Besides validating the header, it also parses the request body (which is available as “php://input”) and ensures it is cast as valid JSON.
Next, update your composer.json
file so it is aware of the new app
namespace by adding the following autoload
block after the require
object:
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
Now, your composer.json file should look like the one below:
{
"require": {
"bref/bref": "^0.5.24",
"slim/slim": "^4.5",
"slim/psr7": "^1.1",
"ext-json": "*",
"twilio/sdk": "^6.5",
"vlucas/phpdotenv": "^4.1"
},
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}
NOTE: You may need to run composer update
after this update in order to access the index /
endpoint.
Test Endpoints With cURL
You can test your application before deploying to AWS using cURL or Postman by spinning up the built-in PHP server in the project directory. Start the server with the command below:
$ php -S localhost:8000 -t app/
The following cURL command sends a verification token to the phone number specified:
$ curl -H "Content-Type: application/json" -X POST -d '{"phone": "+23480000000"}' http://localhost:8000/send
Make sure you run the previous command in a new terminal and update the phone number with your own.
You can verify a given token by using the command below:
$ curl -H "Content-Type: application/json" -X POST -d '{"phone": "+23480000000", "token": "123456"}' http://localhost:8000/verify
Deploying to Lambda
Create IAM Policy and User
To make your AWS resources available to the serverless framework, grab the AWS Key, Secret, and region of an IAM user. Ensure the user has full access to the following resources:
- IAM
- AWS S3
- API Gateway
- Lambda
- CloudFormation
- CloudWatch Logs
You can do that by creating a new policy using the JSON policy document below and attaching it to your new IAM user.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"iam:*",
"s3:*",
"apigateway:*",
"lambda:*",
"cloudformation:*",
"logs:*"
],
"Resource": "*"
}
]
}
Navigate one folder up in your terminal and set up the AWS credentials locally with the command below:
$ serverless config credentials --provider aws --key AWS_KEY --secret AWS_SECRET --profile bref-twilio-verify
Deploy with Serverless
Next, replace the content of the serverless manifest file (i.e serverless.yml
) with the one below:
service: twilio-verify
provider:
name: aws
region: us-west-2
runtime: provided
profile: bref-twilio-verify
plugins:
- ./vendor/bref/bref
functions:
index:
handler: app/index.php
timeout: 28 # in seconds (API Gateway has a timeout of 29 seconds)
layers:
- ${bref:layer.php-73-fpm}
events:
- http:
path: /
method: GET
sendToken:
handler: app/index.php
layers:
- ${bref:layer.php-73-fpm}
events:
- http:
path: /send
method: POST
verifyToken:
handler: app/index.php
layers:
- ${bref:layer.php-73-fpm}
events:
- http:
path: /verify
method: POST
# Exclude files from deployment
package:
exclude:
- 'node_modules/**'
- 'tests/**'
NOTE: Remember to change the value of the region flag to match your IAM user’s region if it is different.
The configuration now uses the AWS credentials we created earlier. It also creates a unique instance of the service for each endpoint, that way, we can take advantage of Lambda’s monitoring to gain insights such as:
- The number of times a route is invoked.
- How long it takes for a route to respond.
We can then deploy the bref application from the project root directory with:
$ serverless deploy
The above command outputs your application URL (similar to https://XXXXXXX.execute-api.us-west-2.amazonaws.com/dev
) in the endpoints
section when it’s done. You can go ahead and test the endpoints again, but using the generated URL this time.
Conclusion
You have successfully deployed a Bref microservice that verifies users’ phone numbers via the Twilio Verify API. You can find the complete project on Github. You can further extend it by restricting access to the endpoints we implemented using mechanisms like AWS Lambda authorizers.
Michael Okoko is a software engineer and CS undergrad at Obafemi Awolowo University, Nigeria. He loves open source and is mostly interested in Linux, Golang, PHP, and fantasy novels! You can reach him via: