Status: Draft (The final form of this may move to a blog post or a full repo.)
This describes how to set up the repos for WordPress/wordpress-develop, WordPress/gutenberg, and WordPress/performance in one single place all using their own built-in environments (in Docker). Importantly, the latter two repos (for plugins) are configured to use the core files from the repos for both their development and tests environments.
Benefits:
- WordPress core files are shared across all environments, including when running unit tests. So if you run PHPUnit tests for Gutenberg or Performance Lab, any core changes you've made will be reflected. This resolves a big painpoint with trying out core patches in plugin unit tests.
- Plugins, mu-plugins, and themes and are shared across all environments, although you can add additional environment-specific extensions in your
.wp-env.override.json. To add a new plugin or theme, you can just install it in WordPress via any of the environments. Or you can clone a new repo into thethemes/pluginsdirectories. - The same
wp-content/debug.logis shared by all environment (by default). - You can run the tests for each project in their own respective built-in environments.
The repos for gutenberg and performance are cloned into the src/wp-content/plugins directory of the wordpress-develop repo.
Clone repo and enter:
git clone https://github.com/WordPress/wordpress-develop.git
cd wordpress-develop
wordpress_develop_dir=$(pwd)Add remote to my fork, including setting my fork as the origin remote for pushing:
git remote add westonruter https://github.com/westonruter/wordpress-develop.git
git remote set-url --push origin https://github.com/westonruter/wordpress-develop.gitAlso add the SVN repo per svn-git-up.
Install dependencies and build:
npm install
composer install
npm run build:devFrom the wordpress-develop directory:
cd src/wp-content/plugins
plugins_dir=$(pwd)Clone repo and enter:
cd "$plugins_dir"
git clone https://github.com/WordPress/gutenberg.git
cd gutenbergAdd override files that set unique port numbers for this environment while also reusing core files from wordpress-develop.
Note
@wordpress/env deprecated starting both a development and a tests environment by default, along with the env, testsPort, and testsEnvironment options. The tests environment is now a separate config file (.wp-env.test.json) started via wp-env --config. Accordingly, the local overrides are split into two files: .wp-env.override.json (development) and .wp-env.test.override.json (tests). Both must be flat — the env key is ignored.
cat << 'WPENVOVERRIDE' > .wp-env.override.json
{
"$schema": "./schemas/json/wp-env.json",
"core": "../../../",
"port": 8891,
"phpmyadminPort": 9001
}
WPENVOVERRIDE
cat << 'WPENVOVERRIDE' > .wp-env.test.override.json
{
"$schema": "./schemas/json/wp-env.json",
"core": "../../../",
"port": 8991,
"mappings": {
"/wordpress-phpunit": "../../../../tests/phpunit"
}
}
WPENVOVERRIDEInstall dependencies and build:
npm install
composer install
npm run buildClone repo and enter:
cd "$plugins_dir"
git clone https://github.com/WordPress/performance.git
cd performanceAs with Gutenberg, add the split override files that set unique port numbers for this environment while also reusing core files from wordpress-develop. The tests override also adds a mappings entry so the WordPress PHPUnit test suite from wordpress-develop is used:
cat << 'WPENVOVERRIDE' > .wp-env.override.json
{
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
"core": "../../../",
"port": 8890,
"phpVersion": "8.3",
"phpmyadminPort": 9000
}
WPENVOVERRIDE
cat << 'WPENVOVERRIDE' > .wp-env.test.override.json
{
"$schema": "https://schemas.wp.org/trunk/wp-env.json",
"core": "../../../",
"port": 8990,
"phpVersion": "8.3",
"mappings": {
"/wordpress-phpunit": "../../../../tests/phpunit"
}
}
WPENVOVERRIDEInstall dependencies and build:
npm install
composer install
npm run buildAdd symlinks so the Performance Lab plugins are available in wordpress-develop:
cd "$plugins_dir"
for plugin in $(ls performance/plugins); do ln -s "performance/plugins/$plugin"; doneClone these repos into src/wp-content/plugins as desired:
cd "$plugins_dir"
repos=(
od-admin-ui
od-content-visibility
od-default-disabled
od-dev-mode
od-intrinsic-dimensions
od-store-query-vars
od-store-user-agent
)
for repo in "${repos[@]}"; do
git clone https://github.com/westonruter/$repo.git
doneFirst, start up the wordpress-develop environment:
cd "$wordpress_develop_dir"
npm run env:start
npm run env:installThis causes the wp-config.php file to be emitted at the wordpress-develop repo root.
Note that at this point there is no wp-config.php file in the src directory.
This is the tricky part, to get multiple wp-env instances to use the same wp-config.php while also preventing wordpress-develop from using it.
First, start up the Gutenberg environment:
cd "$plugins_dir/gutenberg"
npm run wp-env startExpected output (note that wp-env start now only starts the single development environment; the tests environment is started separately via wp-env --config .wp-env.test.json start, e.g. npm run wp-env-test start):
WordPress development site started at http://localhost:8891
MySQL is listening on port 58145
phpMyAdmin started at http://localhost:9001
At this point you'll be able to successfully navigate to the Gutenberg development environment, but if you try going to the wordpress-development environment, you'll see an error from WordPress:
Warning: mysqli_real_connect(): (HY000/1045): Access denied for user 'example username'@'172.18.0.3' (using password: YES) in /var/www/src/wp-includes/class-wpdb.php on line 1988
This is because when starting wp-env for Gutenberg, it created a wp-config.php in the src directory of wordpress-develop, and WordPress defaults to using the wp-config.php at its root directory before looking one directory above, which is what wordpress-develop normally does. The wp-config.php generated by wp-env comes from wp-config-docker.php in docker-library/wordpress, and then wp-env uses WP-CLI to set the WP constants to be whatever is located in the .wp-env.json/.wp-env.override.json. For example:
wp-config.php
<?php
/**
* The base configuration for WordPress
*
* The wp-config.php creation script uses this file during the installation.
* You don't have to use the website, you can copy this file to "wp-config.php"
* and fill in the values.
*
* This file contains the following configurations:
*
* * Database settings
* * Secret keys
* * Database table prefix
* * ABSPATH
*
* This has been slightly modified (to read environment variables) for use in Docker.
*
* @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/
*
* @package WordPress
*/
// IMPORTANT: this file needs to stay in-sync with https://github.com/WordPress/WordPress/blob/master/wp-config-sample.php
// (it gets parsed by the upstream wizard in https://github.com/WordPress/WordPress/blob/f27cb65e1ef25d11b535695a660e7282b98eb742/wp-admin/setup-config.php#L356-L392)
// a helper function to lookup "env_FILE", "env", then fallback
if (!function_exists('getenv_docker')) {
// https://github.com/docker-library/wordpress/issues/588 (WP-CLI will load this file 2x)
function getenv_docker($env, $default) {
if ($fileEnv = getenv($env . '_FILE')) {
return rtrim(file_get_contents($fileEnv), "\r\n");
}
else if (($val = getenv($env)) !== false) {
return $val;
}
else {
return $default;
}
}
}
// ** Database settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define( 'DB_NAME', getenv_docker('WORDPRESS_DB_NAME', 'wordpress') );
/** Database username */
define( 'DB_USER', getenv_docker('WORDPRESS_DB_USER', 'example username') );
/** Database password */
define( 'DB_PASSWORD', getenv_docker('WORDPRESS_DB_PASSWORD', 'example password') );
/**
* Docker image fallback values above are sourced from the official WordPress installation wizard:
* https://github.com/WordPress/WordPress/blob/1356f6537220ffdc32b9dad2a6cdbe2d010b7a88/wp-admin/setup-config.php#L224-L238
* (However, using "example username" and "example password" in your database is strongly discouraged. Please use strong, random credentials!)
*/
/** Database hostname */
define( 'DB_HOST', getenv_docker('WORDPRESS_DB_HOST', 'mysql') );
/** Database charset to use in creating database tables. */
define( 'DB_CHARSET', getenv_docker('WORDPRESS_DB_CHARSET', 'utf8') );
/** The database collate type. Don't change this if in doubt. */
define( 'DB_COLLATE', getenv_docker('WORDPRESS_DB_COLLATE', '') );
/**#@+
* Authentication unique keys and salts.
*
* Change these to different unique phrases! You can generate these using
* the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}.
*
* You can change these at any point in time to invalidate all existing cookies.
* This will force all users to have to log in again.
*
* @since 2.6.0
*/
define( 'AUTH_KEY', getenv_docker('WORDPRESS_AUTH_KEY', 'de8724d4375446bc55cab44d0e30dd5fd889c3fd') );
define( 'SECURE_AUTH_KEY', getenv_docker('WORDPRESS_SECURE_AUTH_KEY', '26a684919680c6f85faa6e665a382fc653da2032') );
define( 'LOGGED_IN_KEY', getenv_docker('WORDPRESS_LOGGED_IN_KEY', 'e039bb46676f860f8ba591250c3d1f0ff0afa405') );
define( 'NONCE_KEY', getenv_docker('WORDPRESS_NONCE_KEY', '8095fdb8ffd5974f45018fae0833f19fa67f21ce') );
define( 'AUTH_SALT', getenv_docker('WORDPRESS_AUTH_SALT', '25587d086964742511fff7f9cf9c75fcf5c71bff') );
define( 'SECURE_AUTH_SALT', getenv_docker('WORDPRESS_SECURE_AUTH_SALT', 'b8b12ced9f4c27288ef2db5d43e489dd9dea1a41') );
define( 'LOGGED_IN_SALT', getenv_docker('WORDPRESS_LOGGED_IN_SALT', '87ecae825ca29c34d93b9517597aa8c9b67cfb7b') );
define( 'NONCE_SALT', getenv_docker('WORDPRESS_NONCE_SALT', '3d450ea8b90a58a3ba04aff962cde33e326ceadd') );
// (See also https://wordpress.stackexchange.com/a/152905/199287)
/**#@-*/
/**
* WordPress database table prefix.
*
* You can have multiple installations in one database if you give each
* a unique prefix. Only numbers, letters, and underscores please!
*
* At the installation time, database tables are created with the specified prefix.
* Changing this value after WordPress is installed will make your site think
* it has not been installed.
*
* @link https://developer.wordpress.org/advanced-administration/wordpress/wp-config/#table-prefix
*/
$table_prefix = getenv_docker('WORDPRESS_TABLE_PREFIX', 'wp_');
/**
* For developers: WordPress debugging mode.
*
* Change this to true to enable the display of notices during development.
* It is strongly recommended that plugin and theme developers use WP_DEBUG
* in their development environments.
*
* For information on other constants that can be used for debugging,
* visit the documentation.
*
* @link https://developer.wordpress.org/advanced-administration/debug/debug-wordpress/
*/
define( 'FS_METHOD', 'direct' );
define( 'SCRIPT_DEBUG', true );
define( 'WP_ENVIRONMENT_TYPE', 'local' );
define( 'WP_PHP_BINARY', 'php' );
define( 'WP_TESTS_EMAIL', 'admin@example.org' );
define( 'WP_TESTS_TITLE', 'Test Blog' );
define( 'WP_TESTS_DOMAIN', 'localhost:8891' );
define( 'WP_SITEURL', 'http://localhost:8891' );
define( 'WP_HOME', 'http://localhost:8891' );
define( 'WP_DEBUG', true );
/* Add any custom values between this line and the "stop editing" line. */
// If we're behind a proxy server and using HTTPS, we need to alert WordPress of that fact
// see also https://wordpress.org/support/article/administration-over-ssl/#using-a-reverse-proxy
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false) {
$_SERVER['HTTPS'] = 'on';
}
// (we include this by default because reverse proxying is extremely common in container environments)
if ($configExtra = getenv_docker('WORDPRESS_CONFIG_EXTRA', '')) {
eval($configExtra);
}
/* That's all, stop editing! Happy publishing. */
/** Absolute path to the WordPress directory. */
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}
/** Sets up WordPress vars and included files. */
require_once ABSPATH . 'wp-settings.php';Most notably:
define( 'WP_TESTS_DOMAIN', 'localhost:8891' );
define( 'WP_SITEURL', 'http://localhost:8891' );
define( 'WP_HOME', 'http://localhost:8891' );If the Gutenberg environment is currently running (in which the .wp-env.override.json has a port set to 8891), and then I now try starting the Performance Lab environment (in which the .wp-env.override.json has a port set to 8890), then wp-env updates the constants used in the wp-config.php created by wp-env in Gutenberg to use the values for the Performance Lab environment:
define( 'WP_TESTS_DOMAIN', 'localhost:8890' );
define( 'WP_SITEURL', 'http://localhost:8890' );
define( 'WP_HOME', 'http://localhost:8890' );The result is attempting to access the Gutenberg environment (http://localhost:8891) will cause WordPress to do a canonical redirection (with 301 Moved Permanently) to the Performance Lab environment (http://localhost:8890), meaning you can't access both environments at the same time (besides the fact that the wordpress-develop environment is also broken right now).
So here is the solution I've come up with to ensure that these three environments are all able to run concurrently.
1️⃣ First, open the src/wp-config.php file, and add the following PHP code to the top of after the file header comment:
// Load the wp-config.php for wordpress-develop when the current request is coming from that environment.
if ( basename( __DIR__ ) === 'src' && file_exists( __DIR__ . '/../wp-config.php' ) ) {
require __DIR__ . '/../wp-config.php';
return;
}At this point, the wordpress-develop environment (http://localhost:8889/) works again! 🎉
2️⃣ Second, we need to dynamically load the correct config constants for the current wp-env environment (e.g. Gutenberg or Performance Lab). This can be done be adding the following code right after the the if statement added above.
There are two ways the correct environment is determined:
- Web requests have
$_SERVER['SERVER_PORT']set, so the environment is matched by port. Each plugin's config is now split across two files:.wp-env.json/.wp-env.override.json(development) and.wp-env.test.json/.wp-env.test.override.json(tests). Each pair is evaluated independently so the development and tests environments for a plugin can match their respective ports. - WP-CLI/PHPUnit runs (e.g.
npm run test-php,npm run test:unit:php) have noSERVER_PORT. Instead the running plugin is identified by the working directory (set viawp-env run --env-cwd=…), walking up to find its.wp-env.test.jsonand applying the tests config. This is what keepsWP_DEBUG/SCRIPT_DEBUGfalseduring tests so that minified assets are enqueued and markup snapshot assertions pass. (__DIR__can't be used here, because this file is also copied towp-tests-config.phpin a different directory.)
( function () {
$host = 'localhost';
if ( isset( $_SERVER['SERVER_PORT'] ) ) {
$host .= ':' . $_SERVER['SERVER_PORT'];
}
$env_config = array(
'FS_METHOD' => 'direct',
'SCRIPT_DEBUG' => true,
'WP_DEBUG' => true,
'WP_DEBUG_LOG' => true,
'WP_DEVELOPMENT_MODE' => 'all',
'WP_ENVIRONMENT_TYPE' => 'local',
'WP_HOME' => 'http://' . $host,
'WP_PHP_BINARY' => 'php',
'WP_SITEURL' => 'http://' . $host,
'WP_TESTS_DOMAIN' => $host,
'WP_TESTS_EMAIL' => 'admin@example.org',
'WP_TESTS_TITLE' => 'Test Blog',
);
if ( isset( $_SERVER['SERVER_PORT'] ) ) {
$matched = false;
foreach ( glob( __DIR__ . '/wp-content/plugins/*' ) as $plugin_dir ) {
// Each wp-env config has its own (base, override) pair and is
// evaluated independently: `.wp-env.json` is the development
// environment, while `.wp-env.test.json` (started via
// `wp-env --config`) is the separate tests environment.
$variants = array(
array( '/.wp-env.json', '/.wp-env.override.json' ),
array( '/.wp-env.test.json', '/.wp-env.test.override.json' ),
);
foreach ( $variants as $variant ) {
$env_port = null;
$plugin_config = array();
foreach ( $variant as $config_file ) {
if ( ! file_exists( $plugin_dir . $config_file ) ) {
continue;
}
$plugin_env = json_decode( file_get_contents( $plugin_dir . $config_file ), true );
if ( isset( $plugin_env['env']['development']['port'] ) ) {
$env_port = $plugin_env['env']['development']['port'];
} elseif ( isset( $plugin_env['port'] ) ) {
$env_port = $plugin_env['port'];
}
if ( isset( $plugin_env['config'] ) ) {
$plugin_config = array_merge( $plugin_config, $plugin_env['config'] );
}
if ( isset( $plugin_env['env']['development']['config'] ) ) {
$plugin_config = array_merge( $plugin_config, $plugin_env['env']['development']['config'] );
}
}
if ( $env_port && $env_port === (int) $_SERVER['SERVER_PORT'] ) {
$env_config = array_merge( $env_config, $plugin_config );
$matched = true;
break;
}
}
if ( $matched ) {
break;
}
}
} elseif (
isset( $_SERVER['argv'][0] ) &&
str_contains( $_SERVER['argv'][0], 'phpunit' )
) {
// WP-CLI/PHPUnit requests have no SERVER_PORT, so the port-based lookup
// above can't run. Identify which monorepo's suite is executing by the
// working directory (set via `wp-env run --env-cwd=...`) and apply that
// monorepo's tests environment config (`.wp-env.test.json`). This keeps
// constants such as WP_DEBUG and SCRIPT_DEBUG aligned with the tests
// environment (rather than the development defaults, which would enqueue
// unminified assets and break markup snapshots) for any mapped plugin,
// e.g. both performance and gutenberg. Note __DIR__ cannot be used to
// locate the plugins here: this file is also copied to
// wp-tests-config.php in a different directory, so walk up from the
// working directory instead.
$dir = (string) getcwd();
while ( '' !== $dir && '/' !== $dir ) {
if ( file_exists( $dir . '/.wp-env.test.json' ) ) {
foreach ( array( '/.wp-env.test.json', '/.wp-env.test.override.json' ) as $config_file ) {
if ( ! file_exists( $dir . $config_file ) ) {
continue;
}
$plugin_env = json_decode( file_get_contents( $dir . $config_file ), true );
if ( isset( $plugin_env['config'] ) ) {
$env_config = array_merge( $env_config, $plugin_env['config'] );
}
}
break;
}
$dir = dirname( $dir );
}
}
foreach ( $env_config as $key => $value ) {
// Guard against re-defining: this file (wp-config.php, and its
// wp-tests-config.php copy) can be loaded more than once in a single
// process (e.g. WP-CLI loads it twice), which would otherwise emit
// "Constant already defined" warnings on the second pass.
if ( ! defined( $key ) ) {
define( $key, $value );
}
}
} )();3️⃣ In order to prevent PHP from warning about the config constants already being defined, you can wrap the original constants in if ( false ) {:
+ if ( false ) {
define( 'FS_METHOD', 'direct' );
define( 'SCRIPT_DEBUG', true );
define( 'WP_ENVIRONMENT_TYPE', 'local' );
define( 'WP_PHP_BINARY', 'php' );
define( 'WP_TESTS_EMAIL', 'admin@example.org' );
define( 'WP_TESTS_TITLE', 'Test Blog' );
define( 'WP_TESTS_DOMAIN', 'localhost:8890' );
define( 'WP_SITEURL', 'http://localhost:8890' );
define( 'WP_HOME', 'http://localhost:8890' );
define( 'WP_DEVELOPMENT_MODE', 'plugin' );
define( 'WP_DEBUG', true );
+ }This is because wp-env will continue use WP-CLI to add/update/remove these constants. So this hack ensures that the wp config commands will succeed, by ultimately be ingored in favor of the logic added to dynamically read them from the .wp-env.json/.wp-env.override.json files.
Each plugin now has two independent environments (development and tests), so start both. The wp-env-test script is shorthand for wp-env --config=.wp-env.test.json.
cd "$wordpress_develop_dir"
npm run env:start
cd "$plugins_dir/gutenberg"
npm run wp-env start # development (http://localhost:8891)
npm run wp-env-test start # tests (http://localhost:8991)
cd "$plugins_dir/performance"
npm run wp-env start # development (http://localhost:8890)
npm run wp-env-test start # tests (http://localhost:8990)The plugin PHP/E2E tests run against the tests environment, so make sure it is running first (see above). Note that after editing src/wp-config.php, run npm run wp-env-test start -- --update in the plugin so the change is copied into the regenerated wp-tests-config.php.
cd "$wordpress_develop_dir"
npm run test:php
npm run test:e2e
npm run test:visual
cd "$plugins_dir/gutenberg"
npm run test:unit:php # Starts the tests env (setup) then runs PHPUnit.
npm run test:unit
npm run test:e2e
cd "$plugins_dir/performance"
npm run wp-env-test start # Ensure the tests env is running.
npm run test-php
npm run test-e2eYou can continue to have separate PhpStorm projects for all three repos.
TODO: Pointing to core files when opening Gutenberg or Performance Lab projects.
- WordPress/wordpress-develop#8931
- WordPress/gutenberg#70280
- WordPress/performance#2505 (migrate off the deprecated wp-env tests environment to a separate
.wp-env.test.jsonconfig)