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-wasm

If 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-gd

Minimal 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:

  • rewrite rewrites incoming paths before routing
  • entrypoint forces a single PHP entry script such as index.php
  • exclude bypasses PHP for matching URL prefixes
  • env sets CGI environment variables
  • notFound returns a custom 404 response
  • onRequest lets 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.status
  • result.headers
  • result.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:

  • pathPrefix
  • directory
  • entrypoint

That lets one runtime serve multiple PHP applications by URL prefix from different directories.

The current Node demo in the upstream repo lives at:

The PhpCgiNode class itself is implemented at: