Did you know that the WordPress admin area isn’t the only way to update themes, plugins, and WordPress core? And have you ever been tasked with creating, updating, or deleting numerous users based on a CSV file? Migrating a site and wishing for a simpler solution is a common desire among us WordPress developers.

Let me introduce you to a powerful tool that can simplify these tasks: WP-CLI. But first, let me illustrate its usefulness with a real-world example.
The Challenge: In a recent project, I needed to automate the process of updating user permissions based on their membership status. The client wanted to revoke membership access for users who had stopped their subscriptions or whose trial periods had expired. Manually managing potentially thousands of subscriptions through the admin panel was not a viable option.
The Solution: WP-CLI came to the rescue. With just a few commands, I was able to automatically remove the appropriate membership levels for users who were no longer subscribed.
This post aims to introduce you to WP-CLI and guide you through building a simple custom command similar to the one I used. You’ll gain valuable insights into incorporating WP-CLI into your own development workflow.
WP-CLI: A Brief Introduction
Even if you’re new to WP-CLI, you’re in good company. Despite its existence for several years, it remained relatively unknown in the WordPress community for some time.
Here’s a concise definition from the official website:
WP-CLI is a command-line toolset for managing WordPress installations. It empowers you to update plugins, configure multisite setups, and accomplish various other tasks without relying on a web browser.
Consider these examples to grasp the capabilities of WP-CLI:
wp plugin update --all: Updates all eligible plugins.wp db export: Generates a SQL dump of your database.wp media regenerate: Regenerates image thumbnails for attachments, particularly useful after modifying image sizes in your theme.wp checksum core: Verifies the integrity of WordPress core files to detect any unauthorized modifications.wp search-replace: Efficiently searches for and replaces strings within your database.
A visit to this link reveals an extensive collection of commands designed for tasks frequently performed by WordPress developers and site administrators. These commands have significantly reduced the time and effort required for routine maintenance.
Intrigued? Ready to begin? Excellent!
Before proceeding, ensure that WP-CLI is installed on your WordPress environment, either locally or globally on your machine. If you haven’t already installed it on your local development environment, the official website provides detailed installation instructions here. For users of Varying Vagrant Vagrants (VVV2), WP-CLI comes pre-installed. Many hosting providers also include WP-CLI as part of their platform. For this tutorial, I’ll assume you have WP-CLI successfully installed.
Tackling the Problem with WP-CLI
To automate the recurring tasks, we’ll create a custom WP-CLI command specifically for our WordPress installation. Creating a plugin provides an efficient way to add this functionality to our site. We’ll opt for a plugin for three primary reasons:
- Easy activation and deactivation of the custom command as needed.
- Modular structure for extending commands and subcommands.
- Portability of functionality across different themes and even separate WordPress installations.
Building the Plugin
Let’s start by creating a directory within our wp-content/plugins directory. Name this new directory toptal-wpcli. Inside this directory, create two files:
index.php: This file should contain a single line of code:<?php // Silence is goldenplugin.php: This file will house our main code. (Feel free to choose a different name if you prefer.)
Open the plugin.php file and insert the following code:
| |
This initial code snippet serves two primary purposes:
First, it defines the plugin header, which provides essential information about our plugin on the WordPress Plugins admin page. This header allows WordPress to recognize and activate our plugin. While only the plugin name is mandatory, including the other details enhances clarity for potential users and your future self.
Second, we use [check that WP-CLI is defined](https://make.wordpress.org/cli/handbook/commands-cookbook/#include-in-a-plugin-or-theme). This checks for the existence of the WP-CLI constant. If absent, the plugin execution halts. Otherwise, the code proceeds.
Remember that this code is a simplified example and should not be used directly in a production environment. Some functions serve as placeholders for real-world functionalities. Feel free to remove this reminder once you’ve replaced these placeholder functions with your actual implementations.
Introducing the Custom Command
Now, let’s incorporate the following code:
| |
This code block accomplishes the following:
- Defines the
TOPTAL_WP_CLI_COMMANDSclass, which will handle arguments passed to our command. - Associates the
toptalcommand with this class, enabling us to execute it from the command line.
Executing wp toptal remove_user should now produce the following output:
| |
This confirms that our toptal command is registered and the remove_user subcommand is operational.
Variable Setup
Since our goal is to remove users in bulk, let’s define the following variables:
| |
Let’s clarify the purpose of each variable:
$total_warnings: This variable will keep track of warnings, such as when an email address doesn’t exist or isn’t associated with the membership level we’re removing.$total_users_removed: This variable will count the number of users removed during the process. (Note: We’ll address a potential issue related to this count later.)$dry_suffix: If we’re performing a dry run, this variable will append specific wording to the final success message.$emails_not_existing: This array will store a list of email addresses that were not found in the system.$emails_without_level: This array will store a list of email addresses that didn’t have the specified membership level.$dry_run: This boolean variable will indicate whether the script is executing in dry run mode (true) or not (false).$level: This integer variable will represent the membership level we want to check and potentially remove.$email: This array will contain the email addresses we want to process against the specified membership level. We’ll iterate through this array.
With our variables in place, we can proceed to the core functionality of our function.
Crafting the Function Logic
We’ll use a foreach loop to iterate through each email address within our $emails array:
| |
Next, we’ll introduce a conditional check:
| |
This check verifies whether a registered user exists for the email address we’re processing. It utilizes the email_exists() function to determine if a user with the provided email exists. If no matching user is found, it generates a warning message on the terminal:
| |
The email address is then added to the $emails_not_existing array for later display. We also increment the total warning count by one before moving on to the next email in the loop.
If the email exists, we’ll use the $user_id and $level variables to determine if the user has access to the specified membership level. We’ll store the resulting boolean value in the $has_level variable:
| |
Similar to other functions in this example, function_to_check_membership_level() is a placeholder. Most membership plugins offer helper functions to retrieve this information.
Now, let’s focus on the primary action: removing the membership level from the user. We’ll utilize an if/else structure for this purpose:
| |
If $has_level evaluates to true (meaning the user has access to the membership level), we’ll call a function to remove that level. In this demonstration, we’ll use the placeholder function function_to_deactivate_membership_level().
Before removing the level, we’ll enclose the removal function within a conditional check based on the dry-run flag. If it’s a dry run, we only report the intended action without actually modifying anything. Otherwise, we proceed with removing the level, log a success message to the terminal, and continue iterating through the remaining emails.
Conversely, if $has_level is false (meaning the user doesn’t have access to the membership level), we log a warning message to the terminal, add the email address to the $emails_without_level array, and continue with the email loop.
Finalizing and Displaying Results
After processing all email addresses, we’ll display the results on the console. If it was a dry run, we’ll include an additional message:
| |
The $dry-suffix will be appended to both warnings and success notifications, providing clarity about the dry run context.
Finally, we’ll display the results as success and warning messages:
| |
Notice our use of the WP_CLI::success and WP_CLI::warning helper methods. These methods, provided by WP-CLI, streamline the process of logging information to the console. We can easily log strings, as demonstrated here, including our variables like $total_users_removed, $total_warnings, and $dry_suffix.
If any warnings were encountered during the script execution, we’ll display them on the console as well. After a conditional check, we’ll convert the $emails_not_existing and $emails_without_level arrays into string variables to facilitate printing them using the WP_CLI::warning helper method.
Enhancing Clarity with Descriptions
Comments are invaluable for both collaborators and our future selves when revisiting code after a period of time. WP-CLI offers a mechanism to annotate our command using short descriptions (shortdesc) and long descriptions (longdesc).
Let’s add the following at the beginning of our command, immediately after the TOPTAL_WP_CLI_COMMANDS class definition:
| |
The longdesc clarifies the expected input for our custom command. The syntax for both shortdesc and longdesc follows this pattern: Markdown Extra. The ## OPTIONS section lists the expected arguments. Required arguments are enclosed in < >, while optional arguments are enclosed in [ ].
These options are validated during command execution. For example, omitting the required email parameter results in the following error:
| |
The ## EXAMPLES section provides a practical example of how to call the command.
Our custom command is now complete. The final code can be found in this gist: here.
Room for Refinement and Potential Enhancements
It’s crucial to review our work and identify areas for improvement, expansion, and refactoring. Let’s explore some potential enhancements for this script.
One observation is that the script might occasionally report removing more users than it actually does. This discrepancy likely arises from the script’s execution speed exceeding the database query execution time. Your mileage may vary depending on your environment and configuration. A simple workaround involves running the script multiple times with the same input until it consistently reports zero users removed.
A potential improvement involves incorporating a validation step to ensure that a user’s level has been successfully removed before logging it as such. While this would slightly increase the script’s execution time, it would enhance accuracy and eliminate the need for multiple runs.
Similarly, we could enhance error handling by throwing specific errors if a level removal fails.
Expanding the script to handle multiple levels and email addresses simultaneously is another area for improvement. The current version processes one level at a time, assuming input from CSV files organized by level. The script could be modified to automatically detect and handle multiple levels and email addresses.
Consider refactoring the code to utilize ternary operators for improved conciseness instead of the more verbose conditional checks. While the current implementation prioritizes readability for demonstration purposes, feel free to customize it according to your preferences.
Instead of directly printing email addresses to the console, we could enhance the final step by automatically exporting them to a CSV or plain text file.
Lastly, the script lacks input validation for the $level and $emails variables. It assumes an integer for $level and a comma-separated list of email addresses for $emails. Currently, using strings instead of integers or providing usernames instead of email addresses would lead to unexpected behavior without clear error messages. Implementing checks for valid integers and email formats would make the script more robust.
Exploring Automation Possibilities and Further Resources
As illustrated by this use case, WP-CLI is remarkably versatile and powerful, enabling you to accomplish tasks efficiently. You might be wondering how to integrate WP-CLI into your everyday development workflow.
Here are some inspiring examples:
- Seamlessly update themes, plugins, and WordPress core from the command line.
- Effortlessly export databases for backup purposes or quickly generate SQL dumps for testing SQL queries.
- Simplify the process of migrating WordPress websites.
- Streamline new WordPress site installations, complete with dummy data or custom plugin setups.
- Ensure the integrity of your core files by running checksums to detect potential compromises. (Consider a project underway to extend this functionality to themes and plugins from the WordPress repository.)
- Create custom scripts for managing and monitoring site hosts, as described in here.
The possibilities with WP-CLI are virtually limitless. To further your exploration, here are some valuable resources:
- Main WP-CLI Website: http://wp-cli.org
- WP-CLI Commands Documentation: https://developer.wordpress.org/cli/commands/
- Official WP-CLI Blog: https://make.wordpress.org/cli/
- Comprehensive WP-CLI Handbook: https://make.wordpress.org/cli/handbook/
- WooCommerce Integration (WC-CLI): https://github.com/woocommerce/woocommerce/wiki/WC-CLI-Overview#woocommerce-commands
- Podcast Interview with Daniel Bachhuber, WP-CLI Maintainer: https://howibuilt.it/episode-28-daniel-bachhuber-wp-cli/