Local Development

As a theme, plugin, or even a core developer, you should be aware of these essential local environment tools:

wp-cli: a command-line tool for performing all kinds of administrative, maintenanance, database and other tasks.

wp-env (version 9.4.0 as of this post): an easy WordPress local environment manager. Uses Docker and node.js. It can be installed and run with practically no configuration, but I’ll show you some of my favorite configs.

Refer to the above linked documentation for installation and basic execution instructions.

Beyond the defaults

Let me describe my particular use case:

  • I am solely interested in maintaining a particular plugin (herein called “Customized”). I will also reference a custom theme (herein called “Local Theme”)
  • I work on multiple projects, so I like to have everything siloed and well-organized
  • Collaboration is key when working with a team, so this setup emphasizes shareability and ease of use.
  • I use a Mac. I can’t guarantee everything here will work on Windows or Linux out of the box.

Configuration starts with .wp-env.json:


{
    "core": "WordPress/WordPress#6.6.1",
    "phpVersion": "8.0",
    "plugins": [
        "./plugins/custom",
        "https://downloads.wordpress.org/plugin/getwid.2.0.3.zip",
        "https://downloads.wordpress.org/plugin/wordpress-seo.22.7.zip",
        "https://downloads.wordpress.org/plugin/wp-gatsby.2.3.3.zip",
        "https://downloads.wordpress.org/plugin/wp-graphql.1.13.7.zip"
    ],
    "mappings": {
        "wp-content/themes/local-theme": "./themes/local-theme",
        "wp-content/startup": "./wp-env-scripts/startup",
        "wp-content/uploads/2024": "./wp-env-scripts/startup/2024",
        "php.ini": "./php/php.ini",                       // php config
        "info.php": "./php/info.php",                     // phpinfo(), can be visited in browser
        "flush.php": "./php/flush.php"                    // flush the cache/permalinks, also visitable
    },
    "config": {
        "WP_DEBUG": true,
        "WP_DEBUG_DISPLAY": true,
        "WP_HOME": "http://127.0.0.1:8888",               // http://localhost is problematic
        "WP_SITEURL": "http://127.0.0.1:8888"
    },
    "lifecycleScripts": {
        "afterStart": "node ./wp-env-scripts/wp-env-afterstart.js"
    }
}

Version control: One significant feature here, is the ability to manage the versions of all core and third-party software. It’s worth pointing out: this is the syntax for specifying a version for third-party plugins available in the plugin marketplace. Find a log of all versions in the “Advanced” section of the plugin info page.

Mappings: Required for making project files available within the Docker environment. Notice I can include initial content in a startup folder that includes some project-managed uploads, plus a startup database.

Config: At this time, it’s necessary to use the local IP address, as http://localhost can be problematic.

This config file instructs wp-env to provision a docker instance using the specked version of PHP. Then, download the specked version of WordPress and install it — by default in your user directory, however I like to use a gitignore’d directory siloed within my project folder (more on this below). Then, retrieve and install the plugins listed (my local plugin is symlinked).

Note: I’ve chosen to map to my local-theme. But it’s also possible to speck "themes" the same way I’ve specked "plugins" above. By using the map, I can edit theme files in my project directory, and see the results live. See the WP documentation for more options.

lifecycleScripts

Using an afterStart script, a lot of setup tasks can be done.


const path = require('path');
const util = require('node:util');
const exec = util.promisify( require('child_process').exec);
const MY_PLUGIN = 'customized';
const BLOG_BASE = 'blog';

const autoActivatePlugins = [
    'getwid',
    'wp-gatsby',
    'wp-graphql',
    'wordpress-seo',
    MY_PLUGIN,
];
const autoActivateTheme = 'local-theme';
const startupDb = 'wp-content/startup/starter.sql';

process.env.WP_ENV_HOME = path.resolve( __dirname, '../_envs');
async function wpEnvAfterStart()
{
    await exec(`wp-env run cli wp theme activate ${autoActivateTheme}`);
    await exec(`wp-env run cli wp db import ${startupDb}`);
    await exec(`wp-env run cli wp plugin activate ${autoActivatePlugins.join(' ')}`);
    await exec(`wp-env run cli --env-cwd=wp-content/plugins/${MY_PLUGIN} composer install`);
    await exec('wp-env run cli wp user create admin admin@dev.null --role=administrator --user_pass=password');
    await exec(`wp-env run cli wp option update category_base '${BLOG_BASE}'`);
    await exec(`wp-env run cli wp rewrite structure '/${BLOG_BASE}/%postname%/'`);
    await exec('wp-env run cli php flush.php');
}
wpEnvAfterStart();

This node script, named wp-env-afterstart.js, and specked in the config json above, automates many tasks.

Lines #1–17 declare the constants, then the wpEnvAfterStart function is invoked.

Notable constants:

  • startupDb: Remember, I mapped a folder startup so I could manage a project startup database with initial posts, authors, settings, etc.
  • WP_ENV_HOME: When this is defined in process.env, then wp-env stores all docker & downloaded files here, instead of your user home directory. I like to organize it this way, so everything related to my project is contained within a single project folder.

wp-cli Tasks:

  • #20: Activate the theme, which is configured to load from my project folder
  • #21: Import the startup database
  • #22: Activate third-party and project plugins
  • #23: Install composer in my project plugin, per the composer config file
  • #24: Create user admin with password password, for convenience. This is in case we decide to pull the production database; it would be terribly insecure if this generic user & password existed there.
  • #25 & #26: We want our blog to exist at /blog, and this sets the permalinks to serve it there.
  • #27: As with any permalinks change, the cache must be flushed, and this runs the php script to do it.

flush.php
In addition to flushing the permalinks cache, this script looks for the .htaccess, and creates it if it doesn’t exist. This file should exist in docker space, since we chose not to map it to a local file. However, we did map flush.php, so it executes in the scope of the docker space. Here is the script:


<?php
require('wp-load.php');
if (!file_exists('.htaccess') ):
    global $wp_rewrite;
    file_put_contents('.htaccess', $wp_rewrite->mod_rewrite_rules() ) ;
    echo 'wrote .htaccess';
    echo PHP_EOL;
endif;
flush_rewrite_rules(true);
echo 'rewrite rules flushed.' . PHP_EOL;

Package scripts

The local development environment is configured. Environment can be spun up, and various actions can be taken, using cli commands and options. By adding certain scripts to package.json, these can actions can be automated. CLI syntax is simply npm run script-name where “script-name” is one of the names below.

Here is the "scripts" section of my package.json:


  "scripts": {
    "wp-env": "WP_ENV_HOME=./_envs wp-env",
    "wp-env:start": "WP_ENV_HOME=./_envs wp-env start --scripts --xdebug=coverage,develop",
    "wp-env:afterstart": "node ./wp-env-scripts/wp-env-afterstart.js",
    "wp-env:stop": "WP_ENV_HOME=./_envs wp-env stop",
    "start:wp": "wp-scripts start --config=./plugins/customized/webpack.config.js --output-path=./plugins/customized/build",
    "build:wp": "wp-scripts build --config=./plugins/customized/webpack.config.js --output-path=./plugins/customized/build",
    "clear-db": "npm run wp-env run cli -- wp db import wp-content/startup/starter.empty.sql && npm run wp-env run cli -- wp user create admin admin@dev.null --role=administrator --user_pass=password"
  },


  • "wp-env": Since I want to scope all actions within my local _envs folder, this adds the appropriate option. By defining this script, I can run all of wp-env‘s commands without having top add the option.
  • "wp-env:start": Start the local development environment. Incidentally, I config certain xdebug options here too.
  • "wp-env:stop": Stops the environment.
  • "wp-env:afterstart": In case something was missed or needs to be re-run (e.g. the flush.php script), this provides a simple command to run all tasks again.
  • "start:wp": Our plugin uses wp-scripts, and this starts the local environment for it. It uses a webpack config file in our plugin directory, and configures the built output.
  • "build:wp": Same as "start:wp", but builds everything for production.
  • "clear-db": A convenient command for clearing the database entirely, leaving only the factory settings, and creating the default admin user.

Useful commands

Here are some notable wp-cli commands, for additional development features:

  • npm run wp-env run cli wp [command]: executes the command, with options
  • npm run wp-env run cli /bin/sh or npm run wp-env run cli /bin/bash: shell CLI within docker space
  • npm run wp-env run cli wp db cli: MySQL CLI interface to the active WP database
  • npm run wp-env run cli wp shell: php CLI with all WordPress native functions enabled