Skip to content

Commit 56514a4

Browse files
committed
Integrate PHP 5.2 support in web worker, CLI, and mu-plugins
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.
1 parent 4dae086 commit 56514a4

File tree

13 files changed

+249
-52
lines changed

13 files changed

+249
-52
lines changed

packages/php-wasm/universal/src/lib/proxy-file-system.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
import type { PHP } from './php';
22

3+
/**
4+
* Reads the major PHP version from an instance's Emscripten runtime and
5+
* returns true for anything older than PHP 7. Used to decide whether the
6+
* PROXYFS mmap patch should apply — see {@link proxyFileSystem} for the
7+
* legacy-PHP rationale. Falls back to `false` when the runtime hasn't
8+
* populated phpVersion yet, matching the pre-refactor default behaviour.
9+
*/
10+
function isLegacyPhpInstance(phpInstance: PHP): boolean {
11+
// Find the private runtime symbol by inspecting its value rather
12+
// than assuming it's the first symbol (fragile if more symbols
13+
// are added to the PHP class).
14+
const symbols = Object.getOwnPropertySymbols(phpInstance);
15+
let runtime: any;
16+
for (const sym of symbols) {
17+
// @ts-ignore
18+
const val = phpInstance[sym];
19+
if (val && typeof val === 'object' && 'phpVersion' in val) {
20+
runtime = val;
21+
break;
22+
}
23+
}
24+
const major: number | undefined = runtime?.phpVersion?.major;
25+
return typeof major === 'number' && major < 7;
26+
}
27+
328
/**
429
* Adds mmap support to PROXYFS for memory-mapping files across PHP instances.
530
*
@@ -136,8 +161,12 @@ function ensureProxyFSHasMmapSupport(phpInstance: PHP) {
136161
* For example, mounting /wordpress from the parent instance into a child worker allows
137162
* both to access the same WordPress installation without copying the entire directory.
138163
*
139-
* The function automatically patches PROXYFS with mmap support before mounting, ensuring
140-
* libraries like ICU can memory-map data files through the proxied filesystem.
164+
* The function automatically patches PROXYFS with mmap support before mounting on
165+
* PHP 7+, so libraries like ICU can memory-map data files through the proxied
166+
* filesystem. Legacy PHP (< 7) skips the mmap patch: its `zend_compile_file` trusts
167+
* stale fstat sizes on mmap'd streams and reads past the real EOF when the primary
168+
* has rewritten files after the secondary was created. PHP 7+ removed the mmap path
169+
* from `zend_stream_fixup` entirely so the patch is only needed there.
141170
*
142171
* Mounts are registered via php.mount() so they survive runtime rotation.
143172
* When the replica's WASM module is hot-swapped, hotSwapPHPRuntime()
@@ -152,6 +181,7 @@ export async function proxyFileSystem(
152181
replica: PHP,
153182
paths: string[]
154183
) {
184+
const replicaIsLegacy = isLegacyPhpInstance(replica);
155185
// We can't just import the symbol from the library because
156186
// Playground CLI is built as ESM and php-wasm-node is built as
157187
// CJS and the imported symbols will differ in the production build.
@@ -164,7 +194,9 @@ export async function proxyFileSystem(
164194
// after runtime rotation in hotSwapPHPRuntime().
165195
replica.mkdir(path);
166196
await replica.mount(path, (php: PHP) => {
167-
ensureProxyFSHasMmapSupport(php);
197+
if (!replicaIsLegacy) {
198+
ensureProxyFSHasMmapSupport(php);
199+
}
168200
const replicaSymbol = Object.getOwnPropertySymbols(php)[0];
169201
// @ts-ignore
170202
php[replicaSymbol].FS.mount(

packages/playground/cli/src/blueprints-v1/blueprints-v1-handler.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { logger } from '@php-wasm/logger';
22
import { EmscriptenDownloadMonitor, ProgressTracker } from '@php-wasm/progress';
33
import {
44
consumeAPI,
5+
isLegacyPHPVersion,
56
type Pooled,
67
type UniversalPHP,
78
} from '@php-wasm/universal';
@@ -116,7 +117,13 @@ export class BlueprintsV1Handler {
116117
sqliteIntegrationPluginZip = undefined;
117118
} else {
118119
this.cliOutput.updateProgress('Preparing SQLite database');
119-
sqliteIntegrationPluginZip = await fetchSqliteIntegration();
120+
// Use pre-patched v2.2.22 for legacy PHP (closures replaced
121+
// with named functions, PHP 5.2 polyfills added offline).
122+
const phpVersion = this.args.php || RecommendedPHPVersion;
123+
const isLegacyPhp = isLegacyPHPVersion(phpVersion);
124+
const sqliteVersion = isLegacyPhp ? 'v2.2.22-php52' : 'trunk';
125+
sqliteIntegrationPluginZip =
126+
await fetchSqliteIntegration(sqliteVersion);
120127
}
121128

122129
this.cliOutput.updateProgress('Booting WordPress');
@@ -130,6 +137,7 @@ export class BlueprintsV1Handler {
130137
playground as unknown as PlaygroundCliBlueprintV1Worker
131138
).bootWordPress(
132139
{
140+
phpVersion: runtimeConfiguration.phpVersion,
133141
wpVersion: runtimeConfiguration.wpVersion,
134142
siteUrl: this.siteUrl,
135143
wordpressInstallMode:

packages/playground/cli/src/blueprints-v1/download.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import path, { basename } from 'path';
66

77
export const CACHE_FOLDER = path.join(os.homedir(), '.wordpress-playground');
88

9-
export async function fetchSqliteIntegration(): Promise<File> {
9+
export async function fetchSqliteIntegration(
10+
version: 'trunk' | 'v2.1.16' | 'v2.2.22' | 'v2.2.22-php52' = 'trunk'
11+
): Promise<File> {
1012
// Production builds: the ZIP sits next to the bundled JS.
1113
const dir =
1214
typeof __dirname !== 'undefined' ? __dirname : import.meta.dirname;
@@ -22,7 +24,7 @@ export async function fetchSqliteIntegration(): Promise<File> {
2224
wpBuildsDir,
2325
'src',
2426
'sqlite-database-integration',
25-
'sqlite-database-integration-trunk.zip'
27+
`sqlite-database-integration-${version}.zip`
2628
);
2729
}
2830

packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { FileLockManager } from '@php-wasm/universal';
22
import { loadNodeRuntime } from '@php-wasm/node';
33
import { EmscriptenDownloadMonitor } from '@php-wasm/progress';
4-
import type { PathAlias, SupportedPHPVersion } from '@php-wasm/universal';
4+
import type { AllPHPVersion, PathAlias } from '@php-wasm/universal';
55
import {
66
PHPWorker,
77
releaseApiProxy,
@@ -27,6 +27,7 @@ import type { Mount } from '@php-wasm/cli-util';
2727

2828
export type WorkerBootWordPressOptions = {
2929
siteUrl: string;
30+
phpVersion?: string;
3031
wpVersion?: string;
3132
wordpressInstallMode: WordPressInstallMode;
3233
wordPressZip?: ArrayBuffer;
@@ -40,7 +41,7 @@ export type WorkerBootWordPressOptions = {
4041

4142
interface WorkerBootRequestHandlerOptions {
4243
siteUrl: string;
43-
phpVersion: SupportedPHPVersion;
44+
phpVersion: AllPHPVersion;
4445
processId: number;
4546
trace: boolean;
4647
nativeInternalDirPath: string;
@@ -102,6 +103,7 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
102103
this.bootedWordPress = true;
103104
const {
104105
siteUrl,
106+
phpVersion,
105107
wordpressInstallMode,
106108
wordPressZip,
107109
sqliteIntegrationPluginZip,
@@ -112,6 +114,7 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
112114
try {
113115
await bootWordPress(this.__internal_getRequestHandler()!, {
114116
siteUrl,
117+
phpVersion,
115118
wordpressInstallMode,
116119
wordPressZip:
117120
wordPressZip !== undefined
@@ -161,6 +164,7 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
161164
try {
162165
const requestHandler = await bootRequestHandler({
163166
siteUrl: options.siteUrl,
167+
phpVersion: options.phpVersion,
164168
maxPhpInstances: 1,
165169
createPhpRuntime: createPhpRuntimeFactory(
166170
options,

packages/playground/cli/src/run-cli.ts

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
type PHPRequest,
77
type PathAlias,
88
type RemoteAPI,
9-
type SupportedPHPVersion,
9+
type AllPHPVersion,
1010
} from '@php-wasm/universal';
1111
import {
1212
PHPResponse,
@@ -46,6 +46,8 @@ import type { PlaygroundCliBlueprintV2Worker } from './blueprints-v2/worker-thre
4646
import type { XdebugOptions } from '@php-wasm/node';
4747
/* eslint-disable no-console */
4848
import {
49+
AllPHPVersions,
50+
isLegacyPHPVersion,
4951
SupportedPHPVersions,
5052
FileLockManagerInMemory,
5153
} from '@php-wasm/universal';
@@ -118,7 +120,7 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
118120
describe: 'PHP version to use.',
119121
type: 'string',
120122
default: RecommendedPHPVersion,
121-
choices: SupportedPHPVersions,
123+
choices: AllPHPVersions,
122124
},
123125
wp: {
124126
describe: 'WordPress version to use.',
@@ -373,7 +375,7 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
373375
describe: 'PHP version to use.',
374376
type: 'string',
375377
default: RecommendedPHPVersion,
376-
choices: SupportedPHPVersions,
378+
choices: AllPHPVersions,
377379
},
378380
wp: {
379381
describe: 'WordPress version to use.',
@@ -662,14 +664,24 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
662664
const hasDebugDefine = (name: string) => {
663665
return name in define || name in defineBool || name in defineNumber;
664666
};
665-
if (!hasDebugDefine('WP_DEBUG')) {
666-
define['WP_DEBUG'] = 'true';
667-
}
668-
if (!hasDebugDefine('WP_DEBUG_LOG')) {
669-
define['WP_DEBUG_LOG'] = 'true';
670-
}
671-
if (!hasDebugDefine('WP_DEBUG_DISPLAY')) {
672-
define['WP_DEBUG_DISPLAY'] = 'false';
667+
// Don't default WP_DEBUG* on for legacy PHP: old WordPress
668+
// (pre-2.3) prints E_NOTICE output before headers are sent,
669+
// which corrupts redirects and breaks the installer. The web
670+
// worker path applies the same gate in
671+
// @wp-playground/client/src/blueprints-v1-handler.ts.
672+
const phpVersionForDebug = (args['php'] ||
673+
RecommendedPHPVersion) as AllPHPVersion;
674+
const isLegacyPhpForDebug = isLegacyPHPVersion(phpVersionForDebug);
675+
if (!isLegacyPhpForDebug) {
676+
if (!hasDebugDefine('WP_DEBUG')) {
677+
define['WP_DEBUG'] = 'true';
678+
}
679+
if (!hasDebugDefine('WP_DEBUG_LOG')) {
680+
define['WP_DEBUG_LOG'] = 'true';
681+
}
682+
if (!hasDebugDefine('WP_DEBUG_DISPLAY')) {
683+
define['WP_DEBUG_DISPLAY'] = 'false';
684+
}
673685
}
674686

675687
const cliArgs = {
@@ -736,9 +748,7 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) {
736748
currentError = currentError.cause as Error;
737749
} while (currentError instanceof Error);
738750
console.error(
739-
'\x1b[1m' +
740-
messageChain.join(' caused by: ') +
741-
'\x1b[0m'
751+
'\x1b[1m' + messageChain.join(' caused by: ') + '\x1b[0m'
742752
);
743753
}
744754
} else {
@@ -811,7 +821,7 @@ export interface RunCLIArgs {
811821
mount?: Mount[];
812822
'mount-before-install'?: Mount[];
813823
outfile?: string;
814-
php?: SupportedPHPVersion;
824+
php?: AllPHPVersion;
815825
port?: number;
816826
'site-url'?: string;
817827
quiet?: boolean;
@@ -990,6 +1000,15 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
9901000
args.memcached = await jspi();
9911001
}
9921002

1003+
// Disable all extensions for legacy PHP versions — they're not available.
1004+
const isLegacyPhp = isLegacyPHPVersion(args.php || RecommendedPHPVersion);
1005+
if (isLegacyPhp) {
1006+
args.intl = false;
1007+
args.redis = false;
1008+
args.memcached = false;
1009+
args.xdebug = false;
1010+
}
1011+
9931012
// Setup phpMyAdmin if enabled.
9941013
if (args.phpmyadmin) {
9951014
if (true === args.phpmyadmin) {
@@ -1098,13 +1117,16 @@ export async function runCLI(args: RunCLIArgs): Promise<RunCLIServer | void> {
10981117
vfsPath: '/',
10991118
};
11001119

1101-
const isPHP85orHigher =
1102-
SupportedPHPVersions.indexOf(
1103-
args.php || RecommendedPHPVersion
1104-
) <= SupportedPHPVersions.indexOf('8.5');
1120+
const phpVer = args.php || RecommendedPHPVersion;
1121+
// SupportedPHPVersions is ordered newest-first, so a
1122+
// lower index means a higher version.
1123+
const isPhp85OrHigher =
1124+
SupportedPHPVersions.includes(phpVer as any) &&
1125+
SupportedPHPVersions.indexOf(phpVer as any) <=
1126+
SupportedPHPVersions.indexOf('8.5');
11051127

11061128
// And, if PHP >= 8.5, add the new Xdebug config.
1107-
if (isPHP85orHigher) {
1129+
if (isPhp85OrHigher) {
11081130
await createTempDirSymlink(
11091131
nativeDir.path,
11101132
symlinkPath,

packages/playground/cli/tests/run-cli.spec.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,11 @@ describe.each(blueprintVersions)(
171171
test('should set WordPress version', async () => {
172172
const { MinifiedWordPressVersionsList } =
173173
await import('@wp-playground/wordpress-builds');
174-
const oldestSupportedVersion =
175-
MinifiedWordPressVersionsList[
176-
MinifiedWordPressVersionsList.length - 1
177-
];
174+
// Use the oldest non-legacy version. Legacy versions
175+
// (< 5.0) require legacy PHP and can't boot on modern PHP.
176+
const oldestSupportedVersion = MinifiedWordPressVersionsList.filter(
177+
(v) => parseFloat(v) >= 5
178+
).pop()!;
178179
await using cliServer = await runCLI({
179180
...suiteCliArgs,
180181
command: 'server',
@@ -339,10 +340,7 @@ describe.each(blueprintVersions)(
339340

340341
const mounts = [];
341342
for (let i = 0; i < 5; i++) {
342-
const hostSubDir = path.join(
343-
hostTmpDir,
344-
`migration-${i}`
345-
);
343+
const hostSubDir = path.join(hostTmpDir, `migration-${i}`);
346344
mkdirSync(hostSubDir, { recursive: true });
347345
const hostFilePath = path.join(
348346
hostSubDir,

packages/playground/client/src/blueprints-v1-handler.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,15 @@ export class BlueprintsV1Handler {
8787

8888
/**
8989
* Pre-fetch WordPress update checks to speed up the initial wp-admin load.
90+
* Skip for old WordPress versions — the functions called by prefetch
91+
* (wp_check_php_version, wp_update_plugins, etc.) don't exist or crash
92+
* on legacy WP, and the resulting PHP errors create noise.
9093
*
9194
* @see https://github.com/WordPress/wordpress-playground/pull/2295
9295
*/
93-
if (runtimeConfiguration.networking) {
96+
const wpMajor = parseFloat(runtimeConfiguration.wpVersion);
97+
const isLegacyWpVersion = Number.isFinite(wpMajor) && wpMajor < 5;
98+
if (runtimeConfiguration.networking && !isLegacyWpVersion) {
9499
await playground.prefetchUpdateChecks();
95100
}
96101

0 commit comments

Comments
 (0)