Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,7 @@ export const defineWpConfigConsts: StepHandler<
case 'rewrite-wp-config': {
const documentRoot = await playground.documentRoot;
const wpConfigPath = joinPaths(documentRoot, '/wp-config.php');
await defineWpConfigConstants(
playground,
wpConfigPath,
consts,
'rewrite'
);
await defineWpConfigConstants(playground, wpConfigPath, consts);
break;
}
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ export class BlueprintsV1Handler {
mountsBeforeWpInstall,
mountsAfterWpInstall,
wordPressZip: wordPressZip && (await wordPressZip!.arrayBuffer()),
wpConfigDefaultConstants: this.args.wpConfigDefaultConstants,
sqliteIntegrationPluginZip:
await sqliteIntegrationPluginZip?.arrayBuffer(),
firstProcessId: 0,
Expand Down
3 changes: 3 additions & 0 deletions packages/playground/cli/src/blueprints-v1/worker-thread-v1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type WorkerBootOptions = {
export type PrimaryWorkerBootOptions = WorkerBootOptions & {
wpVersion?: string;
wordPressZip?: ArrayBuffer;
wpConfigDefaultConstants?: Record<string, string | number | boolean | null>;
sqliteIntegrationPluginZip?: ArrayBuffer;
dataSqlPath?: string;
};
Expand Down Expand Up @@ -129,6 +130,7 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
mountsAfterWpInstall,
phpVersion: php = RecommendedPHPVersion,
wordPressZip,
wpConfigDefaultConstants,
sqliteIntegrationPluginZip,
firstProcessId,
processIdSpaceLength,
Expand Down Expand Up @@ -182,6 +184,7 @@ export class PlaygroundCliBlueprintV1Worker extends PHPWorker {
wordPressZip !== undefined
? new File([wordPressZip], 'wordpress.zip')
: undefined,
wpConfigDefaultConstants,
sqliteIntegrationPluginZip:
sqliteIntegrationPluginZip !== undefined
? new File(
Expand Down
13 changes: 12 additions & 1 deletion packages/playground/cli/src/blueprints-v2/worker-thread-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
type ParsedBlueprintV2String,
type RawBlueprintV2Data,
} from '@wp-playground/blueprints';
import { bootRequestHandler } from '@wp-playground/wordpress';
import { bootRequestHandler, ensureWpConfig } from '@wp-playground/wordpress';
import { existsSync } from 'fs';
import path from 'path';
import { rootCertificates } from 'tls';
Expand Down Expand Up @@ -250,6 +250,17 @@ export class PlaygroundCliBlueprintV2Worker extends PHPWorker {
return;
}

/*
* Add required constants to "wp-config.php" if they are not already defined.
* This is needed, because some WordPress backups and exports may not include
* definitions for some of the necessary constants.
*/
await ensureWpConfig(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a good chance the WordPress site doesn't exist at this point. We'll need to run this at the right time during the Blueprint v2 lifecycle, e.g. after resolving the site. We could use this ensureWpConfig() call ATM or inject an initial Blueprint step.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamziel I think that all of this logic makes sense only with the apply-to-existing-site mode, right? For a new site, we always seem to copy the wp-config-sample.php file. If none of wp-config.php and wp-config-sample.php exists, this will be a noop.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced – the comment above this method call even says:

This is needed, because some WordPress backups and exports may not include
definitions for some of the necessary constants.

Which suggests we'll see data structures looking like existing sites that don't have these constants defined. Although, in those cases, I suppose the database won't be configured so they're not going to boot anyway. Hm. 🤔 Perhaps you're right and that only matters for new sites, after all?

Even then, we need to do it at the right time of the Blueprint v2 lifecycle – we'll typically download an unzip a WordPress release as a part of await this.runBlueprintV2(args);.

Copy link
Member Author

@JanJakes JanJakes Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamziel

Perhaps you're right and that only matters for new sites, after all?

Actually, what I want to say is that ensureWpConfig matters only for existing sites (--mode=apply-to-existing-site). That is—you have a site dump in your directory, and maybe the wp-config.php doesn't have some required configuration. This dump is not specified by Blueprint (v2), we can only apply some Blueprint to it.

Even then, we need to do it at the right time of the Blueprint v2 lifecycle – we'll typically download an unzip a WordPress release as a part of await this.runBlueprintV2(args);.

I guess the question is whether the ensureWpConfig should be a Playground feature, or a Blueprint v2 feature. My thinking at the moment is that it is a runtime (Playground) feature. If a Blueprint v2 references or creates an invalid wp-config.php, then it would be an invalid Blueprint.

With both v1 and v2, this can also happen:

  1. You have some constants missing in your existing site dump (e.g., Studio).
  2. We add the missing constants; a DB is provided (e.g., by Studio).
  3. Blueprint v1 or v2 is applied to an existing site.
  4. If the Blueprint messed up the config, we won't try to fix it at this stage.

When you create a new site from a Blueprint (even from a ZIP with missing config constants), then it wouldn't touch it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensureWpConfig matters only for existing sites

I'd like to learn more about the types of import you had in mind in here.

My thinking is we're talking about two separate data structures:

  • site – a WordPress directory structure configured so that it's ready to run via php -S
  • site export – a WordPress directory structure that requires processing before it can run

I'd say ensureWpConfig is not needed for a site at all. We're either running that site or making a change to it, e.g. by installing a plugin. For an site export, however, we need to do some processing before we're able to run it. I assume most site exports were produced by an export tool that has an accompanying import tool.

We can process exports compatible with the Blueprint v2 bundle format, but I don't think there's any generalized way we can process an arbitrarily mangled site export – perhaps we should just bale out on it. Are we doing that now? Or would that be a change that would also break Studio?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamziel All of this makes me wonder if the most "correct" solution for Playground may indeed be to only throw an error and do nothing else when a constant is missing, just like php -S would do.

Maybe this is not on the table now, and it would surely mean moving this logic to Studio, but I do wonder if there are other use cases where auto-backfilling missing constants is needed, or whether we've just gotten here due to some historic behaviors in wp-now or somewhere. Originally, the fallbacks were done at runtime via an MU plugin, but now, when the config is processed statically, the logic could maybe live in Studio as well. I guess ultimately, the question is whether this is needed in Playground in a broader sense.

Otherwise, as for the next steps, I agree with your suggestions 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need something like this for new sites, too, but I guess the existing blueprint runners take care of that.

@adamziel As for this part, I think we currently only copy wp-config-sample.php for new sites. Maybe we should update it to use some more reasonable defaults (e.g., for the salts and stuff).

Copy link
Collaborator

@adamziel adamziel Oct 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah perhaps that's just a historic wp-now issue. It seems like we're really processing a site import, aren't we? Perhaps we could export a repair function for Studio to call on their own if needed. Or just move it there entirely by proposing a Studio PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamziel I agree. If we don't see any other use cases, maybe we really should consider contributing this to Studio. Thinking this through:

The WP_Config_Transformer logic will be needed in Blueprints v2 (and v1 runner as well, while it's around). If we move it to PHP toolkit, is there a way to reuse it from there in Studio and Blueprints v1?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we move it to PHP toolkit, is there a way to reuse it from there in Studio and Blueprints v1?

There probably is, but let's not overcomplicate it for now and just do a copy&paste until we set up some infrastructure for reusing PHP toolkit.

primaryPhp,
primaryPhp.requestHandler!.documentRoot,
args.wpConfigDefaultConstants
);

await this.runBlueprintV2(args);
}

Expand Down
15 changes: 15 additions & 0 deletions packages/playground/cli/src/run-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,20 @@ export async function parseOptionsAndRunCLI() {
type: 'boolean',
default: false,
})
.option('wp-config-default-constants', {
describe:
'Configure default constant values to use in "wp-config.php", in case the constants are missing. Encoded as JSON string.',
type: 'string',
coerce: (value: string) => {
try {
return JSON.parse(value);
} catch (e /* eslint-disable-line @typescript-eslint/no-unused-vars */) {
throw new Error(
'Invalid JSON string for --wp-config-default-constants'
);
}
},
})
.option('skip-wordpress-setup', {
describe:
'Do not download, unzip, and install WordPress. Useful for mounting a pre-configured WordPress directory at /wordpress.',
Expand Down Expand Up @@ -420,6 +434,7 @@ export interface RunCLIArgs {
xdebug?: boolean;
experimentalDevtools?: boolean;
'experimental-blueprints-v2-runner'?: boolean;
wpConfigDefaultConstants?: Record<string, string | number | boolean | null>;

// --------- Blueprint V1 args -----------
skipWordPressSetup?: boolean;
Expand Down
124 changes: 124 additions & 0 deletions packages/playground/cli/tests/run-cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { exec } from 'node:child_process';
import {
mkdirSync,
readdirSync,
readFileSync,
writeFileSync,
symlinkSync,
unlinkSync,
Expand Down Expand Up @@ -131,6 +132,129 @@ describe.each(blueprintVersions)(
expect(response.text).toContain(oldestSupportedVersion);
});

test('should add missing constants to wp-config.php', async () => {
const tmpDir = await mkdtemp(
path.join(tmpdir(), 'playground-test-')
);

const args: RunCLIArgs = {
...suiteCliArgs,
command: 'server',
'mount-before-install': [
{
hostPath: tmpDir,
vfsPath: '/wordpress',
},
],
mode: 'create-new-site',
};

const newSiteArgs: RunCLIArgs =
version === 2
? {
...args,
'experimental-blueprints-v2-runner': true,
mode: 'create-new-site',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused why this test passes given my comment above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@adamziel Calling ensureWpConfig with a new site (no existing wp-config.php nor wp-config-sample.php) will be a noop, so this passes and creates a new wp-config.php.

}
: args;

const existingSiteArgs: RunCLIArgs =
version === 2
? {
...args,
'experimental-blueprints-v2-runner': true,
mode: 'apply-to-existing-site',
}
: {
...args,
skipWordPressSetup: true,
};

// Create a new site so we can load it as an existing site later.
cliServer = await runCLI(newSiteArgs);
const wpConfigPath = path.join(tmpDir, 'wp-config.php');
let wpConfig = readFileSync(wpConfigPath, 'utf8');
expect(wpConfig).toContain(
"define( 'DB_NAME', 'database_name_here' );"
);
expect(wpConfig).not.toContain(
'BEGIN: Added by WordPress Playground.'
);
expect(wpConfig).not.toContain(
'END: Added by WordPress Playground.'
);

// Remove the "DB_NAME" constant.
writeFileSync(
wpConfigPath,
wpConfig.replace("'DB_NAME'", "'UNKNOWN_CONSTANT'")
);
wpConfig = readFileSync(wpConfigPath, 'utf8');
expect(wpConfig).not.toContain(
"define( 'DB_NAME', 'database_name_here' );"
);

// Use the existing site and confirm the missing constant is added.
cliServer = await runCLI(existingSiteArgs);
wpConfig = readFileSync(wpConfigPath, 'utf8');
expect(wpConfig).toContain(
"define( 'DB_NAME', 'database_name_here' );"
);
expect(wpConfig).toContain('BEGIN: Added by WordPress Playground.');
expect(wpConfig).toContain('END: Added by WordPress Playground.');

// Ensure the "--wp-config-default-constants" argument works as well.
try {
cliServer = await runCLI({
...existingSiteArgs,
wpConfigDefaultConstants: {
DB_NAME: 'test_database_name',
CUSTOM_CONSTANT: 'test_custom_constant',
},
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
// The boot will fail due to incorrect database name,
// but the wp-config.php file should be updated.
}

wpConfig = readFileSync(wpConfigPath, 'utf8');
expect(wpConfig).not.toContain(
"define( 'DB_NAME', 'database_name_here' );"
);
expect(wpConfig).toContain(
"define( 'DB_NAME', 'test_database_name' );"
);
expect(wpConfig).toContain(
"define( 'CUSTOM_CONSTANT', 'test_custom_constant' );"
);
expect(wpConfig).toContain('BEGIN: Added by WordPress Playground.');
expect(wpConfig).toContain('END: Added by WordPress Playground.');

// Ensure the injected constants are removed when no longer needed.
writeFileSync(
wpConfigPath,
wpConfig.replace("'UNKNOWN_CONSTANT'", "'DB_NAME'")
);
await runCLI(existingSiteArgs);
wpConfig = readFileSync(wpConfigPath, 'utf8');
expect(wpConfig).toContain(
"define( 'DB_NAME', 'database_name_here' );"
);
expect(wpConfig).not.toContain(
"define( 'DB_NAME', 'test_database_name' );"
);
expect(wpConfig).not.toContain(
"define( 'CUSTOM_CONSTANT', 'test_custom_constant' );"
);
expect(wpConfig).not.toContain(
'BEGIN: Added by WordPress Playground.'
);
expect(wpConfig).not.toContain(
'END: Added by WordPress Playground.'
);
});

test('should run blueprint', async () => {
cliServer = await runCLI({
...suiteCliArgs,
Expand Down
12 changes: 10 additions & 2 deletions packages/playground/wordpress/src/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
} from '.';
import { basename, dirname, joinPaths } from '@php-wasm/util';
import { logger } from '@php-wasm/logger';
import { ensureWpConfig } from './rewrite-wp-config';
import { ensureWpConfig } from './wp-config';

export type PhpIniOptions = Record<string, string>;
export type Hook = (php: PHP) => void | Promise<void>;
Expand Down Expand Up @@ -126,6 +126,10 @@ export interface BootWordPressOptions {
dataSqlPath?: string;
/** Zip with the WordPress installation to extract in /wordpress. */
wordPressZip?: File | Promise<File> | undefined;
/**
* Default constant values to use in "wp-config.php", in case they are missing.
*/
wpConfigDefaultConstants?: Record<string, string | number | boolean | null>;
/** Preloaded SQLite integration plugin. */
sqliteIntegrationPluginZip?: File | Promise<File>;
/**
Expand Down Expand Up @@ -186,7 +190,11 @@ export async function bootWordPress(
* This is needed, because some WordPress backups and exports may not include
* definitions for some of the necessary constants.
*/
await ensureWpConfig(php, requestHandler.documentRoot);
await ensureWpConfig(
php,
requestHandler.documentRoot,
options.wpConfigDefaultConstants
);
// Run "before database" hooks to mount/copy more files in
if (options.hooks?.beforeDatabaseSetup) {
await options.hooks.beforeDatabaseSetup(php);
Expand Down
2 changes: 1 addition & 1 deletion packages/playground/wordpress/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export {
getFileNotFoundActionForWordPress,
} from './boot';
export type { PhpIniOptions, PHPInstanceCreatedHook } from './boot';
export { defineWpConfigConstants, ensureWpConfig } from './rewrite-wp-config';
export { defineWpConfigConstants, ensureWpConfig } from './wp-config';
export { getLoadedWordPressVersion } from './version-detect';

export * from './version-detect';
Expand Down
Loading