The src/php-wasm
module brings PHP into JavaScript as a WebAssembly module:
import { createPHP } from 'php-wasm';
const PHPLoaderModule = await import('/php.js');
const php = await createPHP(PHPLoaderModule);
console.log(php.run(`<?php echo "Hello from PHP!";`).stdout);
// Output: "Hello from PHP!"
It consists of two major building blocks:
See also the API Reference.
The pipeline lives in wasm/Dockerfile
. It was originally forked from seanmorris/php-wasm
In broad strokes, that Dockerfile
:
- Installs all the necessary linux packages (like
build-essential
) - Downloads PHP and the required libraries, e.g.
sqlite3
. - Applies a few patches.
- Compiles everything using Emscripten, a drop-in replacement for the C compiler.
- Compiles
php_wasm.c
– a convenient API for JavaScript. - Outputs a
php.wasm
file and one or more JavaScript loaders, depending on the configuration. - Transforms the Emscripten's default
php.js
output into an ESM module with additional features.
To find out more about each step, refer directly to the Dockerfile.
To build, run npm run build:php:web
in the repository root. You'll find the output files in src/php-wasm/build-wasm
.
PHP is built with several extensions listed in the Dockerfile
.
Some extensions, like zip
, can be turned on or off during the build. Others, like sqlite3
, are hardcoded.
If you need to turn off one of the hardcoded extensions, feel free to open an issue in this repo. Better yet, this project needs contributors. You are more than welcome to open a PR and author the change you need.
The C API exposed to JavaScript lives in the wasm/build-assets/php_wasm.c
file. The most important functions are:
void phpwasm_init()
– It creates a new PHP context and must be called before running any PHP code.int phpwasm_run(char *code)
– Runs a PHP script and writes the output to /tmp/stdout and /tmp/stderr. Returns the exit code.void phpwasm_refresh()
– Destroy the current PHP context and starts a new one. Call it after running one PHP script and before running another.
Refer to the inline documentation in php_wasm.c
to learn more.
The build is configurable via the Docker --build-arg
feature. Currently it's not possible to specify these options via npm run build:php:web
so you'll need to run docker build
manually. For example, to change the PHP version, specify the PHP_VERSION
option during the build:
docker build . --build-arg PHP_VERSION=7.4.0
Supported build options:
PHP_VERSION
– The PHP version to build, default:8.0.24
. This value must point to an existing branch of the https://github.com/php/php-src.git repository when prefixed withPHP-
. For example,7.4.0
is valid because the branchPHP-7.4.0
exists, but just7
is invalid because there's no branchPHP-7
. The PHP versions that are known to work are7.4.*
and8.0.*
. Others likely work as well but they haven't been tried.EMSCRIPTEN_ENVIRONMENT
–web
ornode
, default:web
. The platform to build for. When building forweb
, two JavaScript loaders will be created:php-web.js
andphp-webworker.js
. When building for Node.js, only one loader calledphp-node.js
will be created.WITH_LIBXML
–yes
orno
, default:no
. Whether to build withlibxml2
and thedom
,xml
, andsimplexml
PHP extensions (DOMDocument
,SimpleXML
, ..).WITH_LIBZIP
–yes
orno
, default:yes
. Whether to build withzlib
,libzip
, and thezip
PHP extension (ZipArchive
).WITH_VRZNO
–yes
orno
, default:yes
when PHP_VERSION is 7.*. Whether to build with thevrzno
PHP extension that enables running JavaScript code from PHP.WITH_NODEFS
–yes
orno
, default:no
. Whether to include the Emscripten's NODEFS JavaScript library. It's useful for loading files and mounting directories from the local filesystem when running php.wasm from Node.js.
The php.js
file generated by this build process is not a vanilla Emscripten module. Instead, it's an ESM module that wraps the regular Emscripten output and adds some extra functionality.
Here's the API it exposes:
// php.wasm size in bytes:
export const dependenciesTotalSize = 5644199;
// php.wasm filename:
export const dependencyFilename = 'php.wasm';
// Run Emscripten's generated module:
export default function (jsEnv, emscriptenModuleArgs) {}
php-wasm
provides a JavaScript API to interact with the WebAssembly
module. It lives in the src
directory and consists of:
- A
PHP
class to directly interface with the WebAssembly module. - A
PHPServer
class to use PHP for handling HTTP requests. - A
PHPBrowser
class to handle cookies and redirects emitted byPHPServer
To build the JavaScript API, run npm run build:js
in the repository root.
Below you'll find a few especially relevant parts of the API. Consult the php-wasm API reference page to learn about the rest of it.
startPHP(
phpLoaderModule: any,
runtime: JavascriptRuntime,
phpModuleArgs?: any,
dataDependenciesModules?: any[]
): Promise<PHP>
phpLoaderModule
– The ESM-wrapped Emscripten module. Consult the Dockerfile for the build process.runtime
– The current JavaScript environment. One of: NODE, WEB, or WEBWORKER.phpModuleArgs
– Optional. The Emscripten module arguments, see https://emscripten.org/docs/api_reference/module.html#affecting-execution.dataDependenciesModules
– Optional. A list of the ESM-wrapped Emscripten data dependency modules.- Returns: PHP instance.
Initializes the PHP runtime with the given arguments and data dependencies.
This function handles the entire PHP initialization pipeline. In particular, it:
- Instantiates the Emscripten PHP module
- Wires it together with the data dependencies and loads them
- Ensures is all happens in a correct order
- Waits until the entire loading sequence is finished
Basic usage:
const phpLoaderModule = await import("/php.js");
const php = await startPHP(phpLoaderModule, "web");
console.log(php.run(`<?php echo "Hello, world!"; `));
// { stdout: ArrayBuffer containing the string "Hello, world!", stderr: [''], exitCode: 0 }
The /php.js
module:
In the basic usage example, php.js
is not a vanilla Emscripten module. Instead,
it's an ESM module that wraps the regular Emscripten output and adds some
extra functionality. It's generated by the Dockerfile shipped with this repo.
Here's the API it provides:
// php.wasm size in bytes:
export const dependenciesTotalSize = 5644199;
// php.wasm filename:
export const dependencyFilename = 'php.wasm';
// Run Emscripten's generated module:
export default function(jsEnv, emscriptenModuleArgs) {}
PHP Filesystem:
Once initialized, the PHP has its own filesystem separate from the project files. It's provided by Emscripten and uses its FS library.
The API exposed to you via the PHP class is succinct and abstracts await certain unintuitive parts of low-level FS interactions.
Here's how to use it:
// Recursively create a /var/www directory
php.mkdirTree('/var/www');
console.log(php.fileExists('/var/www/file.txt'));
// false
php.writeFile('/var/www/file.txt', 'Hello from the filesystem!');
console.log(php.fileExists('/var/www/file.txt'));
// true
console.log(php.readFile('/var/www/file.txt'));
// "Hello from the filesystem!
// Delete the file:
php.unlink('/var/www/file.txt');
For more details consult the PHP class directly.
Data dependencies:
Using existing PHP packages by manually recreating them file-by-file would be quite inconvenient. Fortunately, Emscripten provides a "data dependencies" feature.
Data dependencies consist of a dependency.data
file and a dependency.js
loader and
can be packaged with the file_packager.py tool.
This project requires wrapping the Emscripten-generated dependency.js
file in an ES
module as follows:
- Prepend
export default function(emscriptenPHPModule) {';
- Prepend
export const dependencyFilename = '<DATA FILE NAME>';
- Prepend
export const dependenciesTotalSize = <DATA FILE SIZE>;
- Append
}
Be sure to use the --export-name="emscriptenPHPModule"
file_packager.py option.
You want the final output to look as follows:
export const dependenciesTotalSize = 5644199;
export const dependencyFilename = 'dependency.data';
export default function(emscriptenPHPModule) {
// Emscripten-generated code:
var Module = typeof emscriptenPHPModule !== 'undefined' ? emscriptenPHPModule : {};
// ... the rest of it ...
}
Such a constructions enables loading the dependency.js
as an ES Module using
import("/dependency.js")
.
Once it's ready, you can load PHP and your data dependencies as follows:
const [phpLoaderModule, wordPressLoaderModule] = await Promise.all([
import("/php.js"),
import("/wp.js")
]);
const php = await startPHP(phpLoaderModule, "web", {}, [wordPressLoaderModule]);
Signature:
class PHPServer
A fake PHP server that handles HTTP requests but does not bind to any port.
PHPServer(php: PHP, config: PHPServerConfigation)
php
– The PHP instance.config
– Server configuration.
Constructs a new instance of the PHPServer
class
absoluteUrl
readonly string – The absolute URL of this PHPServer instance.php
PHP – The PHP instance
request(request: PHPRequest): Promise<PHPResponse>
request
– The request.- Returns: The response.
Serves the request – either by serving a static file, or by dispatching it to the PHP runtime.
import { createPHP, PHPServer } from 'php-wasm';
const PHPLoaderModule = await import('/php.js');
const php = await createPHP(PHPLoaderModule);
// Create a file to serve:
php.mkdirTree('/www');
php.writeFile('/www/index.php', '<?php echo "Hi from PHP!"; ');
// Create a server instance:
const server = new PHPServer(php, {
// PHP FS path to serve the files from:
documentRoot: '/www',
// Used to populate $_SERVER['SERVER_NAME'] etc.:
absoluteUrl: 'http://127.0.0.1'
});
console.log(
server.request({ path: '/index.php' }).body
);
// Output: "Hi from PHP!"