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.
$ pwd /var/www/html/serverless-vote $ mkdir src/Controllers $ touch src/Controllers/FormController.php $ touch src/Controllers/ThankyouController.php $ touch src/Controllers/VoteInterfaceController.php
$ 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 $
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);
}
?>"autoload": { "psr-4": { "Raines\\Serverless\\": "src", "Vote\\Controllers\\": "src/Controllers/", "Twig\\": "vendor/twig/twig/lib" } },
$ 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
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.
<?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 '';
}
}
?><?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'],
]
);
}
}
?>
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.
$ mkdir -p src/Views $ touch src/Views/layout.html $ touch src/Views/thank-you.html $ touch src/Views/vote.html
$ 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
$ sls syncToS3 $ sls deploy
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.