Php-Cgi-Wasm for Node.js
PhpCgiNode runs the CGI build of PHP inside
Node.js and lets you serve requests through a normal Node HTTP
server.
Use it when you want PHP to behave like a web application instead of an embedded expression runner. In practice that means:
- requests go through
php.request(...) - PHP reads from a document root
- cookies and CGI environment variables are managed for you
- files can persist to host directories with NodeFS mounts
Install
npm i php-cgi-wasmIf you want runtime-loadable extensions, install the packages you plan to use as well:
npm i php-wasm-intl php-wasm-libxml php-wasm-phar php-wasm-mbstring php-wasm-openssl php-wasm-dom php-wasm-xml php-wasm-simplexml php-wasm-sqlite php-wasm-zlib php-wasm-gdMinimal HTTP server
This is the basic pattern:
#!/usr/bin/env node
import http from 'node:http';
import { PhpCgiNode } from 'php-cgi-wasm/PhpCgiNode';
const php = new PhpCgiNode({
prefix: '/php-wasm/cgi-bin/',
docroot: '/persist/www',
persist: [
{ mountPath: '/persist', localPath: './persist' },
{ mountPath: '/config', localPath: './config' },
],
});
const server = http.createServer(async (request, response) => {
const result = await php.request(request);
const reader = result.body.getReader();
response.writeHead(result.status, [...result.headers.entries()].flat());
let done = false;
while(!done)
{
const chunk = await reader.read();
done = chunk.done;
if(chunk.value)
{
response.write(chunk.value);
}
}
response.end();
});
server.listen(3003);Open http://localhost:3003/php-wasm/cgi-bin/
after creating a PHP app under ./persist/www.
If you’re running CommonJS instead of ESM, use
const { PhpCgiNode } = require('php-cgi-wasm/PhpCgiNode');
and keep the rest of the constructor options the same.
Loading extensions
When your active library mode is dynamic, pass
extension packages as sharedLibs:
const php = new PhpCgiNode({
prefix: '/php-wasm/cgi-bin/',
docroot: '/persist/www',
persist: [
{ mountPath: '/persist', localPath: './persist' },
{ mountPath: '/config', localPath: './config' },
],
sharedLibs: [
await import('php-wasm-intl'),
await import('php-wasm-libxml'),
await import('php-wasm-phar'),
await import('php-wasm-mbstring'),
await import('php-wasm-openssl'),
await import('php-wasm-dom'),
await import('php-wasm-xml'),
await import('php-wasm-simplexml'),
await import('php-wasm-sqlite'),
await import('php-wasm-zlib'),
await import('php-wasm-gd'),
],
});sharedLibs works the same way here as it does in
the other runtimes: extension packages resolve their
.so files and supporting libraries
automatically.
The extension helper JS packages remain ESM-only. In
CommonJS, do not require() those helper packages.
Manage extension .so, .data,
.wasm, and support-library assets manually through
sharedLibs, dynamicLibs,
files, and locateFile, as described in
the extensions guide.
Important options
docroot
The PHP document root inside the emscripten filesystem.
docroot: '/persist/www'With the persist mounts above, that maps to
./persist/www on the host.
prefix
The URL prefix that identifies requests meant for this PHP application.
prefix: '/php-wasm/cgi-bin/'persist
NodeFS mounts. These expose host directories to PHP.
persist: [
{ mountPath: '/persist', localPath: './persist' },
{ mountPath: '/config', localPath: './config' },
]This is how you keep application files, sessions, caches, and config across requests.
types
Optional MIME type map for static files served through the CGI wrapper.
types: {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
png: 'image/png',
svg: 'image/svg+xml',
}version
Selects the PHP-CGI runtime version to load.
PhpCgiNode currently defaults to
8.4.
version: '8.5'rewrite,
entrypoint, exclude, env,
notFound, onRequest
These work the same way as the CGI worker/web runtimes documented in php-cgi-wasm methods:
rewriterewrites incoming paths before routingentrypointforces a single PHP entry script such asindex.phpexcludebypasses PHP for matching URL prefixesenvsets CGI environment variablesnotFoundreturns a custom 404 responseonRequestlets you inspect each request/response pair
What
php.request() expects
PhpCgiNode accepts a Node HTTP request object
directly:
const result = await php.request(request);Internally it normalizes the Node request into a URL plus
headers map, then returns a standard Response
object. That is why the server bridge above reads:
result.statusresult.headersresult.body.getReader()
Cookies and config
The CGI runtime keeps its cookie jar under
/config/.cookies. If /config is
persisted to disk, cookies survive process restarts along with
the rest of your mounted config.
The runtime also sets PHP_INI_SCAN_DIR to
include /config, /preload, and the
active document root, so project-local configuration files can
be loaded from those paths.
Multi-app routing
If you need to host more than one PHP app behind one Node
server, PhpCgiBase also supports
vHosts entries with:
pathPrefixdirectoryentrypoint
That lets one runtime serve multiple PHP applications by URL prefix from different directories.
Related source
The current Node demo in the upstream repo lives at:
The PhpCgiNode class itself is implemented
at: