PHP in WebAssembly, npm not required.
npm | github | unpkg | reddit | discord
php-wasm,php-cgi-wasm,php-cli-wasm,php-dbg-wasm,php-sdl-wasm,php-cloud-wasm, andphp-wasm-builderare published separately.- Published runtimes currently cover PHP
8.0through8.5, depending on the package and entrypoint. - Runtimes default to PHP
8.4, exceptPhpCliWeb, which defaults to8.3.PhpNode,PhpCliNode, andPhpDbgNodeuse thePHP_VERSIONenvironment variable instead when it names a supported version. Passversionexplicitly when your asset filenames need to line up. - Runtime-loadable libraries are available for
gd,iconv,intl,libxml,xml,dom,simplexml,xmlreader,xmlwriter,yaml,zip,mbstring,openssl,phar,sqlite,tidy, andzlib. - Vrzno, pdo_cfd1, and pdo_pglite are maintained as separate packages.
php-cloud-wasmis a dedicated static ES-module build for Cloudflare Workers and Pages, tested with local workerd for PHP8.0through8.5.- The standalone
php-sdl-wasmbrowser runtime supports SDL graphics, input, image loading, TrueType text, audio, and OpenGL shaders, with a textured cube example. See the SDL guide for canvas setup, build flags, and supported APIs.
Install the packages you need:
$ npm i php-wasm
$ npm i php-cgi-wasm
$ npm i php-cli-wasm
$ npm i php-dbg-wasm
$ npm i php-sdl-wasm
$ npm i php-cloud-wasm
$ npm i php-wasm-builder| Drupal Demo | CakePHP Demo | CodeIgniter Demo | Laravel Demo | Laminas Demo | Code Editor |
php-cgi-wasm runs PHP in web-server mode, similar to Apache or nginx. Running within a Service Worker, it can intercept and respond to HTTP requests just like a normal web server. This means the browser can simply navigate to a URL and let PHP generate the page, with AJAX and other in-page requests still flowing over normal HTTP.
$ npm install php-cgi-wasmimport { PhpCgiWorker } from "php-cgi-wasm/PhpCgiWorker";
// Spawn the PHP-CGI binary
const php = new PhpCgiWorker({
prefix: '/php-wasm',
docroot: '/persist/www',
types: {
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
png: 'image/png',
svg: 'image/svg+xml',
}
});
// Set up the event handlers
self.addEventListener('install', event => php.handleInstallEvent(event));
self.addEventListener('activate', event => php.handleActivateEvent(event));
self.addEventListener('fetch', event => php.handleFetchEvent(event));
self.addEventListener('message', event => php.handleMessageEvent(event));You can see examples of php-cgi-wasm running in a service worker and Node.js in demo-web/src/workers/cgi-worker.mjs and demo-node/index.mjs respectively.
Note: php-cgi-wasm and php-wasm are separate packages. One embeds PHP directly into your JavaScript runtime; the other runs in CGI mode, like PHP under Apache or nginx.
You can find documentation specific to php-cgi-wasm here.
Install php-wasm with npm:
$ npm install php-wasmInclude the module:
import { PhpWeb } from 'php-wasm/PhpWeb.mjs';
const php = new PhpWeb;Note: This does not require npm.
const { PhpWeb } = await import('https://cdn.jsdelivr.net/npm/php-wasm/PhpWeb.mjs');
const php = new PhpWeb;const { PhpWeb } = await import('https://unpkg.com/php-wasm/PhpWeb.mjs');
const php = new PhpWeb;Each runtime module loads its WebAssembly binary with
new URL('<hash>.wasm', import.meta.url). Bundlers that understand this
pattern, such as Vite and webpack 5, emit the binary automatically.
Otherwise, copy the binary referenced by each runtime you import next to the bundled module. It is named by its SHA-1 hash, which changes with every build:
grep -o '[0-9a-f]\{40\}\.wasm' node_modules/php-wasm/php8.4-web.mjs | sort -u
grep -o '[0-9a-f]\{40\}\.wasm' node_modules/php-cgi-wasm/php8.4-cgi-worker.mjs | sort -uRepeat this after upgrading, and for each PHP version and runtime you import.
Core Node runtimes support both ESM and CommonJS.
For 0.2.0, use the published entrypoints across the runtime packages:
php-wasm/PhpNodephp-cgi-wasm/PhpCgiNodephp-cli-wasm/PhpCliNodephp-dbg-wasm/PhpDbgNode
Browser/runtime helper entrypoints such as PhpWeb and php-tags remain ESM-first.
Include the php-tags module from a CDN:
<script async type = "module" src = "https://cdn.jsdelivr.net/npm/php-wasm/php-tags.mjs"></script>To serve the installed package locally, expose its directory through your HTTP
server and use its public URL, for example /node_modules/php-wasm/php-tags.mjs.
Keep the package's relative module paths and matching JavaScript/Wasm assets
together. A relative URL such as node_modules/... resolves against the page's
directory, so nested pages may need a leading / or a different public path.
The loader waits for the initial document to finish parsing, including when
an async module in <head> loads before <body> exists.
And run some PHP right in the page!
<script type = "text/php" data-stdout = "#output">
<?php phpinfo();
</script>
<div id = "output"></div>Inline PHP can use standard input, output, and error with data- attributes. Set each attribute value to a selector that matches the corresponding element.
<script async type = "module" src = "https://cdn.jsdelivr.net/npm/php-wasm/php-tags.mjs"></script>
<script id = "input" type = "text/plain">Hello, world!</script>
<script type = "text/php" data-stdin = "#input" data-stdout = "#output" data-stderr = "#error">
<?php echo file_get_contents('php://stdin');
</script>
<div id = "output"></div>
<div id = "error"></div>The src attribute can be used on <script type = "text/php"> tags, as well as their input elements. For example:
<html>
<head>
<script async type = "module" src = "https://cdn.jsdelivr.net/npm/php-wasm/php-tags.mjs"></script>
<script id = "input" src = "/test-input.json" type = "text/json"></script>
<script type = "text/php" src = "/test.php" data-stdin = "#input" data-stdout = "#output" data-stderr = "#error"></script>
</head>
<body>
<div id = "output"></div>
<div id = "error"></div>
</body>
</html><script async type = "module" src = "https://cdn.jsdelivr.net/npm/php-wasm/php-tags.mjs"></script><script async type = "module" src = "https://unpkg.com/php-wasm/php-tags.mjs"></script>Create a PHP instance:
const { PhpWeb } = await import('https://cdn.jsdelivr.net/npm/php-wasm/PhpWeb.mjs');
const php = new PhpWeb;Add your output listeners:
// Listen to STDOUT
php.addEventListener('output', (event) => {
console.log(event.detail);
});
// Listen to STDERR
php.addEventListener('error', (event) => {
console.log(event.detail);
});Provide some input data on STDIN if you need to:
php.inputString('This is a string of data provided on STDIN.');... then run some PHP!
const exitCode = await php.run('<?php echo "Hello, world!";');run() accepts complete PHP source, including an initial
<?php declare(strict_types=1);, namespaces, and mixed PHP/HTML. A leading
PHP opening tag does not introduce output before the declaration, and line
numbers are preserved. Leading HTML, whitespace outside PHP, or a UTF-8 BOM
still count as output and cannot precede strict_types.
Dynamic extensions can be loaded in static webpages like so:
<script async type = "module" src = "https://cdn.jsdelivr.net/npm/php-wasm/php-tags.mjs"></script>
<script type = "text/php" data-stdout = "#output" data-stderr = "#error" data-libs = '[
{"url": "https://unpkg.com/php-wasm-yaml/php8.4-yaml.so", "ini": true},
{"url": "https://unpkg.com/php-wasm-yaml/libyaml.so", "ini": false}
]'><?php
print yaml_emit([1,2,3,"string",["k1" => "value", "k2" => "value2", "k3" => "value3"],"now" => date("Y-m-d h:i:s")]);
</script>The example above assumes the default php-tags runtime version (8.4). If you set data-version to something else, update the php8.x-*.so filenames to match.
You can pass in the ini property to the constructor to add lines to /php.ini:
const php = new PhpWeb({ini: `
date.timezone=${Intl.DateTimeFormat().resolvedOptions().timeZone}
tidy.clean_output=1
expose_php=0
`});The /config/php.ini and /preload/php.ini files will also be loaded, if they exist. Neither of these files will be created if they do not exist. They're left completely up to the programmer to create & populate.
Options like the following may appear in these files. See the PHP docs for the full list.
[php]
date.timezone=UTC
tidy.clean_output=1
expose_php=0When running in CGI mode, php will look for a php.ini file in the document root directory, and load it along with the files listed above.
PHP will replace strings in INI files in the form ${ENVIRONMENT_VARIABLE} with the env value of ENVIRONMENT_VARIABLE. The PHP_VERSION environment variable is available to allow loading of the extension compatible with the currently running version of PHP:
[php]
extension=php${PHP_VERSION}-phar.soRemember to correctly escape the $ if you're supplying the INI from JavaScript with backticks:
const php = new PhpWeb({ini: `
extension=php\${PHP_VERSION}-phar.so
date.timezone=${Intl.DateTimeFormat().resolvedOptions().timeZone}
`});The following extensions may be loaded at runtime. This allows the shared extensions and their dependencies to be cached, reused, and selected a la carte for each application.
- gd (https://www.npmjs.com/package/php-wasm-gd)
- iconv (https://www.npmjs.com/package/php-wasm-iconv)
- intl (https://www.npmjs.com/package/php-wasm-intl)
- libxml (https://www.npmjs.com/package/php-wasm-libxml)
- xml (https://www.npmjs.com/package/php-wasm-xml)
- dom (https://www.npmjs.com/package/php-wasm-dom)
- simplexml (https://www.npmjs.com/package/php-wasm-simplexml)
- xmlreader (https://www.npmjs.com/package/php-wasm-xmlreader)
- xmlwriter (https://www.npmjs.com/package/php-wasm-xmlwriter)
- yaml (https://www.npmjs.com/package/php-wasm-yaml)
- zip (https://www.npmjs.com/package/php-wasm-libzip)
- mbstring (https://www.npmjs.com/package/php-wasm-mbstring)
- openssl (https://www.npmjs.com/package/php-wasm-openssl)
- phar (https://www.npmjs.com/package/php-wasm-phar)
- sqlite (https://www.npmjs.com/package/php-wasm-sqlite)
- pdo-sqlite (https://www.npmjs.com/package/php-wasm-sqlite)
- tidy (https://www.npmjs.com/package/php-wasm-tidy)
- zlib (https://www.npmjs.com/package/php-wasm-zlib)
There are two ways to load extensions at runtime, using the dl() function or php.ini.
<?php
dl('php8.4-xml.so');
dl('php8.4-dom.so');Or pass a sharedLibs array to the constructor from JavaScript to auto-generate an INI file that loads your extensions:
const php = new PhpWeb({sharedLibs: [
`php8.4-xml.so`,
`php8.4-dom.so`,
]});Keep the runtime version and the shared-library filenames in sync when you target something other than the default build:
const version = '8.4';
const php = new PhpWeb({version, sharedLibs: [
`php${version}-xml.so`,
`php${version}-dom.so`,
]});You can also load extensions from remote servers with URLs:
const version = '8.4';
const php = new PhpWeb({version, sharedLibs: [`https://unpkg.com/php-wasm-phar/php${version}-phar.so`]});The above is actually shorthand for the following code. Passing ini: true will automatically load the extension via /php.ini, passing ini: false will wait for a call to dl() to do the lookup.
const php = new PhpWeb({sharedLibs: [
{
name: `php8.4-phar.so`,
url: `https://unpkg.com/php-wasm-phar/php8.4-phar.so`,
ini: true,
}
]});Strings starting with /, ./, http:// or https:// will be treated as URLs:
const php = new PhpWeb({sharedLibs: [
`./php8.4-phar.so`
]});Some extensions require supporting libraries. You can provide URLs for those as sharedLibs as well, just pass ini: false:
(name is implied to be the last section of the URL here.)
const php = new PhpWeb({sharedLibs: [
{ url: 'https://unpkg.com/php-wasm-sqlite/php8.4-sqlite.so', ini: true },
{ url: 'https://unpkg.com/php-wasm-sqlite/libsqlite3.so', ini: false },
]});Dynamic extensions can be loaded as modules. The module's main file defines getLibs, and optionally getFiles for preload files. Extensions may be loaded like so:
new PhpNode({sharedLibs:[ await import('php-wasm-intl') ]})Dynamic extensions can also be loaded as modules from any static HTTP server with an ESM directory structure.
// This will load both libsqlite3.so and php8.4-sqlite.so:
const php = new PhpWeb({sharedLibs: [ await import('https://cdn.jsdelivr.net/npm/php-wasm-sqlite') ]});This notation is not available in Service Workers, which do not support dynamic import(). Import the extension module statically or pass its asset URLs instead.
The extension helper JS packages shown above are ESM-only. If you need to bypass those helper packages, pass the extension assets manually instead of importing the helper package:
import { PhpNode } from 'php-wasm/PhpNode';
const php = new PhpNode({
sharedLibs: [
{
name: 'php8.4-intl.so',
url: new URL('./vendor/php8.4-intl.so', import.meta.url).href,
ini: true,
},
{ name: 'libicuuc.so', url: new URL('./vendor/libicuuc.so', import.meta.url).href },
{ name: 'libicutu.so', url: new URL('./vendor/libicutu.so', import.meta.url).href },
{ name: 'libicutest.so', url: new URL('./vendor/libicutest.so', import.meta.url).href },
{ name: 'libicuio.so', url: new URL('./vendor/libicuio.so', import.meta.url).href },
{ name: 'libicui18n.so', url: new URL('./vendor/libicui18n.so', import.meta.url).href },
{ name: 'libicudata.so', url: new URL('./vendor/libicudata.so', import.meta.url).href },
],
files: [
{
name: 'icudt72l.dat',
parent: '/preload/',
url: new URL('./vendor/icudt72l.dat', import.meta.url).href,
},
],
});When you manage extension assets this way, build mode matters:
dynamic: provide the extension.soplus any support libraries and preload files it needsshared: provide only the extra support libraries and preload files the runtime still needsstatic: do not inject the extension assets again
Extensions may be compiled as dynamic, shared, or static. See Custom Builds for more information on compiling php-wasm.
- dynamic - these extensions may be loaded selectively at runtime.
- shared - these extensions will always be loaded at startup and can be cached and reused.
- static - these extensions will be built directly into the main wasm binary (may cause a huge filesize).
When spawning a new instance of PHP, a files array can be provided to be loaded into the filesystem. For example, the php-intl extension requires us to load icudt72l.dat into the /preload directory.
const sharedLibs = [`https://unpkg.com/php-wasm-intl/php\${PHP_VERSION}-intl.so`];
const files = [
{
name: 'icudt72l.dat',
parent: '/preload/',
url: 'https://unpkg.com/php-wasm-intl/icudt72l.dat'
}
];
const php = new PhpWeb({sharedLibs, files});Use the PRELOAD_ASSETS key in your .php-wasm-rc file to define a list of files and directories to include by default.
The files and directories will be collected into a single directory. Individual files & directories will appear in the top level, while directories will maintain their internal structure.
When you use php-wasm-builder, relative entries are resolved from the current project directory. Anchored paths such as /path/to/file.txt and ~/path/to/file.txt are copied as-is.
These files & directories will be available under /preload in the final package, packaged into the .data file that is built along with the .wasm file.
PRELOAD_ASSETS='./php-scripts /some/directory ~/other-dir/example.php /path/to/other_file.txt'You can provide the locateFile option to php-wasm as a callback to map the names of files to URLs where they're loaded from. undefined can be returned as a fallback to default.
You can use this if your static assets are served from a different directory than your JavaScript.
This applies to .wasm files, shared libraries, single files and preloaded FS packages in .data files.
const php = new PhpWeb({locateFile: filename => `/my/static/path/${filename}`});To use IDBFS in PhpWeb, pass a persist object with a mountPath key.
mountPath will be used as the path to the persistent directory within the PHP environment.
const { PhpWeb } = await import('https://cdn.jsdelivr.net/npm/php-wasm/PhpWeb.mjs');
const php = new PhpWeb({persist: {mountPath: '/persist'}});To use NodeFS in PhpNode, pass a persist object with mountPath and localPath keys.
localPath will be used as the path to the HOST directory to expose to PHP.
mountPath will be used as the path to the persistent directory within the PHP environment.
import path from 'node:path';
import { PhpNode } from 'php-wasm/PhpNode';
const php = new PhpNode({
persist: {
mountPath: '/persist',
localPath: path.join(process.cwd(), 'persist'),
}
});The following EmscriptenFS methods are exposed via the php object:
Browser CGI filesystem calls refresh persisted storage automatically when
autoTransaction is enabled. Await the writer's persistence before reading from
another runtime. refresh() recreates the PHP runtime and discards temporary
files and in-memory PHP state; it is not required before each CGI filesystem read.
Get information about a file or directory.
await php.analyzePath(path);Get entry names as string[]:
await php.readdir(path);Pass {withFileTypes: true} to get serializable {name: string, isFolder: boolean}
entries in one queued operation:
const entries = await php.readdir(path, {withFileTypes: true});Both forms preserve filesystem order and include . and ... Types follow
symbolic links, as analyzePath does. Listing and metadata errors, including
dangling links, reject the operation. Omitted options or withFileTypes: false
keep the name-only result. The option is available in embedded, CLI, CGI,
debugger, and Cloudflare runtimes; the declaration overloads reflect each result.
In browser CGI, a typed listing uses one storage refresh for the directory and
all its entry types. This avoids a separate analyzePath request per entry.
It is also available over the service worker message bridge:
const entries = await sendMessage('readdir', [path, {withFileTypes: true}]);Get the content of a file as a Uint8Array by default, or optionally as utf-8.
await php.readFile(path);await php.readFile(path, {encoding: 'utf8'});Get information about a file or directory.
await php.stat(path);Create a directory.
await php.mkdir(path);Delete a directory (must be empty).
await php.rmdir(path);Delete a file.
await php.unlink(path);Rename a file or directory.
await php.rename(path, newPath);Create a new file. Content should be supplied as a Uint8Array, or optionally as a string of text.
await php.writeFile(path, data);await php.writeFile(path, data, {encoding: 'utf8'});Web and Worker only!
With persistence enabled, browser runtimes synchronize their mounted IDBFS
storage while holding the php-wasm-fs-lock Web Lock.
When Web Locks are unavailable, such as on a plain HTTP origin reached by a LAN IP address, browser runtimes fall back to a FIFO lock within the current page or worker. That fallback coordinates runtimes in the same JavaScript realm only. Use HTTPS, where Web Locks are available, when tabs or workers share persistent storage.
Browser CGI batches queued filesystem calls into one transaction. After the
queue becomes idle it waits up to 25 ms for more work. Storage is refreshed
once per batch. A batch containing only analyzePath, readdir, readFile, or
stat does not flush; any mutation makes the batch writable. All calls wait
for the shared commit before their promises resolve or the worker replies.
A commit failure rejects every call in that batch. Callback failures still
commit possible partial writes and do not prevent later calls from running.
The batching window keeps the wrapper transaction open; it does not hold an
IndexedDB transaction open. IDBFS opens those while hydrating or flushing.
Calls submitted together, including through Promise.all, can share a batch.
Awaiting each call before submitting the next creates separate batches. A
batch commits after 64 operations or a 250 ms processing window, checked
between operations, so continuous traffic cannot postpone acknowledgment
indefinitely. HTTP CGI requests use a separate path and still flush each
successful PHP request. A typed readdir obtains names and entry types in one
operation. PhpWeb and PhpWorker retain their existing queues; their results
can become available before the shared transaction commits.
Browser CGI tracks PHP and filesystem API mutations and flushes only changed IDBFS records. Clean mounts do not open a write transaction. Renamed directory trees, deletions, file contents and metadata are persisted in the existing IDBFS format, so existing stored files and older readers remain compatible. Hydration still reconciles with persistent storage; it uses a direct local-node walk to avoid repeatedly resolving every path. Mounts with nested filesystems use ordinary reconciliation. Failed commits retain their pending changes and retry them before a later hydration can replace local state.
With {autoTransaction: false}, the caller owns transaction boundaries and
serialization across runtimes. startTransaction() loads persisted storage;
commitTransaction() flushes changes. These methods do not hold a Web Lock
across a sequence of public calls. Do not acquire php-wasm-fs-lock and then
await a public queued method that needs the same lock. Prefer automatic
transactions unless you provide coordination for the complete operation.
await php.startTransaction();await php.commitTransaction();For a manually managed transaction that performed only reads, use
await php.commitTransaction(true) to close it without flushing. Never pass
true after a mutation that must be persisted.
Install quickbus 1.0.2 or newer to call
php-cgi-wasm filesystem methods on the service worker from the page.
php.handleMessageEvent already speaks its request/reply protocol, so each call
can be awaited:
npm install quickbus@^1.0.2import { Client } from 'quickbus';
const SERVICE_WORKER_SCRIPT_URL = '/cgi-worker.mjs';
await navigator.serviceWorker.register(SERVICE_WORKER_SCRIPT_URL, {type: 'module'});
const registration = await navigator.serviceWorker.ready;
const bus = navigator.serviceWorker.controller
? Client.forServiceWorker(navigator.serviceWorker)
: Client.forServiceWorkerRegistration(registration);
const result = await bus.analyzePath('/path/to/your/file');- Use
Client.forServiceWorker(navigator.serviceWorker)once the page is already controlled by the worker. - Use
Client.forServiceWorkerRegistration(registration)on first load, afterawait navigator.serviceWorker.ready.
Errors raised in the worker, including denied persistent storage, reject the
call. Each call returns a request handle; call abort() on it to stop waiting
locally. Private-mode storage availability depends on the browser.
php-cgi-wasm/msg-bus.mjs, the original helper that quickbus grew out of, still
ships for existing code. Use quickbus for new code.
Once you've got the above set up, use php.handleMessageEvent to handle the message events on the service worker:
self.addEventListener('message', event => php.handleMessageEvent(event));To use the in-place builder, first install php-wasm-builder globally:
Requires Docker with the docker compose plugin, Node.js/npm, coreutils, wget, and Make.
$ npm install -g php-wasm-builderphp-wasm-build is an alias for php-wasm-builder; both commands use the same
Make targets. The builder package includes build sources, package templates,
and Docker image helpers. Runtime binaries are produced in your project.
Maintainers can prepare a source-only release without publishing it:
make package-builder
npm install -g ./.cache/release/php-wasm-builder-0.2.0.tgzThe tarball and its SHA-256 inventory are written to .cache/release/ (override
with BUILDER_PACKAGE_OUTPUT). Native outputs, caches, local environment files,
and credentials are excluded. ./publish-packages.sh next --dry-run includes
this staged builder in the release inventory and skips unchanged packages.
To prepare the publishable packages, start from a clean checkout of the release commit, then replace every generated package file with the output of a successful Build Artifacts run for that exact commit:
git worktree add --detach ../php-wasm-release v0.2.0
cd ../php-wasm-release
make release-overlay RUN_ID=<build-artifacts-run-id>
./publish-packages.sh latest --dry-runmake release-overlay refuses runs that did not succeed or were built from a
different commit, runs make clean-packages (which removes generated package
files without touching build caches), then overlays the php-indexed-packages
artifact with the GitHub CLI and deletes its download afterwards.
Create the build environment (can be run from anywhere):
$ php-wasm-builder imageOptionally clean up files from a previous build:
$ php-wasm-builder cleanThen navigate to the directory you want the files to be built in, and run php-wasm-builder build
$ cd ~/my-project
$ php-wasm-builder build
# php-wasm-builder build web
# "web" is the default here$ cd ~/my-project
$ php-wasm-builder build nodeBuild ESM modules with:
$ php-wasm-builder build web mjs
$ php-wasm-builder build node mjsBuild CGI modules with:
$ php-wasm-builder build web cgi mjs
$ php-wasm-builder build node cgi mjs
$ php-wasm-builder build worker cgi mjsBuild php-cli-wasm modules with:
$ php-wasm-builder build node cli mjsBuild php-dbg-wasm modules with:
$ php-wasm-builder build node dbg mjsBuild the standalone php-sdl-wasm and php-cloud-wasm packages with:
$ php-wasm-builder build sdl mjs
$ php-wasm-builder build cloudflare mjsThese targets support only embedded PHP as ES modules. See the SDL guide and CLOUDFLARE.md.
This will build the package inside the current directory (or in PHP_DIST_DIR, see below for more info.)
You can also create a .php-wasm-rc file in this directory to customize the build.
# Select a PHP version
PHP_VERSION=8.4
# Build the package to a directory other than the current one (RELATIVE path)
PHP_DIST_DIR=./public
# Build the extensions to a directory other than the current one (RELATIVE path)
PHP_ASSET_DIR=./public
# Build the cgi package to a directory other than the current one (RELATIVE path)
PHP_CGI_DIST_DIR=./public
# Build the cgi package's extensions to a directory other than the current one (RELATIVE path)
PHP_CGI_ASSET_DIR=./public
# Space separated list of files/directories to include under /preload.
# Relative paths are resolved from the current project directory.
PRELOAD_ASSETS=./php-scripts ~/other-dir/example.php
# Memory to start the instance with, before growth
INITIAL_MEMORY=2048MB
# Build with assertions enabled
ASSERTIONS=0
# Select the optimization level
OPTIMIZE=3
# Build with extensions
WITH_GD=1
WITH_LIBPNG=1
WITH_LIBJPEG=1
WITH_FREETYPE=1The following options may appear in .php-wasm-rc.
8.0|8.1|8.2|8.3|8.4|8.5
PHP 8.0 builds must also set WITH_PDO_PGLITE=0, because PDO-PGlite requires
PHP 8.1 or newer.
This is the directory where JavaScript and wasm files will be built. It accepts
an absolute path or a path relative to the current build directory. When using
php-wasm-builder, relative paths in .php-wasm-rc resolve from the project
directory.
This is the directory where preload .data / .dat files and other supporting
assets will be built. Paths resolve in the same way as PHP_DIST_DIR, which is
also the default. Shared libraries and side modules remain in their owning
packages. Preload staging reports an error if the native build's .data output
is missing.
0|1|2|3
The optimization level to use while compiling.
The optimization level to use while compiling libraries. Defaults to OPTIMIZE.
A list of files & directories to build to the /preload directory. Relative paths are resolved from the current project directory. Anchored paths such as /path/to/file and ~/path/to/file are copied as-is. Will produce a .data file.
0|1
Build with/without assertions.
As stated above, extensions may be compiled as dynamic, shared, or static.
- dynamic - these extensions may be loaded selectively at runtime.
- shared - these extensions will always be loaded at startup and can be cached and reused.
- static - these extensions will be built directly into the main wasm binary (may cause a huge filesize).
(defaults provided below in bold)
The following options are available for building static PHP extensions:
WITH_BCMATH # [0, 1] Enabled by default
WITH_CALENDAR # [0, 1] Enabled by default
WITH_CTYPE # [0, 1] Enabled by default
WITH_EXIF # [0, 1] Enabled by default
WITH_FILTER # [0, 1] Enabled by default
WITH_TOKENIZER # [0, 1] Enabled by default
WITH_VRZNO # [0, 1] Enabled by default
WITH_WAITLINE # [0, 1] Disabled by default in raw custom builds
The following extensions may be compiled as static, shared, or dynamic:
WITH_PHAR # [0, 1, static, dynamic]
WITH_LIBXML # [0, 1, static, shared, dynamic]
WITH_ICONV # [0, 1, static, shared, dynamic]
WITH_SQLITE # [0, 1, static, shared, dynamic]
WITH_LIBZIP # [0, 1, static, shared, dynamic]
WITH_ZLIB # [0, 1, static, shared, dynamic]
WITH_GD # [0, 1, static, dynamic]
WITH_LIBPNG # [0, 1, static, shared]
WITH_FREETYPE # [0, 1, static, shared]
WITH_LIBJPEG # [0, 1, static, shared]
WITH_YAML # [0, 1, static, shared, dynamic]
WITH_TIDY # [0, 1, static, shared, dynamic]
WITH_MBSTRING # [0, 1, static, dynamic]
WITH_ONIGURUMA # [0, 1, static, shared, dynamic]
WITH_OPENSSL # [0, 1, static, shared, dynamic]
WITH_INTL # [0, 1, static, shared, dynamic]
Build the standalone php-sdl-wasm browser package with make sdl-mjs.
Its Make profile selects WITH_SDL=1. dynamic remains a
legacy alias for 1; SDL PHP extensions are built into the main runtime.
| Option | Values | Default |
|---|---|---|
WITH_SDL |
0, 1, dynamic |
0 |
WITH_SDL_IMAGE |
0, 1 |
Follows SDL |
WITH_SDL_MIXER |
0, 1 |
Follows SDL |
WITH_SDL_TTF |
0, 1 |
Follows SDL |
WITH_OPENGL |
0, 1 |
Follows SDL |
The add-ons require SDL. Image loading reuses WITH_LIBPNG/WITH_LIBJPEG, and
text reuses WITH_FREETYPE; those codec libraries must be enabled as static or
shared. WITH_ZLIB=0 still provides the native zlib archive needed by codecs.
Set all four add-on flags to 0 for core SDL only. Build with the existing
make sdl-mjs target, or set the flags in .php-wasm-rc and use
php-wasm-builder build sdl mjs. See the SDL build and API guide.
static|dynamic
When compiled as a dynamic extension, this will produce the extension file php8.x-phar.so.
static|shared|dynamic
The libxml extension itself must be statically compiled, but libxml2 may be loaded as a shared library.
When compiled as a shared library, it will produce the library libxml2.so.
static|shared|dynamic
When compiled as a dynamic extension, this will produce the extension php8.x-zip.so.
When compiled as a dynamic or shared extension, it will produce the library libzip.so.
This extension depends on zlib.
static|shared|dynamic
When compiled as a dynamic extension, this will produce the extension php8.x-iconv.so.
When compiled as a dynamic or shared extension, it will produce the library libiconv.so.
static|shared|dynamic
When compiled as a dynamic extension, this will produce the extensions php8.x-sqlite.so and php8.x-pdo-sqlite.so.
When compiled as a dynamic or shared extension, it will produce the library libsqlite3.so.
static|dynamic
This extension makes use of freetype, libjpeg, libpng, and zlib.
When compiled as a dynamic extension, this will produce the extension php8.x-gd.so.
static|shared
When compiled as a shared library, this will produce the library libpng.so.
If WITH_GD is dynamic, then loading will be deferred until after gd is loaded.
static|shared
When compiled as a shared library, this will produce the library libfreetype.so.
If WITH_GD is dynamic, then loading will be deferred until after gd is loaded.
static|shared
When compiled as a shared library, this will produce the library libjpeg.so.
If WITH_GD is dynamic, then loading will be deferred until after gd is loaded.
static|shared|dynamic
When compiled as a dynamic extension, this will produce the extension php8.x-zlib.so.
When compiled as a dynamic or shared extension, it will produce the library libz.so.
static|shared|dynamic
When compiled as a dynamic extension, this will produce the extension php8.x-yaml.so.
When compiled as a dynamic or shared extension, it will produce the library libyaml.so.
static|shared|dynamic
When compiled as a dynamic extension, this will produce the extension php8.x-tidy.so.
When compiled as a dynamic or shared extension, it will produce the library libtidy.so.
static|dynamic
When compiled as a dynamic extension, this will produce the extension php8.x-mbstring.so.
static|shared|dynamic
Support library for mbstring.
When compiled as a dynamic or shared library, this will produce the library libonig.so.
If WITH_MBSTRING is dynamic, then loading will be deferred until after mbstring is loaded.
static|shared|dynamic
When compiled as a dynamic extension, this will produce the extension php8.x-openssl.so.
When compiled as a dynamic or shared extension, it will produce the libraries libssl.so & libcrypto.so.
static|shared|dynamic
When compiled as a dynamic or shared extension, this will produce the extension php8.x-intl.so and the following libraries:
- libicuuc.so
- libicutu.so
- libicutest.so
- libicuio.so
- libicui18n.so
- libicudata.so
- icudt72l.dat
Use this to build a custom version of php-wasm, php-cgi-wasm, php-cli-wasm, or php-dbg-wasm. It's recommended to build into an empty directory using a .php-wasm-rc file.
npx php-wasm-builder buildThis will build the docker container used to build php-wasm.
npx php-wasm-builder imageThis will scan the current package's node_modules directory for supporting data files, and copy them to PHP_ASSET_DIR.
You can use this with .php-wasm-rc to copy assets even if you're not using a custom build.
npx php-wasm-builder copy-assetsSimilar to copy-assets, but will build the supporting asset files described by .php-wasm-rc, then copy them to PHP_ASSET_DIR.
You can use this with .php-wasm-rc to copy assets even if you're not using a custom build.
npx php-wasm-builder build-assetsClear cached build resources.
npx php-wasm-builder cleanClear out all downloaded dependencies and start from scratch.
npx php-wasm-builder deep-cleanPrint the help text for a given command
npx php-wasm-builder help COMMANDThe Node, Deno, and Bun Make targets share the same ESM runtime, extension, documentation, and packaging suites. Documentation examples run with the dynamic profile. make test runs Node and Bun for every supported PHP version, plus Deno for PHP 8.2β8.5. CI pins Bun to 1.4.0 and Deno to 2.5.6.
To run Bun against a selected build configuration:
make test-bun ENV_FILE=.github/.env_8.3.dynamic.ci PHP_VERSION=8.3 LIB_TYPE=dynamic
make test-bun-standard ENV_FILE=.github/.env_8.3.dynamic.ci PHP_VERSION=8.3 LIB_TYPE=dynamic
make test-bun-cjs-standard ENV_FILE=.github/.env_8.3.dynamic.ci PHP_VERSION=8.3 LIB_TYPE=dynamic
make test-cgi-bun ENV_FILE=.github/.env_8.3.dynamic.ci PHP_VERSION=8.3 LIB_TYPE=dynamic
make test-cgi-bun-cjs ENV_FILE=.github/.env_8.3.dynamic.ci PHP_VERSION=8.3 LIB_TYPE=dynamicThe -standard targets also exercise CLI PHPT cases and the debugger. test-bun-cjs and test-bun-cjs-standard mirror the corresponding Node CommonJS targets. The test-cgi-bun targets use the shared CGI HTTP/cookie harness with a pinned Bun Docker image, plus dynamic-profile CGI documentation examples. Bun Make targets use a five-minute per-test timeout to accommodate full tarball checks, configurable through BUN_TEST_FLAGS.
The Build Artifacts workflow tests Node, Deno, and Bun across PHP 8.0β8.5, dynamic/shared/static builds, and uncompressed/compressed packages. Bun and Node run both ESM and CommonJS standard and CGI suites. These tests reuse the existing native build artifacts. Fast Bun wrapper and build-helper checks also run before native builds.
The repository pib-legacy was created to preserve the original state of the project.
https://github.com/seanmorris/pib-legacy
php-wasm is dual licensed under the Apache License, Version 2.0 and the GNU General Public License, Version 2; you may use it under the terms of either license. See NOTICE for copyright and third-party attributions.
Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses.
Special thanks to Alex Haussmann