@kirigami/php-wasm
v8.5.11-4
Published
A custom PHP-WASM build for Node.js — JSPI only, no browser support. Built for the Kirigami project.
Maintainers
Readme
@kirigami/php-wasm
A custom PHP 8.5 WebAssembly build for Node.js — JSPI-only, no browser target.
Built for the Kirigami static site generator.
Overview
@kirigami/php-wasm ships a PHP 8.5.11 WebAssembly binary compiled by Kirigami's own toolchain, php-wasm-compiler, together with its Node.js loader and runtime helpers. It is built for exactly what the Kirigami project needs:
- ✅ JSPI (JavaScript Promise Integration) target only
- ✅ Node.js runtime only
- ❌ No browser build
- ❌ No
WORKER/IFRAMEtargets
This intentional reduction keeps the package lean and avoids shipping browser-specific glue code that would never be used inside Kirigami's server-side execution environment.
Part of the Kirigami project ecosystem.
Table of contents
- @kirigami/php-wasm
- Overview
- What's new in 8.5.11-4
- What's new in 8.5.11-3
- What's new in 8.5.11
- Build origin
- Compatibility & Runtime Helpers
- Requirements
- Installation
- Usage
- Security considerations
- TypeScript
- Package contents
- PHP version
- Static extensions
- In-house extensions
- Loading additional extensions
- Related
- Runtime extension inspection
- License
What's new in 8.5.11-4
mdhtml0.1.6: a block plugin's body may contain other{% … %}tags. The block ends at the%}that balances its own{%(before, at the first%}), and the body still reaches the plugin raw — so a block can list{% img-asset %}codes, for example. An unclosed stray{%in a body falls back to the first%}, as before.
What's new in 8.5.11-3
Two native extensions join the binary:
| Extension | Capability |
|---|---|
| fastcsv | Streaming CSV reading and writing: FastCSVReader (headers, nextRecord(), seek(), record count), FastCSVWriter (writeRecord(), writeRecordMap()) and FastCSVConfig (delimiter, enclosure, escape, encoding, BOM, strict mode, empty-line skipping, field trimming). |
| aspect | The Memoize class, for caching the results of a function. |
What's new in 8.5.11
The runtime includes a broader native extension set for Kirigami's PHP templates:
| Extension | Capability | Compared with the previous README snapshot |
|---|---|---|
| bz2 | BZip2 compression/decompression, compress.bzip2://, and stream filters | Newly present in the current phpinfo output |
| mdhtml | Native Markdown rendering through cmark-gfm, used by php-prepros's MD:: wrapper | Already present in the previous snapshot; now documented as the active wrapper backend |
| yaml | Native YAML parsing through LibYAML, used by YAML:: | Already present in the previous snapshot; YAML 1.1 scalar behavior now documented |
| jsonk | Native JSON Schema support and replacement of json_encode() / json_decode() | Already present and enabled in both snapshots |
| lexbor | HTML5 parsing and CSS selection | Already present in both snapshots |
| navicat | HTTP-tunnel database bridge for MySQL, PostgreSQL, and SQLite | Already present in both snapshots |
| norm | Unicode normalization and the Normalizer API | Already present in both snapshots |
| apcu / igbinary | In-memory caching and compact serialization | Already present in both snapshots |
Comparison source: the README's previously committed phpinfo() snapshot versus node packages/cli/bin/kiri.js phpinfo -m on 2026-09-20. Only bz2 is a newly listed extension in that direct comparison; the other extensions should not be described as newly added relative to that snapshot. The current cURL/Navicat build also reports an updated linked zlib library. This comparison describes the local binary, not the contents of a published npm release.
For the PHP wrappers, quote YAML string keys/values such as "NO" to avoid YAML 1.1 boolean coercion. Markdown footnotes now use <section class="footnotes" data-footnotes>; custom CSS should target .footnotes instead of the old div tag. See php-prepros.
Build origin
The WASM binary (jspi/8_5_11/php_8_5.wasm) and the Emscripten-generated loader (jspi/php_8_5.js) are produced by php-wasm-compiler, Kirigami's own PHP → WebAssembly compiler (Docker + Emscripten). A single config.yaml there drives the PHP version, the statically compiled extensions, the third-party libraries, and the build options; it targets JSPI and Node.js only, with no Asyncify build, browser polyfills, or DOM stubs.
The same compiler also builds and publishes the optional @kirigami/phpext-* extension packages described in Loading additional extensions.
The JavaScript side of this package (index.js, runtime/runtime.js: the networking proxy, extension discovery, and php.ini helpers) is Kirigami code. It plugs the loader into @php-wasm/universal, which still provides the PHP class and loadPHPRuntime().
The compiler's Docker recipes originally started from WordPress Playground's compile pipeline; see its NOTICE.md for provenance details.
Compatibility & Runtime Helpers
This package is a drop-in replacement for the loader module consumed by @php-wasm/universal. It exposes the raw PHPLoaderModule interface along with high-level runtime instantiators that include out-of-the-box networking capabilities.
| Export | Description |
|---|---|
| getPHPLoaderModule() | Returns the raw JSPI PHP 8.5 loader module |
| jspi() | Detects JSPI support in the current runtime (re-exported from wasm-feature-detect) |
| getPHPRuntime() | Returns a standard PHP instance. Memoized singleton — the first call creates it, subsequent calls return the same instance |
| getPHPRuntimeWithNetwork() | Returns a PHP instance bound to a local TCP outbound proxy using Node built-ins and ws with SSL root certificates injected. Memoized singleton, separate from getPHPRuntime() |
| createPHPRuntime({ network? }?) | Creates an independent owned runtime, without changing either singleton. Call php.exit() when finished; this also closes its network proxy and sockets when networking is enabled |
| getLoadedExtensions() | Returns the names of every loaded PHP extension, sorted case-insensitively (e.g. ["Core", "curl", "gd", "imagick", "openssl", …]) |
| exec(code, network?) | Executes a PHP code snippet against the standard runtime, or the network-enabled one if network is true. Returns { returnCode, stdout, stderr } |
| phpversion() | Returns the running PHP interpreter's version string, e.g. "8.5.11" |
| phpinfo() | Returns the HTML result of phpinfo() |
| setPhpIniValues(php, values, iniPath?) | Updates or adds one or more php.ini directives on a PHP instance. Also available as php.setIniValues(values) on instances from getPHPRuntime() / getPHPRuntimeWithNetwork() |
| getPhpIniValue(php, key, iniPath?) | Reads the current value of a single, active (uncommented) php.ini directive |
Requirements
- Node.js
>= 24.0.0 - npm
>= 10.2.3
JSPI support: use the exported async
jspi()feature check before creating a runtime. The package requires Node.js>= 24.0.0; embedded editor runtimes must be checked independently.
Installation
npm install @kirigami/php-wasmUsage
1. High-level execution with Outbound Networking
The package provides a built-in proxy architecture (node:http, node:net & node:dgram) that routes Emscripten SOCKFS actions into genuine outbound TCP traffic, and UDP for datagram sockets (one WebSocket message per datagram). It also automatically binds your Node environment's root certificates (node:tls) to the PHP layer so cURL and OpenSSL HTTPS requests work immediately.
UDP works through PHP streams (stream_socket_client('udp://host:port'), fsockopen('udp://…')) with their usual read timeouts. With the sockets extension, socket_sendto()/socket_recvfrom() relay too: a blocking receive waits for a datagram (up to SO_RCVTIMEO when set), and socket_select() reports a UDP socket readable only once a datagram is queued, so C libraries that wait with select()/poll() work (ext/snmp over UDP, for example). connect() waits for the proxy to reach the destination and fails with ECONNREFUSED when it can't; a non-blocking connect returns EINPROGRESS. Blocking TCP reads wait for data, EOF or SO_RCVTIMEO. TCP_NODELAY and SO_KEEPALIVE are applied by the proxy to the real connection (also when set before connect()), SO_RCVTIMEO/SO_SNDTIMEO are handled inside PHP-WASM, and other options fail with ENOPROTOOPT.
import { getPHPRuntimeWithNetwork, jspi } from '@kirigami/php-wasm';
// Guard: verify JSPI is available before proceeding
if (!(await jspi())) {
throw new Error('WASM JSPI is not available in this runtime.');
}
// Spins up the runtime and its companion local proxy on a random free port
const php = await getPHPRuntimeWithNetwork();
php.writeFile('/network-demo.php', `<?php
// Native HTTPS request inside WASM using cURL!
$ch = curl_init("https://api.github.com/zen");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, "Kirigami-PHP-WASM");
$response = curl_exec($ch);
echo "GitHub says: " . $response;
`);
const streamedResponse = await php.runStream({ scriptPath: '/network-demo.php' });
console.log(await streamedResponse.stdoutText);
// Clean up the proxy server when done if necessary
if (php._networkProxyServer) {
php._networkProxyServer.close();
}
2. Quick execution with exec()
For one-off PHP snippets, exec() skips the manual writeFile/runStream dance: it writes your code to a temporary file, runs it, cleans up, and gives you back a plain result object.
import { exec } from '@kirigami/php-wasm';
// Standard runtime (no networking)
const { returnCode, stdout, stderr } = await exec('echo "Hello, Kirigami!";');
console.log(returnCode, stdout, stderr); // 0 "Hello, Kirigami!" ""
// Pass `true` as the second argument to run against the network-enabled runtime
const net = await exec(`
$ch = curl_init("https://api.github.com/zen");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
echo curl_exec($ch);
`, true);
console.log(net.stdout);The <?php opening tag is added automatically if you don't include it.
Two small helpers are built on top of exec():
import { phpversion, phpinfo } from '@kirigami/php-wasm';
console.log(await phpversion()); // "8.5.11"
console.log(await phpinfo()); // full phpinfo() HTML output3. Standard isolated runtime
If you need the raw PHP instance instead of the exec() shorthand — for example to keep writing files to its virtual filesystem across multiple calls — use the lightweight isolated helper:
import { getPHPRuntime } from '@kirigami/php-wasm';
const php = await getPHPRuntime();
php.writeFile('/version.php', '<?php echo PHP_VERSION;');
const streamedResponse = await php.runStream({ scriptPath: '/version.php' });
console.log(await streamedResponse.stdoutText); // "8.5.11"
getPHPRuntime()andgetPHPRuntimeWithNetwork()are memoized: every call within the same process returns the same shared instance, so state persists between calls.
4. Low-level configuration (Manual)
For manual construction, use loadPHPRuntime() and pass its runtime ID to the PHP constructor:
import { getPHPLoaderModule } from '@kirigami/php-wasm';
import { PHP, loadPHPRuntime } from '@php-wasm/universal';
const loaderModule = await getPHPLoaderModule();
const runtimeId = await loadPHPRuntime(loaderModule);
const php = new PHP(runtimeId);
php.writeFile('/hello.php', '<?php echo "Hello, Kirigami!";');
const streamedResponse = await php.runStream({ scriptPath: '/hello.php' });
console.log(await streamedResponse.stdoutText); // Hello, Kirigami!
Security considerations
exec() and the low-level PHP instance run inside the compiled WASM/Emscripten sandbox: PHP code sees a virtual filesystem (writeFile/unlink operate on it, not on your real disk) unless the host explicitly mounts files or installs bridges for filesystem, environment, or process access. php-prepros configures additional integration; do not treat arbitrary project PHP as untrusted isolated input. This is not equivalent to child_process.exec(), which runs directly on the host.
Two things narrow that isolation and are worth keeping in mind:
getPHPRuntimeWithNetwork()gives the sandboxed PHP instance genuine outbound TCP and UDP access via the local proxy (not just HTTP/HTTPS). The proxy itself binds to127.0.0.1only, but the PHP code running inside can now reach out to the network like any other client.- WASM sandboxing reduces host exposure but isn't a substitute for a security boundary like a container or VM if you're running fully untrusted PHP (e.g. user-submitted code) — apply the isolation appropriate to your threat model on top.
TypeScript
The instances returned by getPHPRuntime() and getPHPRuntimeWithNetwork() aren't plain @php-wasm/universal PHP objects — this package attaches its own convenience members to them, and the types reflect that:
getPHPRuntime()resolves to aKirigamiPHP, which extendsPHPwith a bound.setIniValues(values)method.getPHPRuntimeWithNetwork()resolves to aKirigamiNetworkPHP, which further adds._networkProxyServer(thenode:httpServerbacking the outbound proxy).
import { getPHPRuntimeWithNetwork } from '@kirigami/php-wasm';
const php = await getPHPRuntimeWithNetwork();
php.setIniValues({ memory_limit: '256M' }); // typed, no cast needed
php._networkProxyServer.close(); // typed, no cast neededPackage contents
@kirigami/php-wasm
├── index.js # ESM entry point (re-exports runtime + loaders)
├── index.d.ts # TypeScript declarations
├── runtime/
│ └── runtime.js # Networking proxy and runtime helpers
├── jspi/
│ ├── php_8_5.js # Emscripten-generated Node.js loader (JSPI build)
│ └── 8_5_11/
│ └── php_8_5.wasm # Compiled PHP 8.5.11 WebAssembly binary (size depends on the embedded build)
└── LICENSE
PHP version
This package ships PHP 8.5.11.
The version is encoded in the package version number (major.minor.patch → 8.5.11) so that the installed PHP version is always immediately visible from package.json.
Static extensions
Baked directly into the compiled php.wasm binary — always loaded, no separate install. Built by php-wasm-compiler, whose config.yaml is the single source of truth for this list.
| Extension | Purpose |
|---|---|
| curl | HTTP/HTTPS client — also what getPHPRuntimeWithNetwork()'s outbound proxy rides on |
| openssl | TLS/crypto primitives |
| iconv | Character set conversion |
| libxml / dom / simplexml / xmlreader / xmlwriter | XML/DOM support |
| mbstring | Multibyte string handling, built with oniguruma regex support |
| gd | Image processing/generation |
| imagick | ImageMagick bindings |
| exif | Image metadata reading |
| sockets | Low-level socket functions |
| zip | ZIP archive read/write (from libzip) |
| sqlite3 / pdo / pdo_sqlite | SQLite database + PDO abstraction |
| bz2 | BZip2 compression — also gives Phar its .tar.bz2 archive support |
| opcache | Bytecode caching (JIT disabled) |
| yaml | YAML 1.1 parsing (LibYAML) — see the YAML 1.1 scalar-coercion note above |
| jsonpath | JSONPath queries over decoded JSON (3.1.0, from supermetrics-public/pecl-jsonpath) |
| apcu / igbinary | In-memory user cache + compact binary serialization (igbinary is also apcu's default serializer) |
Query getLoadedExtensions() at runtime for the exact installed inventory rather than assuming an extension from another PHP build is available.
In-house extensions
Four of the extensions above are Maxime Larrivée-Roy's own PHP extensions, purpose-built for Kirigami and vendored straight from their own repos rather than pecl.php.net:
jsonk— fast JSON encode/decode plus JSON Schema validation (vendoredsimdjson/yyjson); replaces the built-injson_encode()/json_decode()whenjsonk.replace_json_functionsis enabled, the default in this buildnavicat— native client for Navicat Premium's HTTP-tunnel protocol (ntunnel_mysql.phpand friends), reusing the samelibcurlalready linked in abovemdhtml— CommonMark+GFM Markdown rendering viacmark-gfm, the backend behindphp-prepros'sMD::wrappernorm— Unicode normalization (aNormalizerclass plusnormalizer_normalize()/normalizer_is_normalized()), wrappingutf8proc
Loading additional extensions
Beyond the static set above, more PHP extensions ship as separate, on-demand WASM side modules — install any @kirigami/phpext-* package and @kirigami/php-wasm picks it up automatically at runtime, no core rebuild needed (see Automatic extension discovery below for the mechanism). Published by php-wasm-compiler: anydoc, dba, enchant, fastchart, ffi, fileinfo, ftp, gettext, gmp, intl, ldap, mysqli (bundles mysqlnd), odbc, pdo_dblib, pdo_firebird, pdo_mysql (bundles mysqlnd), pdo_odbc, pdo_pgsql, pgsql, posix, rar, scanmeqr, snmp, soap, sodium, tidy, xsl. intl is about 38 MB, since it embeds the ICU data; anydoc is written in Rust, and a panic there aborts the PHP runtime.
npm install @kirigami/phpext-pgsqlRelated
php-wasm-compiler— the compiler that builds the WASM binary and the@kirigami/phpext-*packages@php-wasm/universal— the runtime this loader integrates withwasm-feature-detect— used for JSPI detection
Runtime extension inspection
Inspect the embedded runtime used by your checkout:
npx kiri phpinfo -m
# From the monorepo, without relying on a globally installed CLI:
node packages/cli/bin/kiri.js phpinfo -m-m / --md selects Markdown output; -j / --json selects JSON. The full output includes runtime settings and environment data, so it is generated on demand instead of maintained as a large machine-specific snapshot here.
The local runtime inspection on 2026-09-20 confirmed mdhtml with cmark-gfm and yaml with LibYAML enabled. MD:: delegates to mdhtml; YAML:: delegates to native YAML. The PHP YAML path uses YAML 1.1 scalar rules, including implicit booleans; quote string keys/values such as "NO". Node’s kirigami.yaml parsing is a separate path. See the PHP class reference for wrapper behavior and Markdown migration notes.
Other bundled capabilities include JSON/JSONK, DOM/XML, GD/Imagick, cURL/OpenSSL, SQLite, APCu, igbinary, norm, lexbor, and navicat. Query getLoadedExtensions() for the exact installed inventory rather than assuming an extension from another PHP installation is available.
import { getLoadedExtensions } from '@kirigami/php-wasm';
console.log(await getLoadedExtensions());The standard and network helpers each cache their runtime. Closing the network proxy is final for that cached instance; it is not a per-request cleanup step followed by automatic reopening.
Automatic extension discovery
At runtime creation, the loader searches for directories named phpext-*, including those under @kirigami, in node_modules and packages under the current working directory and its ancestors. It also searches npm's global root for globally installed extension packages. That root is computed the way npm resolves its global prefix (npm_config_prefix, then prefix= in ~/.npmrc, then the default next to the Node executable) rather than by spawning npm; a prefix set only in a global or builtin npmrc is not seen. Sibling projects are not searched, so an unrelated compiler checkout next to a Kirigami site cannot affect its runtime.
When a package's index.js exports a default register(phpVersion) function (every package generated by php-wasm-compiler does), the loader calls it with the runtime's PHP major/minor version. It returns the package's modules in load order as { name, soPath, iniEntries? } entries, so one package can bundle a dependency module ahead of its extension (phpext-mysqli ships mysqlnd, then mysqli). iniEntries (an object of php.ini settings, also read from manifest.json in the fallback below) are written as key=value lines after the module's extension directive. A module bundled by several packages is loaded once.
Otherwise, manifest.json may provide an extension name and an artifacts array with phpVersion and sourcePath. The loader prefers an artifact matching the runtime's PHP major/minor version, then a matching patch version, then a generic artifact. If no usable manifest artifact is found, it recursively selects the first .so file.
The resolved extensions are staged under /internal/shared/extensions, each with an NNN-<name>.ini file holding its extension directive. The numeric prefix keeps the discovery order, since PHP reads that directory alphabetically.
Set KIRIGAMI_PHPEXT_DISCOVERY to local to skip the global root, or to off to disable discovery entirely (the regression suite uses off). There is no explicit search-root option. Artifact selection does not establish binary compatibility. Use getLoadedExtensions() to check what actually loaded; finding or staging a .so does not prove that PHP accepted it. Cached runtimes are not rescanned for every execution.
License
This package is distributed under GPL-2.0-or-later and is treated as a separate runtime component from the project core. See LICENSE for the full text.
The repository root is licensed under GPL-3.0-or-later, but @kirigami/php-wasm remains independently licensed because it contains a compiled PHP/WASM runtime and binary assets with their own provenance and compatibility constraints. See NOTICE for the package-level summary.
