A Real World PHP Lambda App Part 5: Models

Submitted by nigel on Monday 9th April 2018
Now it's time to create our model. This will be easy at this stage since we are only going to be writing a record to our DynamoDB table. However since the scope of the project could increase over time, it would make sense to create an abstract class which will deal with the AWS SDK Client configuration and creation. So to start we should create a directory and two PHP files - one for the abstract class and one to write the record.
$ pwd
/var/www/html/serverless-vote
$ mkdir -p src/Models
$ touch src/Models/VoteModel.php
$ touch src/Models/PutVote.php
Installing AWS PHP SDK
We are going to use the AWS PHP SDK for writing our records out to DynamoDB since it contains a very convenient DynamoDB Client. To install it, use composer:
$ composer require aws/aws-sdk-php
Using version ^3.54 for aws/aws-sdk-php
The PHP Abstract Class
VoteModel.php

<?php

namespace Vote\Models;


use 
Aws\DynamoDb\DynamoDbClient;


abstract class 
VoteModel {


    protected 
$data;
    protected 
$logger;

    protected 
$client_config = [
        
'region' => 'eu-west-1',
        
'version' => '2012-08-10',
        
'credentials.cache' => TRUE,
        
'validation' => FALSE,
        
'scheme' => 'http'
    
];


    public function 
__construct($logger$data)
    {
        
$this->logger $logger;
        
$this->data $data;
    }


    public function 
__set($key$value)
    {
        
$this->$key $value;
    }


    public function 
__get($key)
    {
        return 
$this->$key;
    }

    
/**
     * Get an aws sdk client for extended classes
     *
     * @return \Aws\DynamoDb\DynamoDbClient
     */
    
protected function loadClient()
    {
        
$this->client_config['credentials'] = \Aws\Credentials\CredentialProvider::env();
        return(new 
DynamoDbClient($this->client_config));
    }

}
?>
This shouldn't hold too many surprises. The constructor requires the data and logger which is made available to any class that extends from it - we will need both since the logger object is used to notify our status through CloudWatch, and the data will contain the submitted form data. There are setters and getters as you would expect to provide a consistent way of dealing with the class's properties.

The loadClient method instantiates the DynamoDB client, and note that we are getting the necessary credentials from the environment which will be passed to the client.
PHP DynamoDB PutVote Class
PutModel.php
<?php

namespace Vote\Models;

use 
Aws\DynamoDb\Exception\DynamoDbException;



class 
PutVote extends VoteModel {


    private 
$id '';


    
/**
     * Put request to AWS DynamoDB API
     *
     *
     * @param array $payload
     */
    
public function put($payload)
    {
        
$client $this->loadClient();

        
$this->id $this->generateUUID();
        
$current_datetime date('Y-m-d\TH:i:s');

        
$item = [
            
'id'               => ['S' => $this->id], // Primary Key
            
'forename'        => ['S' => $payload['first_name']],
            
'surname'        => ['S' => $payload['last_name']],
            
'datetime'        => ['S' => $current_datetime],
            
'optradio'      => ['S' => $payload['optradio']],
        ];

        try {
            
$client->putItem(array(
                
'TableName' => $this->data['dynamodb_table'],
                
'Item' => $item
            
));
        } catch (
DynamoDbException $e) {
            
$this->logger->notice('DynamoDB Put Failed', array(
                
'table'         => $this->data['dynamodb_table'],
                
'id'             => $this->id,
                
'RequestID'     => $e->getAwsRequestId(),
                
'ErrorType'     => $e->getAwsErrorType(),
                
'ErrorCode'     => $e->getAwsErrorCode(),
                
'ErrorMessage'    => $e->getAwsErrorMessage(),
                
'StatusCode'     => $e->getStatusCode()
            ));
            return 
TRUE;
        }
        return 
FALSE;
    }


    
/**
     * Create a UUID - PHP doesn't have a good function for this, so hand-rolled
     *
     * @see https://rogerstringer.com/2013/11/14/generate-uuids-php/
     *
     * @return string generated UUID
     */
    
protected function generateUUID()
    {
        return 
sprintf'%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            
mt_rand00xffff ), mt_rand00xffff ),
            
mt_rand00xffff ),
            
mt_rand00x0fff ) | 0x4000,
            
mt_rand00x3fff ) | 0x8000,
            
mt_rand00xffff ), mt_rand00xffff ), mt_rand00xffff )
        );
    }
}
?>
The PutVote class is also very simple since the legwork is undertaken by the AWS PHP SDK. The primary key can be auto-increment but I elected to use a UUID which is created in the generateUUID method. The payload passed to the put method is an array of the POSTed data of the form which is bundled into an record before being passed to the SDK putItem.
DynamoDB Configuration
There are a number of changes we need to inform to our codebase about DynamoDB. The first place is serverless.yml- we need to set an environment variable with the name of the DynamoDB table, so we will stick with the convention of appending the stage to the end of it. Add it to the end of the environmental section as below:
serverless.yml
  environment:
    STATIC_URL: https://s3-${self:provider.region}.amazonaws.com/${self:custom.s3StaticBucket}
    DYNAMODB_TABLE: vote_${self:provider.stage}
Further down in the Resources section we need to define our DynamoDB resource.
    ## Specify the DynamoDB Table
    DynamoDbTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: vote_${self:provider.stage}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 5
          WriteCapacityUnits: 5
    DynamoDBIamPolicy:
      Type: AWS::IAM::Policy
      DependsOn: DynamoDbTable
      Properties:
        PolicyName: lambda-dynamodb
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            - Effect: Allow
              Action:
                - dynamodb:GetItem
                - dynamodb:PutItem
              Resource: arn:aws:dynamodb:*:*:table/vote_${self:provider.stage}
        Roles:
          - Ref: IamRoleLambdaExecution
Finally, in the handler.php file that interfaces with the JavaScript shim there is a section of the code that sets PHP variables from the environment - this will make sure our model has the table name surfaced to use when a record is written out to DynamoDB. Make sure this section looks like mine below:
handler.php
<?php
// Get the handler service and execute
$handler $container->get(getenv('HANDLER'));
$event['static_url'] = getenv('STATIC_URL');
$event['dynamodb_table'] = getenv('DYNAMODB_TABLE');
?>
DynamoDB Deploy
AWS Table Deploy
Once the serverless.yml file has been updated it makes sense to do a deploy to make sure the table has been created correctly in AWS. The deply is below, and you should end up with the screenshot above. To get there, navigate to Services->DynamoDB->Tables and you should see vote_dev
$ sls deploy
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (12.53 MB)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
................................
Serverless: Stack update finished...
Service Information
service: vote
stage: dev
region: eu-west-1
stack: vote-dev
api keys:
  None
endpoints:
  GET - https://n4kofna4l1.execute-api.eu-west-1.amazonaws.com/dev/
  POST - https://n4kofna4l1.execute-api.eu-west-1.amazonaws.com/dev/
  POST - https://n4kofna4l1.execute-api.eu-west-1.amazonaws.com/dev/thank_you
functions:
  vote_get: vote-dev-vote_get
  vote_post: vote-dev-vote_post
  thank_you: vote-dev-thank_you
Serverless: Removing old service versions...
Controller Modifications
Now the model exists, we need to made changes to our controllers so the models are instantiated and invoked. Firstly the save method in the FormController needs to be rewritten - see below. Once we get the submitted POSTed data, it is parsed into an array ready for the model to use. The model is called, and if it returns TRUE then we reload the index page with an error message.
FormController.php
<?php
    
public function save()
    {
        
// Get the data ready for for the model and the redirect
        
$data $this->__get('data');
        
parse_str($data['body'], $params);
        
$model $this->createModel('PutVote');

        
// Bail on Error
        
if ($model->put($params)) {
            return 
$this->_index(
                
'vote.html',
                [
                
'static_url' => $data['static_url'],
                
'title' => 'Badzilla\'s Lambda Vote Form',
                
'error' => 'Something has gone wrong with your submission. Please try later.'
            
]);
        }

        
// Construct the url using the Lambda data we have
        
$url $data['headers']['X-Forwarded-Proto'].'://';
        
$url .= $data['headers']['Host'];
        if (isset(
$data['requestContext']['stage']) && $data['requestContext']['stage'] != '') {
            
$url .= '/'.$data['requestContext']['stage'];
        }
        
$url .= '/thank_you';        

        
// Set the required http settings for a redirect resubmit POST
        
$this->__set('statusCode'307);
        
$this->__set('headers', array('Location' => $url));

        
// No body text to return 
        
return '';
    }
?>
The ThankYouController requires a small modification - we need to parse the submitted data from the form and then pass the first name and last name to the Twig template. This occurs in the index method.
ThankYouController.php
<?php
    
public function index()
    {
        
parse_str($this->data['body'], $params);

        return 
$this->_index(
            
'thank-you.html',
            [
                
'title' => 'Badzilla\'s Big Thank You',
                
'static_url' => $this->data['static_url'],
                
'first_name' => htmlspecialchars($params['first_name'], ENT_QUOTES'UTF-8'),
                
'last_name' => htmlspecialchars($params['last_name'], ENT_QUOTES'UTF-8'),
            ]
        );
    }
?>
Composer Autoloader
We will need to inform composer of our new autoloader requirements for the model. So make sure your composer has the following entries under psr-4 in composer.json
  "autoload": {
    "psr-4": {
      "Raines\\Serverless\\": "src",
      "Vote\\Controllers\\": "src/Controllers/",
      "Vote\\Models\\": "src/Models/",
      "Twig\\": "vendor/twig/twig/lib"
    }
  },
Then update with:
$ composer dump-autoload
Generating autoload files
Deploy and Add Records
Vote records

The code can now be deployed (as per the instructions earlier) and by pointing a browser to the dev instance of the site, records can be successfully added to the DynamoDB database. By navigating in the AWS Console to Services->DynamoDV->Tables->{select vote_dev}->Items we can see that the records are being created successfully. 

The solution is now functionally ok but there is a long way to go before we are production ready. Also I have spoon fed the code, but in the real world there would have been plenty of trial and error, and problems to solve. To cut down the endless deploy to AWS/test cycle, it is better to develop and test as far as possible locally. We prepped for this in the earlier blog A Real World PHP Lambda App Part 4: Setting up DynamoDb Locally and in the next blog I'll show you how to develop locally.