Since we are making a PHP application we really should stick to the recognised right way of doing this; the MVC pattern is the established best practice. Sitting in front of this pattern is the Front Controller which will act as a controller to determine which C of the MVC will actually be called, and to do a bit of the routing job, and to act as the layer between our actual code and the hander,js / handler.php shim that comes with serverless-php.
In fact we get a starter Front Controller in the serverless-php repo already although it is very basic and will need a little extending to serve a purpose in a MVC web app.
The name of the game is to keep whatever we develop to be very lightweight. Whilst it would be good to get Symfony / Zend / Laravel running under serverless-php, that will come at a cost of execution time and development time.
So the 'Front Controller' is called HelloHandler.php in the src/ directory but you should've renamed it VoteHandler.php already and changed the name of the class it contains.
The second consideration is the AWS API Gateway - that too needs to know which routes we are defining. We can accomplish that by configuration in the serverless.yml file.
As previously mentioned our voting app is going to present a form (HTTP GET), then when submitted (HTTP POST) it will cache the name of the voter and that person's choice in a NoSQL data store, then redirect to a thank you page (HTTP POST). Note that the redirect must be a POST since Google don't take kindly to URL parameters containing personal data, and we want to display the voter's name on the thank you page.
functions: vote_get: handler: handler.handle environment: HANDLER: handler.vote # This is the service name which will be used (from services.yml) events: - http: path: / method: get vote_post: handler: handler.handle environment: HANDLER: handler.vote # This is the service name which will be used (from services.yml) events: - http: path: / method: post thank_you: handler: handler.handle environment: HANDLER: handler.vote # This is the service name which will be used (from services.yml) events: - http: path: /thank_you method: post
$ 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 (9.96 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

If we log into the AWS Console and navigate to the API Gateway we can see our functions are declared correctly.
<?php
namespace Raines\Serverless;
use Psr\Log\LoggerInterface;
class VoteHandler implements Handler
{
/**
* Routing table
*/
protected $routing = [
[
'path' => '/',
'method' => 'GET',
'controller' => 'Form',
'action' => 'index'
],
[
'path' => '/',
'method' => 'POST',
'controller' => 'Form',
'action' => 'save'
],
[
'path' => '/thank_you',
'method' => 'POST',
'controller' => 'Thankyou',
'action' => 'index'
],
];
/**
* {@inheritdoc}
*/
public function handle(array $event, Context $context)
{
$logger = $context->getLogger();
$logger->notice('Got event', $event);
// get the controller and action based on the path and method
$route = [];
foreach($this->routing as $k => $r) {
if ($r['path'] == $event['path'] && $r['method'] == $event['httpMethod']) {
$route[] = $this->routing[$k];
}
}
try {
if (count($route) != 1) {
throw new \Exception('Inconsistent routing provided');
}
// Construct the controller name
$classname = "Vote\Controllers\\{$route[0]['controller']}Controller";
// See if classname and action both exist
if (!class_exists($classname) || !method_exists($classname, $route[0]['action'])) {
throw new \Exception('Unknown classname or action provided');
}
} catch (\Exception $e) {
return [
'headers' => ['Content-Type' => 'application/json'],
'statusCode' => 500,
'body' => json_encode($e->getMessage(), JSON_HEX_QUOT),
];
}
// Create the controller
$controller = $this->createController($classname, $logger, $event);
// Execute the action
$output = $controller->{$route[0]['action']}();
return [
'headers' => $controller->__get('headers'),
'statusCode' => $controller->__get('statusCode'),
'body' => $output,
];
}
/**
* Instantiate the controller
*
* @param string $classname
* @param LoggerInterface $logger
* @param array $event
*
* @return object
*/
private function createController(string $classname, LoggerInterface $logger, array $event)
{
return new $classname($logger, $event);
}
}
?>
<?php
$loader = require __DIR__ . '/vendor/autoload.php';
use Raines\Serverless\Context;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
// Set up service container
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/config'));
$loader->load('services.yml');
// Get event data and context object
$event = json_decode($argv[1], true) ?: [];
$logger = $container->get('logger');
$fd = fopen('php://fd/3', 'r+');
$context = new Context($logger, $argv[2], $fd);
// Get the handler service and execute
$handler = $container->get(getenv('HANDLER'));
$response = $handler->handle($event, $context);
// Send data back to shim
printf(json_encode($response, JSON_HEX_QUOT));
?>
We are now ready to run our code from the command line locally. Of course it isn't going to do too much since we've not added the MVC part of the code yet! However I would expect it to get to the point it tries to instantiate a controller before throwing an exception.
The syntax for local invocation is below. Note the JSON structure passed using the flag --data - this will be interpreted by our Front Controller in the populated $event array. The -f flag denotes the Lambda function we are running.
$ sls invoke local -f vote_get --data '{"path":"/","httpMethod":"POST"}' Got event {"path":"/","httpMethod":"POST"} [] { "headers": { "Content-Type": "application/json" }, "statusCode": 500, "body": "Unknown classname or action provided" }