A Real World PHP Lambda App Part 3: Controllers

Submitted by nigel on Sunday 28th January 2018

The next step in our odyssey to build a real world Lambda app is to create the controllers. We will need two - one for the form and one for the thankyou page. We should - and therefore will - create an interface for those two. In addition since we want to be able to test our controllers out, we will need to create some placeholder views. The views will be constructed using the Twig template engine so that will have to be added to our build too. 

First lets create our controllers. 

Navigate to the root directory of the project and issue:
$ pwd
/var/www/html/serverless-vote
$ mkdir src/Controllers
$ touch src/Controllers/FormController.php
$ touch src/Controllers/ThankyouController.php
$ touch src/Controllers/VoteInterfaceController.php
Twig Installation
Twig installation is by composer and thus simplicity itself.
$ composer require "twig/twig:^2.0"
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Installing symfony/polyfill-mbstring (v1.7.0) Downloading: 100%         
  - Installing twig/twig (v2.4.4) Downloading: 100%         
Writing lock file
Generating autoload files
$ 
Interface

The interface is listed below. Note that we construct with the two properties we are going to need to do our work - the data array structure, and the logger object we can use to report to stderr which will be picked up by CloudWatch. We are setting the default HTTP status code to 200 and the headers will always return HTML by default. We have two protected methods in there - the createModel uses the new operator which dooms unit testing so it has been placed outside of the main code, and the _index method which also performs instantiation thus making unit testing difficult.

<?php
namespace Vote\Controllers;



abstract class 
VoteInterfaceController
{

    
// Lambda context
    
public $data;
    public 
$logger;


    
// http protocol stuff - set default values
    
protected $statusCode 200;
    protected 
$headers = [
        
'Content-Type' => 'text/html'
    
];


    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;
    }


    
/**
     * Instantiate a model.
     * Performing this in it's own function makes unit testing easier
     *
     * @link https://stackoverflow.com/questions/7760635/unit-test-for-mocking-a-method-called-by-new-class-object
     *
     * @param string model
     * @return object
     */
    
protected function createModel($model)
    {
        
$model "\\Vote\\Models\\".$model;
        return new 
$model($this->logger$this->data);
    }


    
/**
     * Wrapper for index controller
     *
     * @param string filename of the template
     * @param array key/values to populate the template
     *
     * @return string rendered template
     */
    
protected function _index($filename$values)
    {
        
$loader = new \Twig_Loader_Filesystem(array(dirname(__DIR__).'/Views'));
        
$twig = new \Twig_Environment($loader, array('cache' => '/tmp/cache'));
        
$content $twig->load($filename);

        return 
$content->render($values);
    }

?>
Composer Autoloader
We will need to inform composer of our new autoloader requirements - both Twig and our controllers. So add the following entries under psr-4 in composer.json
  "autoload": {
    "psr-4": {
      "Raines\\Serverless\\": "src",
      "Vote\\Controllers\\": "src/Controllers/",
      "Twig\\": "vendor/twig/twig/lib"
    }
  },
Then update
$ composer update
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 0 installs, 4 updates, 0 removals
  - Updating symfony/filesystem (v3.3.13 => v4.0.4) Downloading: 100%         
  - Updating symfony/config (v3.3.13 => v3.4.4) Downloading: 100%         
  - Updating symfony/dependency-injection (v3.3.13 => v3.4.4) Downloading: 100%         
  - Updating symfony/yaml (v3.3.13 => v3.4.4) Downloading: 100%         
Writing lock file
Generating autoload files
Controller Code

Here is the code for our controllers. The action index in the FormController merely presents the form to the end user. The save method doesn't actually save the POSTed date yet since we haven't created our models yet. That will be covered in a later tutorial. However it constructs the URL for the redirect to the thank you page using the environmental data which is provided by Lambda. It returns an HTTP status code of 307 - which is the code that says redirect and don't change the original HTTP method, which in our case was POST. Therefore the thank you page will have the POSTed data we want. 

FormController.php
<?php
namespace Vote\Controllers;


class 
FormController extends VoteInterfaceController {

    
/**
     * Load the form
     */
    
public function index()
    {
        return 
$this->_index(
            
'vote.html',
            [
                
'title' => 'Badzilla\'s Lambda Vote Form',
                
'static_url' => $this->data['static_url']
            ]
        );
    }


    
/**
     * Save the captured form and send to DynamoDB then redirect to thank_you
     */
    
public function save()
    {
        
// Get the data ready for for the model and the redirect
        
$data $this->__get('data');

        
// 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 '';
    }
}
?>
ThankyouController.php
<?php
namespace Vote\Controllers;




class 
ThankyouController extends VoteInterfaceController
{
    public function 
index()
    {
        return 
$this->_index(
            
'thank-you.html',
            [
                
'title' => 'Badzilla\'s Big Thank You',
                
'static_url' => $this->data['static_url'],
            ]
        );
    }
}
?>
Custom Serverless Parameter static_url
serverless.yml
handler.php

You'll notice in the code above that I am passing the parameter $this->data['static_url'] to the Twig templates. The static URL contains the base path to the S3 bucket that uses CloudFront to serve our assets from the S3 bucket (via CloudFront) we set up in a previous tutorial. So we need to make sure the URL is set correctly from the configuration. 

Firstly open up the file serverless.yml in your favourite editor. Under custom, add the key/value pair where the key is s3StaticBucket and the value is the identifier of your s3 asset bucket (and not the s3 bucket for your deploys. Then under environment, add the full path of that bucket using STATIC_URL as the key. It's best explained by referring to the first image above. This will populate the value of STATIC_URL in the environment which can be picked up in the PHP when it runs. 

Now edit handler.php and set the array key $event['static_url'] as shown in the second image.

 

Views
The views need to be created. These will hold the raw HTML for sending back to the browser. There will be three in total - an overarching layout.html which holds the main HTML structure such as the html head and body tags, and then vote.html and thank-you.html which hold specific body content for the two pages in our app. The actual HTML is beyond the scope of these tutorials, but they are created with:
$ mkdir -p src/Views
$ touch src/Views/layout.html
$ touch src/Views/thank-you.html
$ touch src/Views/vote.html
You obviously now need to put the HTML code in those empty files, and if you are using Twig, then the placeholders for the variables you pass to the templates. You should also be adding your images and JavaScript and css to directories underneath /dist. The entire project should look like this right now.
$ tree -I vendor
.
├── buildphp.sh
├── CHANGELOG.md
├── composer.json
├── composer.lock
├── config
│   └── services.yml
├── dist
│   ├── css
│   │   └── default.css
│   └── images
│       ├── badzilla-logo32x32.png
│       └── favicon.ico
├── dockerfile.buildphp
├── docroot
│   └── layout.html
├── handler.js
├── handler.php
├── LICENSE
├── node_modules
│   └── serverless-single-page-app-plugin
│       ├── index.js
│       ├── package.json
│       └── README.md
├── package-lock.json
├── php
├── README.md
├── serverless.yml
└── src
    ├── Context.php
    ├── Controllers
    │   ├── FormController.php
    │   ├── ThankyouController.php
    │   └── VoteInterfaceController.php
    ├── Handler.php
    ├── Views
    │   ├── layout.html
    │   ├── thank-you.html
    │   └── vote.html
    └── VoteHandler.php
 
10 directories, 29 files
I've used the Bootstrap theme Cosmo which is hotlinked in the layout.html so I don't need too many assets. But what I do have still needs syncing up to the S3 bucket which, as per my earlier tutorial, is achieved with what is below, followed by my final deploy because we are now ready to test our app.
$ sls syncToS3 
$ sls deploy
Run
Form
Thank You

Ok so time to check out our app. Yes we still have a long way to go because we aren't saving the data as yet. But steady progress is being made.