# [![seanmorris/](https://seanmorris.github.io/php-wasm/sean-icon.png)](https://github.com/seanmorris/php-wasm) php-wasm [![php-wasm](https://img.shields.io/npm/v/php-wasm?color=4f5d95&label=php-wasm&style=for-the-badge)](https://www.npmjs.com/package/php-wasm) [![License: Apache-2.0 OR GPL-2.0](https://img.shields.io/badge/license-Apache--2.0%20OR%20GPL--2.0-427819?style=for-the-badge)](https://github.com/seanmorris/php-wasm/blob/master/NOTICE) [![GitHub Sponsors](https://img.shields.io/github/sponsors/seanmorris?style=for-the-badge&color=f1e05a)](https://github.com/sponsors/seanmorris) ![Size](https://img.shields.io/github/languages/code-size/seanmorris/php-wasm?color=e34c26&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAABQAAAAOCAQAAACFzfR7AAABF0lEQVQoFQXBQWvOAQDA4ef/7o29YWtqKU7ExWE5OIvm4LKcnXwD7aQ0N/kAczO1i1KOO0xJvQojaTm4KbJabnJysLSf5wFAa603CUB322yOAAitVT86BTTQ1+oJDYDQcv+qFRr3vC1ooYPqDkHoYgfVKmnSfhG62t/qBkHn2q8ekjRpryB0v/rZ2eh4r6tpY5pp3Gx7RTONoJfVLnpQfekYtNG0832rRj3tEaT31bOxQ5wc/oATrnnniEMfXfaZDFrAoEk71XajNN9OVVW7HYVeVZ9AF/pd3YPm267qbYs0tF597wygpaquQ7Nt9QLoVlWXCEK3q1oCCF2p6iYBpKGN6kNzATrdr2qVAACa9rgRQKPetAnAf1jX/qSkN8aIAAAAAElFTkSuQmCC&style=for-the-badge) ![GitHub Repo stars](https://img.shields.io/github/stars/seanmorris/php-wasm?style=for-the-badge&label=GitHub%20Stars&link=https%3A%2F%2Fgithub.com%2Fseanmorris%2Fphp-wasm) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/seanmorris/php-wasm/test.yaml?style=for-the-badge) ![NPM Downloads](https://img.shields.io/npm/dw/php-wasm?style=for-the-badge&color=C80&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fphp-wasm&label=npm%20installs) ![jsDelivr hits (npm)](https://img.shields.io/jsdelivr/npm/hw/php-wasm?style=for-the-badge&color=09D&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2Fphp-wasm&label=jsdelivr%20hits) [![Static Badge](https://img.shields.io/badge/reddit-always%20online-336699?style=for-the-badge&logo=reddit)](https://www.reddit.com/r/phpwasm/) [![Discord](https://img.shields.io/discord/1199824765666463835?style=for-the-badge&logo=discord&link=https%3A%2F%2Fdiscord.gg%2Fj8VZzju7gJ)](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/)]
Drupal Demo CakePHP Demo CodeIgniter Demo Laravel Demo Laminas Demo Code Editor
## 🎩 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*