Add PHP 5.2 WebAssembly builds and runtime support#3501
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds PHP 5.2.17 as a “legacy” WebAssembly runtime across web + node, and wires it through Playground (worker + CLI) to support booting older WordPress versions.
Changes:
- Introduces legacy PHP version typing/registry, loader-module wiring, and pre-boot
php.inihandling for PHP 5.2. - Adds new
@php-wasm/*-5-2build packages (web/node) plus runtime guards for unsupported extensions / multi-instance behavior. - Updates Playground boot flow for legacy WP/PHP constraints (WP zip sourcing, SQLite plugin selection, mu-plugin stubs, WP_DEBUG gating).
Reviewed changes
Copilot reviewed 50 out of 60 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.base.json | Adds TS path aliases for new @php-wasm/*-5-2 packages. |
| packages/playground/remote/src/lib/playground-worker-endpoint.ts | Limits instances for legacy PHP and swaps mu-plugin stub for PHP 5.2. |
| packages/playground/remote/src/lib/playground-worker-endpoint-blueprints-v1.ts | Downloads non-minified WP from wordpress.org; selects legacy SQLite; gates WP_DEBUG. |
| packages/playground/remote/src/lib/playground-mu-plugin/playground-includes/wp_http_fetch.php | Replaces null-coalescing for PHP 5.2 compatibility. |
| packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php | Adds PHP/WP version guards and PHP-5.2-safe fallbacks. |
| packages/playground/remote/src/lib/playground-mu-plugin/0-playground-php52.php | Adds PHP 5.2-compatible mu-plugin stub (named callbacks + dummy transport). |
| packages/playground/client/src/blueprints-v1-handler.ts | Skips update-check prefetch for legacy WordPress versions. |
| packages/playground/cli/tests/run-cli.spec.ts | Adjusts tests to avoid legacy WP versions in “supported” CLI boot test. |
| packages/playground/cli/src/run-cli.ts | Accepts legacy PHP versions; gates WP_DEBUG defaults/extensions for legacy; adds loader logic tweaks. |
| packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts | Passes phpVersion through V1 worker boot and request-handler creation. |
| packages/playground/cli/src/blueprints-v1/download.ts | Allows selecting SQLite integration zip version (including php52 variant). |
| packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts | Selects legacy SQLite integration zip for legacy PHP; passes phpVersion to worker boot. |
| packages/playground/blueprints/src/lib/v1/types.ts | Expands Blueprint PHP version typing to include legacy PHP. |
| packages/playground/blueprints/src/lib/v1/compile.ts | Uses AllPHPVersions for compilation-time version selection. |
| packages/playground/blueprints/src/lib/types.ts | Updates RuntimeConfiguration to AllPHPVersion. |
| packages/playground/blueprints/public/blueprint-schema.json | Adds LegacyPHPVersion/AllPHPVersion to schema and references them. |
| packages/php-wasm/web/src/lib/load-runtime.ts | Supports legacy php.ini preRun and throws on intl with legacy PHP. |
| packages/php-wasm/web/src/lib/get-php-loader-module.ts | Adds dynamic import for @php-wasm/web-5-2. |
| packages/php-wasm/web-builds/5-2/tsconfig.lib.json | New TS config for @php-wasm/web-5-2 build. |
| packages/php-wasm/web-builds/5-2/tsconfig.json | New TS project references for web 5.2 package. |
| packages/php-wasm/web-builds/5-2/src/index.ts | Implements JSPI/Asyncify loader selection for web PHP 5.2. |
| packages/php-wasm/web-builds/5-2/project.json | Nx targets for building/publishing web PHP 5.2 binaries. |
| packages/php-wasm/web-builds/5-2/package.json | Defines new @php-wasm/web-5-2 package metadata/exports. |
| packages/php-wasm/web-builds/5-2/build.js | esbuild bundling script for web PHP 5.2 package. |
| packages/php-wasm/universal/src/lib/supported-php-versions.ts | Adds legacy/all version lists + type guard isLegacyPHPVersion. |
| packages/php-wasm/universal/src/lib/proxy-file-system.ts | Skips PROXYFS mmap patch for legacy PHP instances due to parser bug. |
| packages/php-wasm/universal/src/lib/legacy-php-ini.ts | Adds legacy PHP pre-boot php.ini + preRun writer. |
| packages/php-wasm/universal/src/lib/index.ts | Re-exports legacy version helpers and legacy ini helpers. |
| packages/php-wasm/supported-php-versions.mjs | Registers PHP 5.2.17 in version metadata list. |
| packages/php-wasm/node/src/lib/load-runtime.ts | Supports legacy php.ini preRun and rejects extensions on legacy PHP. |
| packages/php-wasm/node/src/lib/get-php-loader-module.ts | Adds dynamic import for @php-wasm/node-5-2. |
| packages/php-wasm/node/src/lib/extensions/memcached/get-memcached-extension-module.ts | Refactors imports formatting (no functional change). |
| packages/php-wasm/node-builds/5-2/tsconfig.lib.json | New TS config for @php-wasm/node-5-2 build. |
| packages/php-wasm/node-builds/5-2/tsconfig.json | New TS project references for node 5.2 package. |
| packages/php-wasm/node-builds/5-2/src/index.ts | Implements JSPI/Asyncify loader selection for node PHP 5.2; stubs extension paths. |
| packages/php-wasm/node-builds/5-2/project.json | Nx targets for building/publishing node PHP 5.2 binaries. |
| packages/php-wasm/node-builds/5-2/package.json | Defines new @php-wasm/node-5-2 package metadata/exports. |
| packages/php-wasm/node-builds/5-2/build.js | esbuild bundling script for node PHP 5.2 package. |
| packages/php-wasm/compile/php/proc_open.h | Adds include guard and PHP 5.x pipe type compatibility. |
| packages/php-wasm/compile/php/proc_open.c | Adds PHP 5.x stub implementation for proc_* APIs. |
| packages/php-wasm/compile/php/php_wasm.c | Adds PHP-version guards for headers/APIs and PHP 5.x compatibility fixes. |
| packages/php-wasm/compile/php/php5.2.patch | Adds PHP 5.2 fixes (empty copy() crash; libxml buffer accessors). |
| packages/php-wasm/compile/php/php5.2-openssl-compat.patch | Adds OpenSSL 1.1+ compatibility shims for PHP 5.2 openssl extension. |
| packages/php-wasm/compile/php/apply-mysqlnd-patch.sh | Tolerates missing mysqlnd in older PHP versions; improves messaging/failure behavior. |
| packages/php-wasm/compile/php/Dockerfile | Adds PHP 5.2-specific build steps (autoconf 2.13, parser sources, flags, feature gating). |
| packages/php-wasm/compile/php-wasm-memory-storage/wasm_memory_storage.c | Disables custom memory storage for PHP < 7 with no-op init/shutdown. |
| packages/php-wasm/compile/php-wasm-dns-polyfill/dns_polyfill.h | Moves arginfo definitions out of header (compat). |
| packages/php-wasm/compile/php-wasm-dns-polyfill/dns_polyfill.c | Adds PHP 5.2 compat for macros/TSRMLS and restores arginfo definitions. |
| packages/php-wasm/compile/php-post-message-to-js/post_message_to_js.h | Moves arginfo definitions out of header (compat). |
| packages/php-wasm/compile/php-post-message-to-js/post_message_to_js.c | Adds PHP 5.2 compat for macros/TSRMLS and return handling. |
Comments suppressed due to low confidence (1)
packages/php-wasm/node/src/lib/load-runtime.ts:1
- Setting
preRun: ... : undefinedafter spreadingoptions.emscriptenOptionsoverwrites any caller-suppliedpreRunhook (even for non-legacy PHP), and for legacy PHP it replaces rather than appends to existingpreRunsteps. Prefer preserving existing preRun(s) and only addingcreateLegacyPhpIniPreRunStep()when legacy (e.g., normalize the existingpreRuninto an array, append the legacy step, and avoid writingpreRun: undefinedfor non-legacy).
import {
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
56514a4 to
ac01d55
Compare
Introduce LegacyPHPVersions and AllPHPVersions constants alongside AllPHPVersion type to distinguish legacy PHP builds from supported ones. Update Blueprint schema and types to accept legacy PHP versions.
Update Node.js and web runtime loaders to detect legacy PHP versions and pre-create php.ini with disable_functions=ini_get_all before SAPI startup (prevents WASM crashes). Gate extension loading (xdebug, intl, redis, memcached) behind legacy checks — these are unavailable for PHP 5.x. Update Dockerfile and C sources for PHP 5.x compilation: conditional libzip, SQLite, cURL, imagick, PCRE JIT, fiber-asm, and inline asm handling.
Add pre-compiled PHP 5.2.17 WASM binaries for both Node.js and web (asyncify and JSPI variants). Register the new packages in loader modules and tsconfig path aliases.
The web worker's blueprints v1 endpoint only knew two ways to fetch WordPress: a pre-built minified bundle (for versions in MinifiedWordPressVersionsList) or a direct HTTP URL. That left bare dotted versions like "4.9" or "1.5" with no path to boot, even though wordpress.org hosts them. Add a third branch that resolves any non-minified, non-URL version to `https://wordpress.org/wordpress-<version>.zip`, routed through the CORS proxy when configured. A regex guard rejects anything that isn't a plain dotted numeric version before it reaches URL interpolation. The WP 1.x releases wordpress.org serves are only keyed under their full patch version (there is no `wordpress-1.5.zip`), so a normalizeWordPressVersion helper rewrites the three known bare <2.0 versions to their canonical patch release. This unblocks booting any historical WordPress release from the web worker and is a prerequisite for the upcoming legacy WP boot test (which iterates versions 1.0 through 4.9).
Web worker: select pre-patched SQLite v2.2.22-php52 for legacy PHP, force single-instance mode for legacy PHP (PROXYFS parser state bug), disable WP_DEBUG for legacy WP (missing WP_DEBUG_DISPLAY). CLI: pass phpVersion through boot chain, select correct SQLite version, disable extensions for legacy PHP, add pre-built WordPress lookup. Mu-plugins: add PHP 5.2-compatible stub (named functions instead of closures), guard function_exists checks for WP < 3.0, use array() syntax for PHP 5.3 compatibility, handle Requests class variations. Skip update checks prefetch for legacy WordPress.
| @@ -0,0 +1,76 @@ | |||
| <?php | |||
| /** | |||
| * PHP 5.2-compatible minimal version of 0-playground.php. | |||
There was a problem hiding this comment.
should we remove closures from 0-playground.php and use the same file everywhere? Or would that just complicate things?
There was a problem hiding this comment.
Claude's opinion:
The two files serve quite different environments, not just different PHP syntax:
0-playground.php (PHP 5.3+, modern WP) has login message, networking-disabled admin UI, link rewriting, URL reporting to parent frame, pattern picker disable, client-side media disable, Fetch or Dummy HTTP transport, uses \WpOrg\Requests\Requests (namespaced, PHP 5.3+), uses str_ends_with (PHP 8.0+), and 9 closures.
0-playground-php52.php (PHP 5.2, old WP) has Dummy HTTP transport only (the fetch transport files use PHP 5.3+ syntax), WP Cron disable, uses class_exists('Requests') (no namespace), and uses substr instead of str_ends_with.
Removing closures is the easy part — just convert to named functions. The hard part is that most features in the main file don't apply to PHP 5.2 + old WordPress:
get_current_user_id()may not exist in WP 2.x- The networking UI assumes modern admin screens
str_ends_withis PHP 8.0+- The Requests namespace class is PHP 5.3+
- Fetch transport files themselves won't parse on PHP 5.2
Unifying would mean wrapping most of the file in version_compare guards, making it harder to read than having two separate files. Keeping them separate seems cleaner given how different the two environments are.
| // polyfills added) | ||
| // - Everything else: whatever the caller requested | ||
| const isLegacyPhp = isLegacyPHPVersion(phpVersion); | ||
| const effectiveSqliteVersion = isLegacyPhp |
There was a problem hiding this comment.
should we support PHP 5.2 in that plugin? Or have a branch where Rector automatically downgrades the codebase to 5.2?
There was a problem hiding this comment.
| } | ||
|
|
||
| const parsedSiteUrl = new URL(siteUrl); | ||
| // Legacy PHP has a parser state bug that corrupts large include |
There was a problem hiding this comment.
Is it legacy PHP, or is it us? Those sites worked back then, perhaps proxyfs is flawed? I think we can merge this PR as is, but wdyt about rephrasing this comment and getting some long-running agent to identify the root cause?
Move all PHP 5.2-specific build logic into a dedicated Dockerfile.php52, restoring the main Dockerfile to its trunk state (zero diff). This avoids cluttering the main Dockerfile with version-specific conditionals. The new file mirrors the main Dockerfile structure but with PHP 5.2 adjustments hardcoded: autoconf 2.13, different configure flags, no OPcache/mysqlnd/imagick/fibers/phar, PHP5_FLAGS for emcc, etc. build.js now selects the appropriate Dockerfile based on the PHP version.
Split the if/else into two independent guards so the extension-loading block doesn't nest inside an else.
The multi-instance corruption on legacy PHP may be a PROXYFS bug rather than a PHP parser issue — these sites worked on real servers originally. Add a TODO to investigate the actual root cause.
adamziel
left a comment
There was a problem hiding this comment.
Looks great! My only real concern was the pointer cast thing and it appears like EMULATE_POINTER_CASTS solved that. I'm still concerned about the overhead part of it:
Use EMULATE_FUNCTION_POINTER_CASTS. When you build with -sEMULATE_FUNCTION_POINTER_CASTS, Emscripten emits code to emulate function pointer casts at runtime, adding extra arguments/dropping them/changing their type/adding or dropping a return type/etc. This can add significant runtime overhead, so it is not recommended, but is worth trying.
It would be interesting to measure that somehow and explore alternatives if anything is significantly slower. That's not a blocker here at all. Thank you @JanJakes!
@adamziel |
|
|
||
| if (loaderOptions.withIntl) { | ||
| emscriptenOptions = withIntl(phpVersion, emscriptenOptions); | ||
| if (isLegacy) { |
There was a problem hiding this comment.
We could use the same approach as for node/load-runtime.ts with :
if (
isLegacy &&
(loaderOptions?.withIntl)
) {
throw new Error(
`Extensions (intl) are not ` +
`available for legacy PHP ${phpVersion}.`
);
}
if (!isLegacy) {
....maybe?
Replace the duck-typing loop that searched all symbols for a phpVersion property with the same [0] access pattern used by every other function in proxy-file-system.ts.
Match the web version's parameter type. SupportedPHPVersion | string was effectively just string, losing type safety. AllPHPVersion enumerates all valid versions including legacy ones.
Replace the hardcoded '5.2' check with the isLegacyPHPVersion() type guard so any future legacy version also gets the compatible mu-plugin stub.
proc_open always returns FALSE in the WASM build, so no process resources are ever created. The destructor exists only because zend_register_list_destructors_ex requires a callback.
Explain how WP_Http prepends "WP_Http_" to filter entries, why the class name is Wp_Http_Dummy, and why it intentionally does not implement the Requests_Transport interface.
Explain how parseFloat extracts the major version from dotted version strings and how non-numeric values like "nightly" or "trunk" are handled via Number.isFinite.
The PROXYFS stale-fstat issue that caused parse errors on secondary PHP instances is already handled by skipping the PROXYFS mmap patch for legacy PHP (isLegacyPhpInstance check in proxyFileSystem). Without mmap, PHP falls back to a read-based path that handles size mismatches gracefully. The maxPhpInstances: 1 workaround was redundant. See 60a57ee on legacy-wordpress-support-v2 for the original fix.
Explain the actual root cause: pre-mount MEMFS nodes retain stale fstat sizes after PROXYFS shadows them, and PHP 5.x's mmap-based zend_stream_fixup trusts that size. Without the mmap patch, Emscripten returns ENOSYS and PHP uses a read-based fallback that handles size mismatches gracefully.
Match the node version's pattern: early throw for legacy + guard the extension setup behind !isLegacy, instead of nesting the legacy check inside the extension check.
|
The failing test looks like a flake, let's ship! |
…3506) ## Summary - Bumps the newly-introduced `@php-wasm/web-5-2` and `@php-wasm/node-5-2` packages from `3.1.19` → `3.1.20` to match the current workspace version (`lerna.json`). Without this they would have been published below their own `@php-wasm/universal@3.1.20` dependency and skipped by the next `lerna publish patch`. - Adds `README.md` for both packages, mirroring the 8-5 style. The blurb notes that 5.2 bundles no extensions (intl/Xdebug/Redis/Memcached getters all throw), so the npmjs.com page isn't blank and consumers aren't surprised. Follow-up to #3501. Both packages are already live on npm at `3.1.20` — this PR brings the source tree in sync so the weekly `lerna publish` CI picks up cleanly from here. ## Test plan - [x] `npm view @php-wasm/web-5-2 version` → `3.1.20` - [x] `npm view @php-wasm/node-5-2 version` → `3.1.20` - [x] `npm access list collaborators @php-wasm/web-5-2` matches `@php-wasm/web-8-5` - [x] Trusted Publisher configured on npmjs.com for both packages (repo `WordPress/wordpress-playground`, workflow `publish-npm-packages.yml`, environment `npm`)
|
It looks like the implementation of |
## Motivation for the change, related issues [Add PHP 5.2 WebAssembly builds and runtime support](#3501) added `TSRMLS_CC` macros to `dns_polyfill` and `post_message_to_js` for PHP 5.x compatibility, but these macros don't exist in PHP 7+. This breaks recompilation of PHP.wasm for all PHP versions 7.0 through 8.5. ## Implementation details Added empty `TSRMLS_CC` fallback defines to `dns_polyfill.c` and `post_message_to_js.c` ## Testing Instructions (or ideally a Blueprint) - [x] `nx run php-wasm-node:recompile-php:jspi` - [x] `nx run php-wasm-node:recompile-php:asyncify` - [x] `nx run php-wasm-web:recompile-php:jspi` - [x] `nx run php-wasm-web:recompile-php:asyncify`
> **Stacked on #3501** (Add PHP 5.2 WebAssembly builds and runtime support). Review that PR first. ## Summary Boot legacy WordPress (1.0 – 6.2) in the browser, with SQLite as the database. <img width="743" height="586" alt="Screenshot 2026-04-15 at 13 30 53" src="https://github.com/user-attachments/assets/1c42ebab-cfb8-4968-88e8-b8a628a42515" /> The Playground settings panel gains an **"Include older versions"** checkbox that exposes every WordPress release from 1.0 through 6.9 (with jazz release code names) and auto-locks the PHP version to whatever runs it. Replaces and closes #3469 (the PHP 5.6 version of the same feature). ## What's in this PR Builds on the PHP 5.2 WASM runtime from #3501 and adds: - **Offline PHP 7+ → 5.2 downgrader** (`scripts/php52-downgrader/`) — a nikic/PhpParser pipeline of ~20 visitors that strips modern syntax so arbitrary PHP 7-era files parse on PHP 5.2. - **SQLite plugin patcher** (`scripts/patch-sqlite-for-php52.mjs`) — produces a PHP 5.2-compatible build of the upstream SQLite integration plugin. - **Legacy WordPress boot flow** (`wordpress/src/boot.ts`, `legacy-wp-fixes.ts`, `mysql-shims.ts`) — legacy-aware installer path, PDO table fallback for WP < 3.5, ~3400 lines of targeted source patches for WP 1.0 – 2.8 parser/SQL quirks, auth/nonce compat, `wp-db.php` SQLite compatibility, and more. - **WP 5.0–6.2 on PHP 7.4** — extends the legacy boot to cover the mid-modern range where PHP 8.x is unreliable. - **Site settings UI** — "Include older versions" checkbox + grouped dropdown + automatic PHP locking: WP 1.0 – 4.9 → PHP 5.2, WP 5.0 – 6.2 → PHP 7.4, WP 6.3+ → default. - **Release code names** — WP version picker shows jazz-musician shorthand (e.g. "6.9 (Gene)", "4.9 (Tipton)"). - **TinyMCE content_css fix** — inlines editor CSS via `content_style` to work around TinyMCE's `document.open()` creating a service-worker-uncontrolled iframe. ## Test plan ### Automated (Playwright, all 32 legacy versions) ```bash npm run dev # Wait for http://localhost:5400/website-server/ to be ready node packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjs ``` Covers front page, single post, admin dashboard, new post page, and plugin activation across every WP minor release from 1.0 to 6.2. ### Manual UI walk-through 1. `npm run dev` and open http://localhost:5400/. 2. Open the settings gear → tick **Include older versions**. 3. Verify the WP dropdown shows two groups and every release from 1.0 to 6.9. 4. Pick **WP 4.9** → PHP locks to 5.2 (disabled). 5. Pick **WP 5.5** → PHP locks to 7.4 (disabled). 6. Pick **WP 4.8**, go to Posts → Add New, verify TinyMCE loads without CSS errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Summary
LegacyPHPVersions/AllPHPVersionstype system and register PHP 5.2.17 in the version registryZEND_FE_END/PHP_FE_END), yytext duplicate-definition handling, mysqlnd gating, CLI getopt linking,php_cli_process_titleversion guardini_get_all, OPcache), single-instance mode for PROXYFS, correct SQLite plugin selection, extension gating, PHP 5.2-compatible mu-plugin stubTest plan
npx nx recompile-php:jspi php-wasm-web -- --PHP_VERSION=5.2succeedsnpx nx recompile-php:asyncify php-wasm-web -- --PHP_VERSION=5.2succeedsnpx nx recompile-php:jspi php-wasm-node -- --PHP_VERSION=5.2succeedsnpx nx recompile-php:asyncify php-wasm-node -- --PHP_VERSION=5.2succeedsnpx nx typecheck playground-websitepassesnpx nx run-many -t lint --projects=php-wasm-web,php-wasm-node,php-wasm-universal,playground-websitepasses🤖 Generated with Claude Code