# [](https://github.com/seanmorris/php-wasm) php-wasm
[](https://www.npmjs.com/package/php-wasm)
[](https://github.com/seanmorris/php-wasm/blob/master/NOTICE) [](https://github.com/sponsors/seanmorris)   
  [](https://www.reddit.com/r/phpwasm/) [](https://discord.gg/j8VZzju7gJ)
_PHP in WebAssembly, npm not required._
[npm](https://npmjs.com/package/php-wasm) | [github](https://github.com/seanmorris/php-wasm) | [unpkg](https://unpkg.com/browse/php-wasm/) | [reddit](https://www.reddit.com/r/phpwasm) | [discord](https://discord.gg/j8VZzju7gJ)
## 📦 Current Packages
* `php-wasm`, `php-cgi-wasm`, `php-cli-wasm`, `php-dbg-wasm`, `php-sdl-wasm`, `php-cloud-wasm`, and `php-wasm-builder` are published separately.
* Published runtimes currently cover PHP `8.0` through `8.5`, depending on the package and entrypoint.
* Runtimes default to PHP `8.4`, except `PhpCliWeb`, which defaults to `8.3`. `PhpNode`, `PhpCliNode`, and `PhpDbgNode` use the `PHP_VERSION` environment variable instead when it names a supported version. Pass `version` explicitly 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`, and `zlib`.
* [Vrzno](https://github.com/seanmorris/vrzno), [pdo_cfd1](https://github.com/seanmorris/pdo-cfd1), and [pdo_pglite](https://github.com/seanmorris/pdo-pglite) are maintained as separate packages.
* `php-cloud-wasm` is a dedicated static ES-module build for [Cloudflare Workers and Pages](CLOUDFLARE.md), tested with local workerd for PHP `8.0` through `8.5`.
* The standalone `php-sdl-wasm` browser runtime supports SDL graphics, input, image loading, TrueType text, audio, and OpenGL shaders, with a textured cube example. See the [SDL guide](packages/php-sdl-wasm/README.md) for canvas setup, build flags, and supported APIs.
[changelog](https://raw.githubusercontent.com/seanmorris/php-wasm/master/CHANGELOG.md)
Install the packages you need:
```sh
$ 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
```
## ☀️ Examples
### [[React + php-web/php-cgi-worker demo](https://seanmorris.github.io/php-wasm/)]
## 🎩 Introducing php-cgi-wasm!
`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.
### Install the php-cgi-wasm package
```bash
$ npm install php-cgi-wasm
```
### Example Service Worker:
```javascript
import { 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`](demo-web/src/workers/cgi-worker.mjs) and [`demo-node/index.mjs`](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](packages/php-cgi-wasm).
## 🛠️ Install & Use
Install php-wasm with npm:
```sh
$ npm install php-wasm
```
Include the module:
### ESM
```javascript
import { PhpWeb } from 'php-wasm/PhpWeb.mjs';
const php = new PhpWeb;
```
#### From a CDN:
***Note: This does not require npm.***
##### jsdelivr
```javascript
const { PhpWeb } = await import('https://cdn.jsdelivr.net/npm/php-wasm/PhpWeb.mjs');
const php = new PhpWeb;
```
##### unpkg
```javascript
const { PhpWeb } = await import('https://unpkg.com/php-wasm/PhpWeb.mjs');
const php = new PhpWeb;
```
#### Pre-Packaged Static Assets:
Each runtime module loads its WebAssembly binary with
`new URL('.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:
```bash
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 -u
```
Repeat 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/PhpNode`
- `php-cgi-wasm/PhpCgiNode`
- `php-cli-wasm/PhpCliNode`
- `php-dbg-wasm/PhpDbgNode`
Browser/runtime helper entrypoints such as `PhpWeb` and `php-tags` remain ESM-first.
## 🍎 Quickstart
### Inline PHP
Include the `php-tags` module from a CDN:
```html
```
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 `` loads before `` exists.
And run some PHP right in the page!
```html
```
Inline PHP can use standard input, output, and error with `data-` attributes. Set each attribute value to a selector that matches the corresponding element.
```html
```
The `src` attribute can be used on `
```
#### CDNs
##### JSDelivr
```html
```
##### Unpkg
```html
```
## 🥤 Running PHP & Taking Output
Create a PHP instance:
```javascript
const { PhpWeb } = await import('https://cdn.jsdelivr.net/npm/php-wasm/PhpWeb.mjs');
const php = new PhpWeb;
```
Add your output listeners:
```javascript
// 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:
```javascript
php.inputString('This is a string of data provided on STDIN.');
```
... then run some PHP!
```javascript
const exitCode = await php.run('
```
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.
## ⚙️ Configuration
You can pass in the `ini` property to the constructor to add lines to `/php.ini`:
```javascript
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](https://www.php.net/manual/en/ini.list.php) for the full list.
```ini
[php]
date.timezone=UTC
tidy.clean_output=1
expose_php=0
```
### CGI Configuration
When 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.
### Writing an INI for multiple PHP versions
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:
```ini
[php]
extension=php${PHP_VERSION}-phar.so
```
Remember to correctly escape the `$` if you're supplying the INI from JavaScript with backticks:
```javascript
const php = new PhpWeb({ini: `
extension=php\${PHP_VERSION}-phar.so
date.timezone=${Intl.DateTimeFormat().resolvedOptions().timeZone}
`});
```
## 🔌 Extensions
### Loading extensions at runtime
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
`/my/static/path/${filename}`});
```
## 💾 Persistent Storage (IDBFS & NodeFS)
### IDBFS (Web & Worker)
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.
```javascript
const { PhpWeb } = await import('https://cdn.jsdelivr.net/npm/php-wasm/PhpWeb.mjs');
const php = new PhpWeb({persist: {mountPath: '/persist'}});
```
### NodeFS (Node.js Only)
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.
```javascript
import path from 'node:path';
import { PhpNode } from 'php-wasm/PhpNode';
const php = new PhpNode({
persist: {
mountPath: '/persist',
localPath: path.join(process.cwd(), 'persist'),
}
});
```
## 📁 Filesystem Operations
### Filesystem Methods
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.
#### php.analyzePath
Get information about a file or directory.
```javascript
await php.analyzePath(path);
```
#### php.readdir
Get entry names as `string[]`:
```javascript
await php.readdir(path);
```
Pass `{withFileTypes: true}` to get serializable `{name: string, isFolder: boolean}`
entries in one queued operation:
```javascript
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:
```javascript
const entries = await sendMessage('readdir', [path, {withFileTypes: true}]);
```
#### php.readFile
Get the content of a file as a `Uint8Array` by default, or optionally as utf-8.
```javascript
await php.readFile(path);
```
```javascript
await php.readFile(path, {encoding: 'utf8'});
```
#### php.stat
Get information about a file or directory.
```javascript
await php.stat(path);
```
#### php.mkdir
Create a directory.
```javascript
await php.mkdir(path);
```
#### php.rmdir
Delete a directory (must be empty).
```javascript
await php.rmdir(path);
```
#### php.unlink
Delete a file.
```javascript
await php.unlink(path);
```
#### php.rename
Rename a file or directory.
```javascript
await php.rename(path, newPath);
```
#### php.writeFile
Create a new file. Content should be supplied as a `Uint8Array`, or optionally as a string of text.
```javascript
await php.writeFile(path, data);
```
```javascript
await php.writeFile(path, data, {encoding: 'utf8'});
```
### Transactions
**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.
#### php.startTransaction
```javascript
await php.startTransaction();
```
#### php.commitTransaction
```javascript
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.
### quickbus
Install [`quickbus`](https://www.npmjs.com/package/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:
```bash
npm install quickbus@^1.0.2
```
```javascript
import { 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, after `await 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.
#### php.handleMessageEvent
Once you've got the above set up, use `php.handleMessageEvent` to handle the `message` events on the service worker:
```javascript
self.addEventListener('message', event => php.handleMessageEvent(event));
```
## 🏗️ Custom Builds
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.***
```sh
$ npm install -g php-wasm-builder
```
`php-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:
```sh
make package-builder
npm install -g ./.cache/release/php-wasm-builder-0.2.0.tgz
```
The 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:
```sh
git worktree add --detach ../php-wasm-release v0.2.0
cd ../php-wasm-release
make release-overlay RUN_ID=
./publish-packages.sh latest --dry-run
```
`make 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):
```sh
$ php-wasm-builder image
```
Optionally clean up files from a previous build:
```sh
$ php-wasm-builder clean
```
### Build for web
Then navigate to the directory you want the files to be built in, and run `php-wasm-builder build`
```sh
$ cd ~/my-project
$ php-wasm-builder build
# php-wasm-builder build web
# "web" is the default here
```
### Build for node
```sh
$ cd ~/my-project
$ php-wasm-builder build node
```
### ESM Modules:
Build ESM modules with:
```sh
$ php-wasm-builder build web mjs
$ php-wasm-builder build node mjs
```
### CGI Modules:
Build CGI modules with:
```sh
$ php-wasm-builder build web cgi mjs
$ php-wasm-builder build node cgi mjs
$ php-wasm-builder build worker cgi mjs
```
### CLI Modules:
Build `php-cli-wasm` modules with:
```sh
$ php-wasm-builder build node cli mjs
```
### DBG Modules:
Build `php-dbg-wasm` modules with:
```sh
$ php-wasm-builder build node dbg mjs
```
### SDL and Cloudflare Packages:
Build the standalone `php-sdl-wasm` and `php-cloud-wasm` packages with:
```sh
$ php-wasm-builder build sdl mjs
$ php-wasm-builder build cloudflare mjs
```
These targets support only embedded PHP as ES modules. See the
[SDL guide](packages/php-sdl-wasm/README.md) and [CLOUDFLARE.md](CLOUDFLARE.md).
This will build the package inside the current directory (or in `PHP_DIST_DIR`, *see below for more info.*)
### .php-wasm-rc
You can also create a `.php-wasm-rc` file in this directory to customize the build.
```make
# 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=1
```
#### Options
The following options may appear in `.php-wasm-rc`.
---
##### PHP_VERSION
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.
---
##### PHP_DIST_DIR
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.
---
##### PHP_ASSET_DIR
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.
---
##### OPTIMIZE
0|1|2|**3**
The optimization level to use while compiling.
---
##### SUB_OPTIMIZE
The optimization level to use while compiling libraries. Defaults to `OPTIMIZE`.
---
##### PRELOAD_ASSETS
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.
---
##### ASSERTIONS
**0**|1
Build with/without assertions.
---
#### Extensions
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]
```
---
##### SDL runtime options
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](packages/php-sdl-wasm/README.md).
---
##### WITH_PHAR
static|**dynamic**
When compiled as a `dynamic` extension, this will produce the extension file `php8.x-phar.so`.
---
##### WITH_LIBXML
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`.
---
##### WITH_LIBZIP
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`.
---
##### WITH_ICONV
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`.
---
##### WITH_SQLITE
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`.
---
##### WITH_GD
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`.
###### WITH_LIBPNG
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.
###### WITH_FREETYPE
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.
###### WITH_LIBJPEG
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.
---
##### WITH_ZLIB
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`.
---
##### WITH_YAML
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`.
---
##### WITH_TIDY
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`.
---
##### WITH_MBSTRING
static|**dynamic**
When compiled as a `dynamic` extension, this will produce the extension `php8.x-mbstring.so`.
---
##### WITH_ONIGURUMA
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.
---
##### WITH_OPENSSL
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`.
##### WITH_INTL
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
---
### php-wasm-builder commands
#### php-wasm-builder build
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.
```bash
npx php-wasm-builder build
```
#### php-wasm-builder image
This will build the docker container used to build php-wasm.
```bash
npx php-wasm-builder image
```
#### php-wasm-builder copy-assets
This 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.
```bash
npx php-wasm-builder copy-assets
```
#### php-wasm-builder build-assets
Similar 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.
```bash
npx php-wasm-builder build-assets
```
#### php-wasm-builder clean
Clear cached build resources.
```bash
npx php-wasm-builder clean
```
#### php-wasm-builder deep-clean
Clear out all downloaded dependencies and start from scratch.
```bash
npx php-wasm-builder deep-clean
```
#### php-wasm-builder help
Print the help text for a given command
```bash
npx php-wasm-builder help COMMAND
```
## Testing
The 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:
```bash
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=dynamic
```
The `-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.
## 🤝 php-wasm started as a fork of oraoto/PIB...
The repository [pib-legacy](https://github.com/seanmorris/pib-legacy) was created to preserve the original state of the project.
https://github.com/oraoto/pib
https://github.com/seanmorris/pib-legacy
## 🍻 Dual licensed under the Apache License, Version 2.0 and the GNU GPL, Version 2
php-wasm is dual licensed under the [Apache License, Version 2.0](LICENSE) and the [GNU General Public License, Version 2](LICENSE-GPL); you may use it under the terms of either license. See [NOTICE](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*