Robin van der Vleuten

JavaScript rendering in PHP with Lambda

As a web developer, you have probably run into polymorphic JavaScript, or universal JavaScript. Whatever name you prefer, the idea is the same: render the same JavaScript application in more than one environment.

For PHP applications, that usually means finding a way to run JavaScript from PHP. One option is AWS Lambda.

If you are not familiar with Lambda, it is a serverless compute service. You upload code as functions, and AWS handles scaling and availability. Those functions can then be invoked from web or mobile applications.

At the time, there were two common ways to run JavaScript from PHP: install the V8js extension, which embeds the V8 engine in your PHP app, or let PHP and Node.js talk through the DNode RPC protocol.

With Lambda, you can upload JavaScript to AWS and call it through the AWS API. Start with a small "Hello, World!" function:

js
/**
* Very simple Lambda which just returns a string on invocation.
*/
exports.handler = function (event, context) {
context.succeed('Hello, World!')
}

Upload that code to your AWS account. Tom Bray wrote a useful tutorial that uses the same kind of example.

Once the function runs in the AWS console, invoke it through the AWS PHP SDK:

php
<?php
require_once __DIR__.'/vendor/autoload.php';
use Aws\Lambda\LambdaClient;
$client = LambdaClient::factory([
'version' => 'latest',
// The region where you have created your Lambda
'region' => 'eu-west-1',
]);
$result = $client->invoke([
// The name of your created Lambda function
'FunctionName' => 'hello-world',
]);
echo json_decode((string) $result->get('Payload'));
// You'll now see "Hello, World! in your output"

The result comes back as a JSON encoded payload. The example is basic, but it is enough to show the shape of the integration. Next, render a small React application inside a simple Silex application.

Suppose we have a React component that renders a simple string:

js
import React from 'react'
const Application = (props) => <div>Hello, {props.name}!</div>
export default Application

That component can be rendered on the client side like this:

html
<html>
<head>
<meta charset="utf-8" />
<title>php-react-lamda-example</title>
</head>
<body>
<div id="root"></div>
<script>
window.__INITIAL_PAYLOAD__ = { name: 'World' }
</script>
<script src="/bundle.js"></script>
</body>
</html>
js
import React from 'react'
import { render } from 'react-dom'
import Application from './app'
const payload = window.__INITIAL_PAYLOAD__
render(<Application name={payload.name} />, document.getElementById('root'))

The client reads the name property from the payload and renders the component into the #root element.

To render the component on Lambda, upload the component code with this handler:

js
import React from 'react'
import { renderToString } from 'react-dom/server'
import Application from '../client/app'
export function handler(event, context) {
const markup = renderToString(<Application name={event.name} />)
context.succeed(markup)
}

Instead of rendering into an HTML element, the handler passes the resulting string to the Lambda function's succeed callback.

Then invoke the Lambda function from a small PHP application:

php
<?php
$filename = __DIR__.'/../dist/'.preg_replace('#(\?.*)$#', '', $_SERVER['REQUEST_URI']);
if (php_sapi_name() === 'cli-server' && is_file($filename)) {
return false;
}
require_once __DIR__.'/../vendor/autoload.php';
use Aws\Lambda\LambdaClient;
use Silex\Application;
$app = new Application();
$app['debug'] = true;
$app->get('/', function () use ($app) {
$client = LambdaClient::factory([
'version' => 'latest',
'region' => 'eu-west-1',
]);
$payload = json_encode(['name' => 'Server']);
$result = $client->invoke([
'FunctionName' => 'php-react-lamda-example',
'Payload' => $payload,
]);
$markup = json_decode((string) $result->get('Payload'));
return <<<EOF
<html>
<head>
<meta charset="utf-8">
<title>php-react-lamda-example</title>
</head>
<body>
<div id="root">$markup</div>
<script>
window.__INITIAL_PAYLOAD__ = $payload;
</script>
<script src="/bundle.js"></script>
<body>
</html>
EOF;
});
$app->run();

When someone opens the root route, the PHP app invokes the Lambda function and passes a JSON encoded payload. Lambda returns a React markup string, which the PHP app uses for the server-rendered HTML. The same payload is also passed to the client.

This is a basic example, but the same shape can be extended for larger PHP and JavaScript applications. The full source, including the webpack build, is available on GitHub.