Server Render JavaScript from PHP via an Unix Socket

Server Render JavaScript from PHP via an Unix Socket

Calling JavaScript code from PHP is suprisingly simple. You can render a react app server side, or call upon one of NPM's many libraries.

Calling JavaScript from PHP is simple.

There are many use cases for running JavaScript code from PHP. You can server side render a React app, or call upon one of NPM's many libraries. And I'm going to show how simple it is to do!


Requirements

1. PHP
2. Node.js

You don't need anything out of the ordinary just PHP, and Node.js. Yup 0 external dependencies.


Sockets

What are Sockets?

Sockets are inter-process communication mechanisms that allows bidirectional data exchange.

What? Let me break that down.

Lets say we're working on a document in Microsoft Word. You really want to create an integration between Microsoft Word and Node.js, so that you can format a documents content with different predefined JavaScript functions. In lower level terms, what you want is a way for two running processes (Microsoft Word, and Node.js) on the same machine to communicate.

You want to be able to send the content from Microsoft Word, into Node.js. And then once Node.js has completed it's work, to send it back to Microsoft Word.

Kinda sounds like you need an "inter-process communication mechanism that allows bidirectional data exchange". Thats a Socket. Sockets allow communication between two different processes running on the same or different machines.

More particularly, sockets are simply file descriptors, however that is beyond the scope of this article.


What is an Unix Socket?

We are going to use a particular type of socket known as an Unix Domain Socket (aka Unix Socket).

An Unix Socket is an inter-process communication mechanism that allows bidirectional data exchange between processes running on the same machine.

There are also IP sockets. IP sockets enable communication between processes on the same or different machines via TCP. The benefit of an Unix Socket, is that they avoid the overhead of a TCP/IP connection.

Anthony Headings provides a great analogy in his article What are Unix Sockets and how do they work?.

Sockets are a direct connection between two processes. Imagine if you wanted to call your friend down the road; you could place a call have it routed up through your telephone company and back down to their house, or you could run a wire directly to their house and cut out the middleman. The latter is obviously impractical in real life, but in the world of Unix it’s very common to establish these direct connections between programs.

We communicate with a socket through it's socket file. This file is created automatically by opening a socket.

Like I said, a socket is really just a file descriptor. The only point of the socket file is to maintain a reference to this file descriptor, and to control access to the socket through filesystem permissions.

Okay, enough theory.


Sockets are Interesting!

If you'd like to explore this topic further, here's a few great reads.

Understanding Sockets
What are Unix Sockets and how do they work?
What is the difference between Unix Sockets and TCP/IP Sockets


Open and Listen on an Unix Socket via Node.js

Before jumping in, let me explain how this all works. We need communicate with a Node.js process from a PHP process.

First, we'll need to start up a Node.js server that opens and listens on an Unix Socket.

Then, we'll connect to this socket from within PHP.


Create the Node.js Server.

So, we need a Node.js server that opens an unix socket and listens for data on it.

Typically when we create a server in Node.js, such as an Express server, we'll have it listen on a port. Well, that port is really just an IP socket. So it's the same idea, we're just using a different kind of socket.

We'll create this file as an executable. Save the following to a file called 'node' in a directory bin. No file extension needed, because this is an executable file. Notice the shebang on the 1st line !#/usr/bin/env node.

js
1#!/usr/bin/env node
2
3const net = require('net')
4const fs = require('fs')
5const path = require('path')
6
7// Where to output the socket file.
8const socketPath = path.resolve(root, 'var/node.sock')
9
10// If a socket is already open, we'll replace it.
11if (fs.existsSync(socketPath)) fs.unlinkSync(socketPath)
12
13// We create a server that accepts a socket to listen on. When the socket receives some javascript, the server will run it through `eval()`.
14const server = net.createServer(socket => {
15 socket.setEncoding('utf8')
16
17 // We push each chunk of data, into the chunks array.
18 // After pushing each chunk we also check if the last character is '\0'.
19 // '\0' is a special token, we use to signal that there will be no more content after this. No more chunks.
20 // This means, we can eval the data (js) we recieved confident that we aren't missing anything.
21 const chunks = []
22 socket.on('data', chunk => {
23 chunks.push(chunk)
24 const lastChar = chunk[chunk.length - 1]
25 if (lastChar === '\0') {
26 // Okay we found the '\0' token. So we join all the chunks together into one string.
27 // We eval() the combined chunks.
28 // Then we send the result back to the unix socket via the write() function.
29 const data = chunks.join('').slice(0, -1)
30 const result = eval(data)
31 socket.write(result)
32 console.log('Request processed')
33 socket.end()
34 }
35 })
36})
37
38// We open the unix socket, just like any other Node.js server. Via the listen() function.
39// Unlike an express server, we don't pass a port. Rather we pass the path to the socket we decided on above.
40server.listen(socketPath, () => {
41 // The socket is now open.
42 // The server we created is listening on it.
43 console.log(`Server is listening at: ${socketPath}`)
44})
45
46process.on('SIGINT', () => {
47 server.close()
48 process.exit()
49})
50

Make it executable.

This is an executable file. But, by default it's not executable until we set it's permissions. No problem, simply run the following.

bash
1# Sets the file permissions to be executable.
2
3chmod +x ./bin/node
4

Connect to a Socket from PHP

Okay, great. See I told you this was simple.

Before we connect to the socket from PHP, we'll need to start up the Node.js server we just created.

Because it's an executable file, the command is super simple.

bash
1# Start the node.js server.
2
3./bin/node
4

Sending JavaScript to an Unix Socket from PHP

There are so many cool things we can do here. I'm going to demonstrate how to server render a webpack bundle. And as a bonus I'll also demonstrate how to inject in some server side data such as the current route.

php
1<?php
2
3namespace Acme;
4
5use RuntimeException;
6use Symfony\Component\Serializer\SerializerInterface;
7
8use function fclose;
9use function stream_get_contents;
10use function stream_socket_client;
11use function stream_socket_sendto;
12
13class NodeRenderer {
14
15 /**
16 * Some server side data you'd like to inject.
17 */
18 private object $serverContext;
19
20 /**
21 * A serializer for converting the server context to json.
22 */
23 private SerializerInterface $serializer;
24
25 /**
26 * Path or url to an open socket.
27 */
28 private string $socketPath;
29
30 /**
31 * Path to your JavaScript bundle.
32 */
33 private string $bundlePath;
34
35 public function __construct(
36 SerializerInterface $serializer,
37 string $socketPath,
38 string $bundlePath,
39 object $serverContext
40 ) {
41 $this->serializer = $serializer;
42 $this->socketPath = $socketPath;
43 $this->bundlePath = $bundlePath;
44 $this->serverContext = $serverContext;
45 }
46
47 public function render(): string {
48
49 // First we'll serialize the server side data that we're injecting into our JavaScript.
50 $context = $this->serializer->serialize($this->serverContext, 'json');
51
52 // This is the javascript you'd like eval'd.
53 // It can be anything.
54 // In this case we're going to pass our webpack bundle to Node.js to server render.
55 // We can pass important data like the current route via the server context we pass in.
56 // The possibilities are endless.
57 $javascript = <<<JS
58 (function() {
59 const Bundle = require($this->bundlePath);
60 return Bundle($context);
61 })();
62 JS;
63
64 // Next we connect to the open socket.
65 $socket = stream_socket_client($this->socketPath, $errno, $errstr);
66 if (!$socket) {
67 throw new RuntimeException($this->socketPath . " does not reference an opened socket. Did you forget to start the Node.js server that opens the socket?");
68 }
69
70 // Here we pass the JavaScript to the socket.
71 // Note the special token "\0". This is used by Node.js, to know theres no more js to wait on.
72 stream_socket_sendto($socket, $javascript . "\0");
73
74 // Annnnd.. we get the result back from le socket.
75 $contents = stream_get_contents($socket);
76 if ($contents === false) {
77 throw new RuntimeException("Server blew up or something. Idk. Ask dev ops. It's their fault.");
78 }
79
80 fclose($sock);
81
82 // And we return the contents to the caller.
83 return $contents;
84 }
85}
86

Configuring your Webpack Bundle

Seperate Server and Client bundles.

If you're going to attempt to server render a webpack bundle, you'll want to create seperate server and client side bundles.

Here's a sample webpack configuration for a React app, written in TypeScript.

It will output the client side bundle to public/build, and the server side bundle to var/node.

As a bonus, install run-node-webpack-plugin to automatically start up your Node.js server, when bundling. It has tons of customization options.

Here I've configured the plugin to start the server only when running webpack in watch mode, and to restart the server upon re-bundle.

Another important detail is the server side bundles output configuration. If you noticed in the PHP script above, we required the server webpack bundle, and then called it as if it were a function. In order for this to be possible, the bundle must be output as a UMD bundle.

js
1const path = require('path')
2const RunNodeWebpackPlugin = require('run-node-webpack-plugin')
3const isEnvProduction = process.env.NODE_ENV === 'production'
4
5module.exports = [
6 {
7 target: 'web',
8 mode: isEnvProduction ? 'production' : 'development',
9 devtool: isEnvProduction ? 'none' : 'inline-source-map',
10 entry: {
11 client: './src/Client.tsx'
12 },
13 output: {
14 filename: 'client.[chunkhash].js',
15 path: path.resolve('public/build'),
16 clean: true
17 },
18 module: {
19 rules: [
20 {
21 test: /\.(ts|js)x?$/,
22 exclude: /node_modules/,
23 use: {
24 loader: 'babel-loader',
25 options: {
26 presets: [
27 '@babel/preset-env',
28 ['@babel/preset-react', { runtime: 'automatic' }],
29 '@babel/preset-typescript'
30 ]
31 }
32 }
33 }
34 ]
35 },
36 optimization: {
37 minimize: isEnvProduction
38 },
39 plugins: [],
40 resolve: {
41 extensions: ['.tsx', '.ts', '.js', '.jsx', '.json']
42 }
43 },
44 {
45 target: 'node',
46 mode: isEnvProduction ? 'production' : 'development',
47 entry: {
48 server: './src/Server.tsx'
49 },
50 output: {
51 filename: 'bundle.js',
52 path: path.resolve('var/node'),
53 library: {
54 name: 'Server',
55 type: 'umd',
56 export: 'default'
57 }
58 },
59 module: {
60 rules: [
61 {
62 test: /\.(ts|js)x?$/,
63 exclude: /node_modules/,
64 use: {
65 loader: 'babel-loader',
66 options: {
67 presets: [
68 '@babel/preset-env',
69 ['@babel/preset-react', { runtime: 'automatic' }],
70 '@babel/preset-typescript'
71 ]
72 }
73 }
74 }
75 ]
76 },
77 plugins: [
78 new RunNodeWebpackPlugin({
79 scriptToRun: './bin/node',
80 runOnlyOnChanges: false,
81 runOnlyInWatchMode: true
82 })
83 ],
84 resolve: {
85 extensions: ['.tsx', '.ts', '.js', '.jsx', '.json']
86 }
87 }
88]
89

- Comments -