Automating repetitive tasks is one of the best ways to save time in your development workflow. In my day to day work as a plugin developer, I often have to reset the database, reinstall a specific version of WordPress, install one or more plugins, and update the settings. This quickly gets tiresome, so it’s a natural fit for automation. In this article, I’ll show you how I use WP-CLI in combination with Robo to automate the tasks needed to manage my WordPress development environment.
The WordPress command line tool, WP-CLI, is a great start to speeding up your workflow. If you’re not familiar with it, I highly recommend reading one of our many blog posts. Our installation guide is a great starting point, laying out how to install it on your OS, set up tab completion, and create a configuration file.
Table of Contents
- What is Robo?
- Maintaining WordPress With Robo
- Leveraging wp-cli.yml
- Creating Custom Robo Commands
- Wrapping Up
What is Robo?
Robo is an open source modern task runner used by a number of projects including Drush and Codeception.
Robo is similar to Gulp and Grunt, but using PHP rather than JavaScript.
The idea is that you can create nifty little commands like this:
# Reset my WP dev environment and make it multisite robo reset --multi # Install and setup the WP Offload Media plugin robo profile ome-dev
The Robo task runner makes it easy to document the commands you create by adding DocBlock comments to your commands, so you can provide help for future versions of yourself:
# Robo without any arguments will show the available commands. robo Available commands: help Displays help for a command list Lists commands profile Run a set of wp-cli commands on an existing WordPress environment reset Reset the WordPress environment to a known state
Asking for more info about a specific command:
robo help reset Description: Reset the WordPress environment to a known state. Reads environments and configuration from robo.yml Usage: reset [options] Options: --env[=ENV] Environment (dev, test etc) [default: "dev"] --multi Make a multi site install --ver[=VER] WordPress version [default: "latest"]
I’ve created some powerful commands with Robo that I use every day to manage my WordPress development environment. In this article, I’m going to share these commands with you and provide an introduction to Robo commands along the way.
Robo Simplifies Command Line PHP
Let’s check the rear view mirror just a bit. Writing command-line PHP commands is certainly not new. It’s always been possible to just run a PHP script from the command line like this:
php myscript.php
And PHP has been available as a command line environment in most *NIX environments for as long as I can remember. Adding the PHP shebang will work in most environments that have the PHP interpreter installed.
// file: myscript
#!/usr/bin/env php
<?php
// do stuff here…
Which makes it possible to execute the script from the command line without specifying that it should be parsed by PHP (or including the .php file extension):
myscript
The downside of running raw PHP scripts from the command line is that there’s quite a lot of overhead for input and output to take care of in each script. The process of accepting arguments from the command line, and then outputting the messages to the command line is a bit cumbersome and doesn’t feel very flexible.
Robo sets out to make it easier to write command line PHP by taking care of a lot of the standard “plumbing” that most scripts need. This lets you focus on the core functionality of your scripts.
Installing Robo
You’ll need to install Robo before we can get started. I keep Robo installed globally on my machine, so I’ve installed it like this:
composer global require consolidation/robo
But as with anything installed via Composer, you can keep it as a project dependency if you’re more comfortable with that. Alternatively, the GitHub has instructions on installing it by downloading robo.phar
.
Your First Command
First, we need to initialize Robo in the project folder:
cd /path/to/myproject robo init
This really just creates a new RoboFile.php
in your folder with the following content:
<?php
class RoboFile extends \Robo\Tasks
{
}
To add our first command, we just add a public method:
<?php
class RoboFile extends \Robo\Tasks
{
public function hello($world)
{
$this->say("Hello, $world");
}
}
As you can probably guess, the above method creates the command hello
and simply outputs a message on the screen.
Parsing Arguments
Just adding that method like we did above is a great way to show one of the more important reasons why I like Robo, namely parsing command line arguments.
To show you what I mean, let’s try to run this command:
robo hello Not enough arguments (missing: "world"). hello [-h|--help] [-q|--quiet] [-v|vv|vvv|--verbose] [-V|--version] [--ansi] [--no-ansi] [-n|--no -interaction] [--simulate] [--progress-delay PROGRESS-DELAY] [-D|--define DEFINE] [--]
Aha! Robo gives me an error message because the hello
command actually expects a $world
parameter and then proceeds to write out the full usage syntax for the command.
Let’s change the method a bit and make the parameter optional:
public function hello($world = ‘from Robo’)
{
$this->say("Hello, $world");
}
…and now let’s run it again:
# Without arguments robo hello Hello, from Robo # With a simple argument robo hello there! Hello, there! # With an arg containing spaces robo hello “I live on the command line” Hello, I live on the command line
That’s better! By making the parameter optional, Robo now happily executes our method even without passing an argument. We’ve also seen that we can pass a simple argument and an argument within quotes and it just works.
If you ever spent any time writing argument checking logic for a command line script you probably realize why this is a nice feature. It just takes a lot of pain away.
The example hello
command uses one single positional argument. Robo also supports using flags and named arguments by creating an array argument with some default values.
Let’s modify the function further to optionally print out some additional characters:
public function hello( $world = 'from Robo', $flags = [ 'stars' => false, 'stripes' => false ] ) {
if ( $flags['stars'] ) $this->say( '***************' );
if ( $flags['stripes'] ) $this->say( '===============' );
$this->say( "Hello, $world" );
if ( $flags['stripes'] ) $this->say( '===============');
if ( $flags['stars'] ) $this->say( '***************' );
}
This will tell Robo that we may optionally pass in named arguments using double dashes:
# Just by including a named argument, it will get the value ‘true’ robo hello --stars *************** Hello, from Robo ***************
Input and Output
We’ve also already seen an example using IO. The say()
function of the Robo task object simply outputs a string back to the user. There is also an ask()
function that lets you ask the user for input:
public function hello() {
$word = $this->ask("Tell me what to say:");
$this->say( $word );
}
robo hello ? Tell me what to say: foobar foobar
Robo uses Symfony Console for creating user interactions. This means that besides the two simple say()
and ask()
functions, we have access to any function from Symfony Console, like table()
:
public function table() {
$this->io()->table(
['Header 1', 'Header 2'],
[
['Cell 1-1', 'Cell 1-2'],
['Cell 2-1', 'Cell 2-2'],
['Cell 3-1', 'Cell 3-2'],
]
);
}
robo table ---------- ---------- Header 1 Header 2 ---------- ---------- Cell 1-1 Cell 1-2 Cell 2-1 Cell 2-2 Cell 3-1 Cell 3-2 ---------- ----------
Pretty cool, eh?
Great Helpers
Another reason to like Robo is that it has built-in support for many common tasks that makes it easier to write understandable code, even if you’re coming back to it 3 months later. Let’s have a look at how Robo helps with writing very clean code for some pretty standard tasks:
# Create a directory, and switch to it:
$this->taskExecStack()
->stopOnFail()
->exec('mkdir site')
->exec('cd site')
->run();
# Search and replace inside a text file:
$this->taskReplaceInFile('VERSION')
->from('0.2.0')
->to('0.3.0')
->run();
# Run composer update:
$this->taskComposerUpdate()->run();
# SSH into a server, go to a specific directory, list the contents of the directory,
and set permissions on the logs subdirectory
$this->taskSshExec('remote.example.com', 'user')
->remoteDir('/var/www/html')
->exec('ls -la')
->exec('chmod g+x logs')
->run();
All of the above is possible using normal PHP and the exec()
function, but that quite often becomes a hopeless exercise of trying to glue command strings together and correctly escaping arguments without messing things up too badly.
Besides the examples above, there is also similar support for Git, Subversion, Rsync, Bower, Gulp, Docker, NPM, and other tools often used in development environments.
Benefits of Robo
Taken together, Robo makes creating command line scripts a lot easier and the end result is usually a lot more semantically appealing than plain PHP.
I find that scripts for Robo end up being easier to read and understand compared with pure PHP scripts, because so much of the command line boilerplate stuff is hidden inside Robo itself. Personally, I can look at my own code after a few months and honestly wonder who wrote it, so anything that helps me write clear, readable code is a welcome addition in my tool belt.
Maintaining WordPress With Robo
The rest of this article assumes that you are somewhat familiar with WP-CLI, using Composer, and working with the command line.
The Important Files
There are four important files in this setup. I will cover each one of them throughout this post:
- wp-cli.yml – The standard WP-CLI configuration file. I’m using this to leverage several of the great built-in features of WP-CLI when it comes to managing multiple environments.
- RoboFile.php – This is the base file for implementing robo commands.
- robo.yml – A custom YAML file for some additional configuration parameters for our Robo commands.
- composer.json – The standard Composer config file.
My Environment
Just to set the stage for the rest of this article, I’m going to quickly describe how my local development environment is set up. This is a simplified version of how I’ve organized things on disk:
~/src ├── devenv/ │ ├── RoboFile.php │ ├── composer.json │ ├── wp-cli.yml │ ├── robo.yml │ ├── wordpress-dev/ │ └── wordpress-test/ ├── plugin1/ │ ├── assets/ │ └── include/ └── plugin2 └── ...
The src
folder is where everything development related is stored and the devenv
folder holds files specific to the actual runtime environment. Since I usually have more than one WordPress environment running at the same time, each WordPress installation has its own sub folder, named wordpress-dev
and wordpress-test
in this example.
Each of the plugins I work on sits in a separate folder per plugin.
In the real world, I have multiple devenv folders so that I can keep my work for Delicious Brains separate from various side projects, but that’s not relevant for this article.
Database User
To make everything work I’ve also created local databases for each of the WordPress installations in my local MySQL installation and there’s a database user, aptly named wordpress
, with the correct access to those databases. The exact details of the databases as well as the user credentials are stored in the wp-cli.yml
file.
Web Server
I’m using nginx as my local web server, but you will find that Apache2 works just as well. My nginx config is set up so that http://www.wordpress-dev.local
and http://www.wordpress-test.local
point to the two WordPress folders mentioned above.
Taking Care of Dependencies
To make my Robo scripts more flexible, I’m using some additional functionality installed via Composer, specifically the Symfony Yaml parser. Since RoboFile.php
is really just a normal PHP file, I’m free to include any libraries I want and I naturally use Composer to do that. The composer.json
file for this project looks like this:
{
"require": {
"symfony/yaml": "^5.2"
}
}
If you copy that, don’t forget to actually install the library using composer update
.
Leveraging wp-cli.yml
When working with WP-CLI, you can make life a lot simpler by using a configuration file such as wp-cli.yml
. I use the WP-CLI configuration file for two main reasons: aliases and setting up defaults for various subcommands.
What Is an Alias in WP-CLI?
At its core, a WP-CLI alias is just a label in the config file that lets you override some of the defaults.
The most common usage of aliases is probably to override the default path
so that each alias points to a separate WordPress installation. Since each WordPress installation keeps its own configuration file with database credentials, the alias used this way also represents a separate database. An alias in the WP-CLI configuration file can override the url
, path
, user
, ssh
, and http
settings, but it can’t override default values for subcommands.
Creating an additional WordPress environment named @test
allows me to run WP-CLI commands like this:
# List all plugins in the dev WordPress installation wp plugin list # List all plugins in the test WordPress installation wp @test plugin list
Command Defaults
If you haven’t tried this before, setting up default parameters for subcommands is super handy. For instance, when you create a new WordPress configuration file using the config create
command, you need to specify at least three parameters every time:
$ wp config create --dbname=somedb --dbuser=myuser --dbpass=secret
If you ever get tired of typing this, you can stick the parameters into a wp-cli.yml
file:
config create:
dbuser: myuser
dbpass: secret
dbname: somedb
Once you’ve done that, you can just use wp config create
and it will pick up the right parameters from your wp-cli.yml
file.
Unfortunately it’s not possible to set up different command defaults for different aliases. This was actually one of the reasons I started looking at Robo for more automation.
My WP-CLI config file looks like this:
# Global parameter defaults
path: wordpress-dev
url: http://www.wordpress-dev.local
user: admin
@test:
path: wordpress-test
url: www.wordpress-test.local
# Subcommand defaults
config create:
dbuser: wordpress
dbpass: *****
dbname: wordpress
extra-php: |
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true);
define( 'SCRIPT_DEBUG', true );
core install:
admin_user: admin
admin_password: admin
admin_email: [email protected]
title: WordPress Dev
core multisite-install:
admin_user: admin
admin_password: admin
admin_email: [email protected]
With just this configuration file in place, I’m able to run common commands without having to specify the individual parameters every time:
# Reset the database on the test environment wp @test db reset --yes # Download the latest version and create the wp-config.php file wp @test core download wp @test config create # Install WordPress wp @test core install --title="WordPress Test"
As WP-CLI picks up most of the parameters from the configuration file, I don’t have to type out all the command line parameters like --dbuser
and --admin_email
that I would normally have to.
Note that in the last example above I provided the title
parameter separately. This is because I want the site title to be different in the test environment, but it’s not possible to override this parameter using an alias.
Creating Custom Robo Commands
Setting up a fresh WordPress installation is almost never enough. Usually there are one or more plugins that need to be installed and activated and quite often a few settings to fix up here and there.
Even with a carefully written WP-CLI configuration file, I’d still end up with a long string of commands if I want to reset my WordPress environment and get everything ready. I often did sequences like this over and over again:
# Reset my dev environment wp db reset --yes rm -rf path/to/wordpress wp core download wp config create wp core install ln -s path/to/my/plugin1 path/to/wordpress/wp-content/plugins/ wp plugin activate plugin1
Even when taking full advantage of the WP-CLI config file, this is a lot of typing. To avoid typing this over and over again and also getting it wrong every now and then, I created two specialized commands using Robo to do it for me:
- reset – Resets a WordPress environment to a known state.
- profile – Runs a set of commands on an existing environment.
Since the WP-CLI subcommand default parameters don’t extend to different command-line environments, we can use Robo to kill that bird with the same stone. Let’s see how.
Command Configuration
Our first stop is to have a look at the Robo configuration file I’ve created for this. It’s written in YAML, which should make it fairly straightforward to understand and extend. I’ll go over each section as we get to the command that uses it:
wordpress-dev:
cli-prefix: ""
path: "wordpress"
core-multisite-install:
title: WordPress Multisite
post-install:
- ln -s $cwd/../plugins1 $path/wp-content/plugins/
- ln -s $cwd/../plugins2 $path/wp-content/plugins/
wordpress-test:
cli-prefix: "@test"
path: "wordpress-test"
config-create:
dbname: wordpress-test
core-install:
title: WordPress Test
core-multisite-install:
title: WordPress Test Multisite
post-install:
- ln -s $cwd/../plugins1 $path/wp-content/plugins/
profiles:
woocommerce:
- $wp plugin install --activate woocommerce
- $wp wc payment_gateway update cheque --enabled=true --user=admin
- $wp option update woocommerce_calc_taxes yes
- $wp wc tax create --name=VAT --rate=10 --user=admin
- $wp wc shipping_zone_method create 0 --method_id=flat_rate --user=admin
- $wp option update --format=json woocommerce_flat_rate_1_settings '{"title":"Flat rate","tax_status":"taxable","cost":"15"}'
imageimport:
- $wp media import $cwd/../media/lots-of-images/*
issue2530:
- $wp plugin install --activate some_plugin
- $wp config set FOOBAR true --raw
Each WordPress environment is identified using a wordpress-$env
key. Each key can hold several configuration values.
The reset
Command
The first command is reset
. It’s used from the command line like this:
# Reset the dev (default) environment robo reset # Or being more explicit robo reset –env=dev # Reset the test environment robo reset –env=test # Reset the dev environment and install a WordPress multisite robo reset --multi # Reset the dev environment to a specific WordPress version robo reset --ver=5.6.1
The first thing this command does is to delete all existing files and folders in the target WordPress installation directory and reset the configured WordPress database.
Then, the reset
command uses the WP-CLI commands core download
, config create
and one of core install
or core multisite-install
depending on the --multi
option.
To the largest extent possible, this uses the command parameter defaults located in the wp-cli.yml
file. The reason for this is that those defaults are also helpful when running WP-CLI directly without the Robo wrapper. But as discussed above, in some cases it’s just not possible.
Therefore the robo.yml
config file offers the possibility to specify overrides for the defaults in wp-cli.yml.
For example, when installing the @test environment we want to override the parameters for the core install
parameter --title
. We can do that by adding the following in robo.yml
:
wordpress-test:
...
...
core-install:
title: WordPress Test
...
...
The syntax here is quite easy: the CLI command name (spaces replaced with dashes) as the key and one subkey for each named parameter. The above example would generate the following extra parameter to the core install
cli command:
wp @test core install --title="WordPress Test"
Post-Install Steps
Each environment key in the config file can optionally specify an array with post-install steps. This is simply a list of bash commands that are executed when the WordPress installation is done. For added flexibility, a few string replacements are made before the command is executed:
String | Replaced with |
---|---|
$cwd | The current working directory |
$path | The path of the target WordPress installation |
$wp | The wp-cli command, including the alias prefix, i.e., wp @test |
~ | (tilde symbol) The HOME directory of the current user |
So a post-install step of ln -s $cwd/../plugin1 $path/wp-content/plugins/
would create a symlink from one of my plugins folders to the plugins subfolder in the target WordPress install.
The profile
Command
The profile
command is quite similar to the post-install steps, but its intended use is to run a set of commands on an existing WordPress installation. Let’s say that you have a very plain development environment that you do most of your work in. However, sometimes you need to install the WooCommerce plugin and do some basic setup for it. This is what the profile
command is for. It can be used like this:
# Reset the dev environment robo reset # Install WooCommerce and make some setup changes robo profile woocommerce
The sample robo.yml
file above has a WooCommerce profile. Applying that profile will:
- Install and activate WooCommerce using WP-CLI.
- Use the
wc
sub command to set up a payment gateway, tax zone, and shipping settings. - Use the
option
sub command to modify some settings directly in the WordPress options table.
Using different profiles is quite useful. I spend most of my days working on the WP Offload Media plugin and I often need to import lots of images to the WordPress media library. WP-CLI has a really convenient command for this:
wp import media /some/long/path/I/often/forget/*
Since I often forget a lot of stuff in general and long path names in particular, I find it easier to remember:
robo profile imageimport
Here’s another example. I’ve been working on a particular GitHub issue where we’re trying to fix a compatibility issue between our plugin and another popular WordPress plugin. When I work on that issue, I need to install that plugin and set a config value in wp-config.php.
So I created a profile for it:
....
issue2530:
- $wp plugin install --activate some_plugin
- $wp config set FOOBAR value
Now I can get my environment ready to go in one step, just robo profile issue2530
.
Just like the post-install
steps in the reset command, each row in the profiles definition is really just a separate bash command. You could use it to launch separate scripts, delete files, or whatever you fancy. It’s also quite possible to shoot yourself in the foot, so tread carefully.
The Source
If any of the above sounds interesting to try out, here’s the RoboFile that I’m using for all of the above stuff, feel free to use it to get started with managing WordPress using Robo.
<?php
use Robo\Symfony\ConsoleIO;
use Robo\Tasks;
use Symfony\Component\Yaml\Yaml;
require_once 'vendor/autoload.php';
/**
* Class RoboFile
*/
class RoboFile extends Tasks {
/**
* Reset the WordPress environment to a known state. Reads environments
* and configuration from robo.yml
*
* @option env Environment (dev, test etc)
* @option multi Make a multi site install
* @option ver WordPress version
*
* @return bool
*/
public function reset( $opts = [ 'env' => 'dev', 'multi' => false, 'ver' => 'latest' ] ) {
$env = $opts['env'];
$version = $opts['ver'];
$multi = $opts['multi'];
$all_config = $this->read_yaml();
$key = "wordpress-$env";
if ( ! $this->ensure_basic_config( $all_config, $env ) ) {
return false;
}
if ( ! isset( $all_config[ $key ]['path'] ) ) {
$this->say( "No path set for environment $env." );
}
$config = $all_config[ $key ];
$prefix = $config['cli-prefix'];
$wp = trim( "wp $prefix" );
$path = $config['path'];
$path = substr( $path, 0, 1 ) !== '/' ?
__DIR__ . '/' . $path :
$path;
$version = $version === 'latest' ? '' : "--version=$version";
$config_create = $this->additional_parameters( 'config create', $config );
$install_cmd = $multi ? 'core multisite-install' : 'core install';
$install_params = $this->additional_parameters( $install_cmd, $config );
echo "$wp $install_cmd $install_params\n";
$this->taskExec( "$wp db reset --yes" )->run();
$this->taskExecStack()
->exec( "rm -rf $path/*" )
->exec( "$wp core download $version" )
->exec( "$wp config create $config_create" )
->exec( "$wp config delete WP_DEBUG" )
->exec( "$wp $install_cmd $install_params" )
->run();
foreach ( $config['post-install'] as $cmd ) {
$cmd = str_replace( '$wp', $wp, $cmd );
$cmd = str_replace( '$path', $path, $cmd );
$cmd = str_replace( '$cwd', __DIR__, $cmd );
$cmd = str_replace( '~', getenv( "HOME" ), $cmd );
echo $cmd . "\n";
$this->taskExec( $cmd )->run();
}
}
/**
* Run a set of wp-cli commands on an existing WordPress environment
*
* @param string $profileName Name of the profile in robo.yml
*
* @option env Environment (dev, test etc)
*/
public function profile( $profileName, $opts = ['env' => 'dev']) {
$env = $opts['env'];
$all_config = $this->read_yaml();
$key = "wordpress-$env";
if ( ! $this->ensure_basic_config( $all_config, $env ) ) {
return false;
}
$config = $all_config[ $key ];
$prefix = $config['cli-prefix'];
$wp = trim( "wp $prefix" );
$path = $config['path'];
$path = substr( $path, 0, 1 ) !== '/' ?
__DIR__ . '/' . $path :
$path;
if ( ! isset( $all_config['profiles'][ $profileName ] ) ) {
$this->say( "Profile $profileName not found" );
return false;
}
$profile = $all_config['profiles'][ $profileName ];
foreach ( $profile as $cmd ) {
$cmd = str_replace( '$wp', $wp, $cmd );
$cmd = str_replace( '$path', $path, $cmd );
$cmd = str_replace( '$cwd', __DIR__, $cmd );
$cmd = str_replace( '~', getenv( "HOME" ), $cmd );
// Quick and dirty. If the cmd exactly matches another profile, run it!
if ( isset( $all_config['profiles'][ $cmd ] ) ) {
$this->profile( $cmd, $env );
continue;
}
echo $cmd . "\n";
$this->taskExec( $cmd )->run();
}
}
/**
* @return array
*/
private function read_yaml() {
return Yaml::parseFile( __DIR__ . '/robo.yml' );
}
/**
* @param $config
* @param $env
*
* @return bool
*/
private function ensure_basic_config( $config, $env ) {
$key = "wordpress-$env";
if ( ! isset( $config[ $key ] ) ) {
$this->say( "No path set for environment $env." );
return false;
}
if ( ! isset( $config[ $key ]['cli-prefix'] ) ) {
$this->say( "No wp-cli prefix set for environment $env." );
return false;
}
return true;
}
/**
* @param string $name
* @param array<string, string> $config
*
* @return string
*/
private function additional_parameters( $name, $config ) {
$name = str_replace( ' ', '-', $name );
if ( ! isset( $config[ $name ] ) ) {
return '';
}
return implode( ' ', array_map(
function ( $v, $k ) {
return sprintf( "--%s='%s'", $k, $v );
},
$config[ $name ],
array_keys( $config[ $name ] )
) );
}
}
Wrapping Up
In development, repetitive tasks tend to go with the territory. I can’t see a way of doing away with them completely, but automating as many of them as possible really helps you to get as much real work done with the time available.
Automating some of those tasks with the combination of WP-CLI and Robo that I’ve outlined here has saved time for me every single day as a plugin developer. I could never go back to doing these things manually again.
What tools do you use for automating the most tedious parts of your development workflow? Let me know in the comments.