Automating Local WordPress Site Setup with Scripts Part 2: Creating a WP-CLI Package

In my previous article in this series, I discussed some simple ways to make working with local WordPress development environments a bit easier by using shell scripts to automate the more repetitive tasks, such as installing and deleting WordPress sites.

Soon after publishing that post, Daniel Bachuber mentioned that the scripts could have been written up as a WP-CLI command, which led me to realize that there are quite a few good reasons to do so –

  • I’m more comfortable in PHP than I am in Shell
  • The Shell scripts were mostly firing off WP-CLI commands anyway
  • WP-CLI can handle a lot of the leg work regarding parameter validation and documentation
  • WP-CLI commands can be easily shared via the Package Index

In this article, we’ll look at automating things a bit further and converting the scripts to WP-CLI commands. We’ll also look at how we can use the WP-CLI Package Index to make things easier and share our workflows with other developers.

Creating a WP-CLI Command

Let’s get started by creating our very own WP-CLI command. We’ll be able to use this command to quickly install or delete WordPress sites instead of using the old Shell scripts.

To do this, we just need to create a new PHP file with the following contents:

<?php

if ( defined( 'WP_CLI' ) && WP_CLI ) {
    WP_CLI::add_command( 'installer', ‘WP_CLI_Installer’, array( ‘when’ => ‘before_wp_load’ );
}

/**
 * Quickly manage WordPress installations.
 *
 * Usages:
 *
 * wp installer install
 * wp installer uninstall
 *
 */
class WP_CLI_Installer {

    /**
     * Install WordPress Core
     *
     * ## OPTIONS
     *
     * <dest>
     * : The destination for the new WordPress install.
     *
     * [--base_path=<path>]
     * : Base path to install all sites in
     *
     * [--base_url=<url>]
     * : Base URL that sites will be subdirectories of
     *
     * [--multisite]
     * : Convert the install to a Multisite installation
     *
     * [--dbuser=<user>]
     * : Database username
     *
     * [--dbpass=<pass>]
     * : Database password
     *
     * [--dbhost=<host>]
     * : Database host
     *
     * [--admin_user]
     * : Admin username
     *
     * [--admin_password]
     * : Admin password
     *
     * [--admin_email]
     * : Admin email
     */
    public function install( $args, $assoc_args ) {

    }

    /**
     * Uninstall the given WordPress install.
     *
     * ## OPTIONS
     *
     * <dest>
     * : The site that should be uninstalled.
     *
     * [--base_path=<path>]
     * : Base path that all sites are installed in
     */
    public function uninstall( $args, $assoc_args ) {

    }

}

The code above does a few things. First, it checks if WP-CLI is available and adds the installer CLI command if it is. The installer command has two subcommands for now, install and uninstall.

The docblock for each function serves a dual purpose – not only does it tell us at a glance what each function does, but it also exposes information to WP-CLI. If you were to load the file as a plugin and run wp installer at this point, you would see usage information about the wp installer command:

wp-installer

Now let’s start adding some functionality to the subcommands. First we’ll implement the wp installer install subcommand, mainly recreating the functionality of the old install-wp.sh script:

public function install( $args, $assoc_args ) {
    $base_path = isset( $assoc_args['base_path'] ) ? $assoc_args['base_path'] : getcwd();
    $site_path = $base_path . '/' . $args[0];
    $dbuser    = $assoc_args['dbuser'];
    $dbpass    = $assoc_args['dbpass'];
    $dbhost    = $assoc_args['dbhost'];

    // Download WordPress
    $download = "wp core download --path=%s";
    WP_CLI::log( 'Downloading WordPress...' );
    WP_CLI::launch( \WP_CLI\Utils\esc_cmd( $download, $site_path ) );

    // Create the wp-config file
    $config = "wp --path=%s core config --dbname=%s --dbuser=%s --dbpass=%s --dbhost=%s";
    WP_CLI::log( 'Creating wp-config.php...' );
    WP_CLI::launch( \WP_CLI\Utils\esc_cmd( $config, $site_path, $args[0], $dbuser, $dbpass, $dbhost ) );

    // Create the database
    $db_create = "wp --path=%s db create";
    WP_CLI::log( 'Creating the database...' );
    WP_CLI::launch( \WP_CLI\Utils\esc_cmd( $db_create, $site_path ) );

    // Install WordPress core.
    $admin_user  = $assoc_args['admin_user'];
    $admin_pass  = $assoc_args['admin_password'];
    $admin_email = $assoc_args['admin_email'];
    $subcommand  = 'install';
    $base_url    = $assoc_args['base_url'];

    if ( isset( $assoc_args['multisite'] ) ) {
        $subcommand = 'multisite-install';
    }

    $core_install = "wp --path=%s core %s --url=%s --title=%s --admin_user=%s --admin_password=%s --admin_email=%s";
    WP_CLI::log( 'Installing WordPress...' );
    WP_CLI::launch( \WP_CLI\Utils\esc_cmd( $core_install, $site_path, $subcommand, $base_url . $args[0], $args[0], $admin_user, $admin_pass, $admin_email ) );

    WP_CLI::success( "WordPress installed at $site_path" );
}

The WP_CLI::launch() command is used here to launch several existing WP-CLI commands that are responsible for downloading WordPress, creating the wp-config.php file, and creating/installing the database.

We use WP_CLI::launch() here instead of WP_CLI::run_command() because launching the commands as new processes allows us to specify an external path to run the commands. In this case, that path will be the base path that all future sites will be installed under. One popular option is to have a ~/Sites folder that will contain all of the sites that you’ll be working on.

By default, the base path will be set to the current working directory in the terminal, but this can be easily overridden by providing the --base_path argument to the command.

Creating the uninstall subcommand is even easier:

public function uninstall( $args, $assoc_args ) {
    $base_path = isset( $assoc_args['base_path'] ) ? $assoc_args['base_path'] : getcwd();
    $site_path = $base_path . '/' . $args[0];

    // Let's make sure we really want to do this
    if ( ! isset( $assoc_args['yes'] ) ) {
        WP_CLI::confirm( 'Are you sure you want to proceed? Data WILL be lost!', $assoc_args );
    }

    // Drop the database
    $db_drop = "wp --path=%s db drop --yes";
    WP_CLI::log( 'Dropping database...' );
    WP_CLI::launch( \WP_CLI\Utils\esc_cmd( $db_drop, $site_path ) );

    // Remove the files
    $remove_files = "rm -rf %s";
    WP_CLI::log( 'Removing files...' );
    WP_CLI::launch( \WP_CLI\Utils\esc_cmd( $remove_files, $site_path ) );

    WP_CLI::success( "Uninstalled WordPress from $site_path" );
}

To prevent accidental data loss, the WP_CLI::confirm() function is used here to prompt the user to make sure they want to proceed. To support integration with other scripts, this confirmation can be skipped by passing --yes as an argument to the subcommand. After that, the database is dropped using the wp db drop command, and the files are deleted.

Utilizing the WP-CLI Config File

You may have noticed that while running the above subcommands, I didn’t pass in any of the arguments such as the base path or database connection information. While it’s definitely possible to pass these arguments to the command manually every time, another possibility is to store commonly used information in the WP-CLI config file.

The WP-CLI config file is usually located at ~/.wp-cli/config.yml, but this can vary from system to system. To be sure, you can run wp cli info and double check the location of the WP-CLI global config.

Once you’ve found that (or created a new file if necessary), you can edit the file to pass default arguments to each subcommand like so:

# WP Installer default args
installer install:
  base_path: ‘/Users/Example/Sites‘
  base_url: http://localhost/
  dbuser: root
  dbpass: root
  dbhost: 127.0.0.1
  admin_user: test
  admin_password: test
  admin_email: [email protected]
installer uninstall:
  base_path: '/Users/Example/Sites'

Now when you run wp installer install <site name>, any of the default values stored in the WP-CLI config file will be automatically applied as associative arguments, and WordPress should be installed in the desired location.

The WP-CLI Package Index

If you haven’t already, you should definitely take a few minutes to check out the WP-CLI Package Index, a growing collection of 3rd party WP-CLI commands that can be installed in seconds. Any package can be installed by simply running wp package install from your terminal.

Luckily, it’s just as easy to submit our own commands to the index, so anyone else can download and use the wp installer command without installing it as a plugin and running the command from an active WordPress installation.

To submit a package to the index, you should create a repository for your command and push the code up to GitHub. Once you’ve done that, you can fork the WP-CLI Package Index GitHub repository and add your repository to the repositories.txt file. Finally, submit a pull request back to the main repo with your changes:

pr-open

Once that gets merged, you can now run wp package install followed by the name of your package to install it. In this case, running wp package install matt/wp-installer will install the wp installer command:

wp-package-install

Now that it has been installed as a package, the wp installer command can be run from any location in the terminal to quickly spin up or delete a site.

What’s Next

While the WP installer command is still somewhat limited in functionality, it’s incredibly easy to extend it further by adding more WP-CLI subcommands. Another logical step would be to add functional tests and some better error handling to make the code more future proof, and I’m sure I’ll be refactoring as I use the commands more.

Have you started converting your old shell scripts to WP-CLI commands? Do you have any suggestions to improve these commands? Let me know in the comments.

About the Author

Matt Shaw Senior WordPress Developer

Matt is a WordPress plugin developer located near Philadelphia, PA. He loves to create awesome new tools with PHP, JavaScript, and whatever else he happens to get his hands on.