How to build a plugin that adds a custom WP-CLI command?

Ever wanted to extend WP-CLI’s power with your own custom commands? It’s totally doable, and actually pretty straightforward! You can build a plugin that adds these commands, letting you automate tasks specific to your WordPress site or development workflow. Instead of manually clicking around or running complex database queries, you can craft a simple command to do the heavy lifting.

Before we dive into building, let’s briefly touch on what makes a WP-CLI command tick. Think of it as a function that you can execute from your terminal, but with some extra WP-CLI magic to make it smart.

Structure of a WP-CLI Command

A typical WP-CLI command involves a class that extends WP_CLI_Command. Inside this class, you’ll define methods, and each public method essentially becomes a subcommand. WP-CLI handles a lot of the plumbing, like parsing arguments and flags, making your job easier.

Autoloading and Command Registration

WP-CLI has its own autoloading mechanism. When you register a command, you’re telling WP-CLI where to find your command class. It then takes care of loading it when needed. This keeps things efficient – your command code only runs when you actually use it.

If you’re interested in expanding your WordPress development skills, you might find it beneficial to explore how to integrate email functionalities into your plugins. A related article that provides insights on this topic is available at Sending Email Using CyberPanel. This resource can help you understand how to manage email sending processes, which can be a valuable addition to your custom WP-CLI commands.

Setting Up Your Plugin Foundation

To build a custom WP-CLI command, you’ll start with a standard WordPress plugin. This keeps your custom code organized and portable across different WordPress installations.

Creating the Plugin File

First, create a new folder in your wp-content/plugins directory. Let’s call it my-custom-cli. Inside this folder, create a PHP file, say my-custom-cli.php. This will be your main plugin file.

“`php

/**

  • Plugin Name: My Custom WP-CLI Commands
  • Description: Adds custom WP-CLI commands for various tasks.
  • Version: 1.0
  • Author: Your Name
  • Text Domain: my-custom-cli

*/

// We’ll add our WP-CLI command registration here

“`

This basic header is all you need to get WordPress to recognize your plugin.

The WP_CLI_Command Class

The core of your command lives within a class that extends WP_CLI_Command. This parent class provides a lot of useful methods and structure.

“`php

// my-custom-cli.php

// … (plugin header)

if ( defined( ‘WP_CLI’ ) && WP_CLI ) {

class My_Custom_CLI_Command extends WP_CLI_Command {

/**

  • Clears all transients.

*

  • ## EXAMPLES

*

  • wp my-commands clear-transients

*

  • @when after_wp_load

*/

public function clear_transients( $args, $assoc_args ) {

global $wpdb;

$deleted = $wpdb->query( “DELETE FROM {$wpdb->options} WHERE option_name LIKE (‘_transient_%’) OR option_name LIKE (‘_site_transient_%’)” );

WP_CLI::success( sprintf( ‘%d transients deleted.’, $deleted ) );

}

/**

  • Deletes all posts of a specific post type.

*

  • ## OPTIONS

*

  • : The post type to delete.

*

  • [–force]
  • : Skip the confirmation prompt.

*

  • ## EXAMPLES

*

  • wp my-commands delete-posts product
  • wp my-commands delete-posts custom_post_type –force

*

  • @when after_wp_load

*/

public function delete_posts( $args, $assoc_args ) {

list( $post_type ) = $args;

if ( ! post_type_exists( $post_type ) ) {

WP_CLI::error( “Post type ‘{$post_type}’ does not exist.” );

}

if ( ! isset( $assoc_args[‘force’] ) ) {

WP_CLI::confirm( “Are you sure you want to delete all posts of type ‘{$post_type}’?” );

}

$posts_to_delete = get_posts( array(

‘post_type’ => $post_type,

‘posts_per_page’ => -1,

‘fields’ => ‘ids’,

‘meta_query’ => array(

‘relation’ => ‘OR’,

array(

‘key’ => ‘_wp_trash_meta_status’,

‘compare’ => ‘NOT EXISTS’,

),

array(

‘key’ => ‘_wp_trash_meta_status’,

‘value’ => ‘trash’, // Include trashed posts to permanently delete them.

‘compare’ => ‘!=’,

),

),

) );

if ( empty( $posts_to_delete ) ) {

WP_CLI::warning( “No posts of type ‘{$post_type}’ found to delete.” );

return;

}

$deleted_count = 0;

foreach ( $posts_to_delete as $post_id ) {

wp_delete_post( $post_id, true ); // true for permanent deletion

$deleted_count++;

}

WP_CLI::success( sprintf( ‘%d posts of type “%s” permanently deleted.’, $deleted_count, $post_type ) );

}

}

WP_CLI::add_command( ‘my-commands’, ‘My_Custom_CLI_Command’ );

}

“`

Notice the if ( defined( 'WP_CLI' ) && WP_CLI ) check. This ensures your command code only runs when WP-CLI is active, preventing errors if someone activates your plugin without WP-CLI installed.

Registering Your Custom Command

The final crucial step is to tell WP-CLI about your command. This is done using WP_CLI::add_command().

The WP_CLI::add_command() function

This function takes two main arguments:

  1. The top-level command name: This is what users will type in the terminal. In our example, it’s my-commands.
  2. The command class: This is the name of the PHP class that contains your command methods. In our example, My_Custom_CLI_Command.

So, WP_CLI::add_command( 'my-commands', 'My_Custom_CLI_Command' ); links wp my-commands to the My_Custom_CLI_Command class.

Crafting Your Command Methods

Each public method within your command class becomes a subcommand. The method name will be used as the subcommand.

Argument Handling

WP-CLI automatically parses positional arguments and named flags, passing them to your method.

  • $args: An array of positional arguments. For example, in wp my-commands delete-posts product, product would be in $args.
  • $assoc_args: An associative array of named flags (or “associative arguments”). For example, in wp my-commands delete-posts product --force, --force would appear as ['force' => true] in $assoc_args.

WP-CLI Utility Functions

WP-CLI provides a suite of helpful functions to interact with the user and WordPress itself:

  • WP_CLI::success( string $message ): Prints a success message in green.
  • WP_CLI::warning( string $message ): Prints a warning message in yellow.
  • WP_CLI::error( string $message, bool $exit = true ): Prints an error message in red and exits the script by default.
  • WP_CLI::log( string $message ): Prints a plain message.
  • WP_CLI::debug( string $message ): Prints a debug message (only visible if --debug flag is used).
  • WP_CLI::confirm( string $question, bool $exit_on_no = true ): Prompts the user for confirmation.
  • WP_CLI::line( string $message = '' ): Prints an empty line or a simple line of text.
  • WP_CLI\Utils\get_flag_value( array $args, string $flag ): Safely retrieves the value of a flag from $assoc_args.

Using these functions makes your commands more robust and user-friendly.

DocBlocks for Help Generation

The special comments (DocBlocks) above your methods are incredibly important. WP-CLI uses them to generate the help text for your command.

  • Short description: The first line after /** is the short description.
  • ## OPTIONS: Describes the expected arguments and flags.
  • : Required positional argument.
  • []: Optional positional argument.
  • --: Boolean flag.
  • --=: Flag with a value.
  • ## EXAMPLES: Provides usage examples.
  • @when / @hook : Tells WP-CLI when to load the command. @when after_wp_load is a common and a good default to ensure WordPress is fully loaded.

Well-written DocBlocks are essential for making your commands discoverable and understandable for anyone who uses them. Without them, users will be left guessing how to use your shiny new command.

If you’re looking to enhance your WordPress development skills, you might find it helpful to explore a related article that discusses the process of creating a payment system for your website. This resource provides valuable insights into integrating payment functionalities, which can complement your efforts in building a plugin that adds a custom WP-CLI command. To learn more about this topic, check out the article on making payments.

Advanced Command Features

Beyond the basics, WP-CLI offers some powerful features to make your commands even more useful.

Using External Files for Commands

As your plugin grows, you might want to split your command class into multiple files for better organization. For instance, you could have a src directory with My_Custom_CLI_Command.php inside it.

“`php

// my-custom-cli.php

// … (plugin header)

if ( defined( ‘WP_CLI’ ) && WP_CLI ) {

// Autoload our command class

require_once __DIR__ . ‘/src/My_Custom_CLI_Command.php’;

WP_CLI::add_command( ‘my-commands’, ‘My_Custom_CLI_Command’ );

}

“`

“`php

// my-custom-cli/src/My_Custom_CLI_Command.php

class My_Custom_CLI_Command extends WP_CLI_Command {

// … (your methods)

}

“`

This keeps your main plugin file clean and makes your codebase easier to manage. For larger plugins, you might even consider a more robust autoloader, but for simple WP-CLI commands, a require_once is often sufficient. If you have multiple command classes, you could have require_once statements for each, or use a loop to include them from a certain directory.

Command Grouping with Sub-commands

You can create hierarchical commands by registering a parent command that acts as a container for other command classes. This is great for keeping related commands together.

Let’s imagine you want to manage users and posts with separate command groups.

“`php

// my-custom-cli.php

// … (plugin header)

if ( defined( ‘WP_CLI’ ) && WP_CLI ) {

require_once __DIR__ . ‘/src/User_CLI_Commands.php’;

require_once __DIR__ . ‘/src/Post_CLI_Commands.php’;

// Register a top-level command group

WP_CLI::add_command( ‘my-plugin’, ‘My_Plugin_CLI_Command_Group’ );

// Register sub-commands under the ‘user’ group

WP_CLI::add_command( ‘my-plugin user’, ‘User_CLI_Commands’ );

// Register sub-commands under the ‘post’ group

WP_CLI::add_command( ‘my-plugin post’, ‘Post_CLI_Commands’ );

}

// Optionally, you can have an empty dummy class for the parent group

// or a class with common methods that apply to the whole group.

class My_Plugin_CLI_Command_Group extends WP_CLI_Command {

/**

  • Commands for My Plugin.

*

  • ## EXAMPLES

*

  • wp my-plugin user list
  • wp my-plugin post clean

*/

public function __invoke() {

// This method can be used for a general help message for the group

// or a default action if someone types wp my-plugin without a subcommand.

// For simple grouping, it can be empty.

}

}

“`

“`php

// my-custom-cli/src/User_CLI_Commands.php

class User_CLI_Commands extends WP_CLI_Command {

/**

  • Lists all users.

*

  • ## EXAMPLES

*

  • wp my-plugin user list

*/

public function list() {

$users = get_users( array( ‘fields’ => array( ‘ID’, ‘display_name’, ‘user_email’ ) ) );

WP_CLI\Utils\json_encode_pretty( $users );

WP_CLI::success( ‘Users listed.’ );

}

/**

  • Deactivates a user.

*

  • ## OPTIONS

*

  • : The ID of the user to deactivate.

*

  • ## EXAMPLES

*

  • wp my-plugin user deactivate 123

*/

public function deactivate( $args, $assoc_args ) {

list( $user_id ) = $args;

$user = get_user_by( ‘ID’, $user_id );

if ( ! $user ) {

WP_CLI::error( “User with ID ‘{$user_id}’ not found.” );

}

// A real deactivation would involve more logic, e.g., setting a meta key,

// revoking capabilities, logging out sessions. This is just an example.

if ( update_user_meta( $user->ID, ‘is_deactivated’, true ) ) {

WP_CLI::success( “User ‘{$user->display_name}’ (ID: {$user->ID}) deactivated.” );

} else {

WP_CLI::warning( “Failed to deactivate user ‘{$user->display_name}’ (ID: {$user->ID}).” );

}

}

}

“`

“`php

// my-custom-cli/src/Post_CLI_Commands.php

class Post_CLI_Commands extends WP_CLI_Command {

/**

  • Cleans up old post revisions.

*

  • ## OPTIONS

*

  • [–keep=]
  • : The number of latest revisions to keep per post. Default 5.

*

  • ## EXAMPLES

*

  • wp my-plugin post clean-revisions
  • wp my-plugin post clean-revisions –keep=2

*/

public function clean_revisions( $args, $assoc_args ) {

$keep = WP_CLI\Utils\get_flag_value( $assoc_args, ‘keep’, 5 );

// This is a simplified example. A real implementation would iterate posts

// and delete excessive revisions.

WP_CLI::debug( “Keeping latest {$keep} revisions per post.” );

$post_types = get_post_types( array( ‘public’ => true ), ‘names’ );

$total_deleted = 0;

foreach ( $post_types as $post_type ) {

$posts = get_posts( array(

‘post_type’ => $post_type,

‘posts_per_page’ => -1,

‘fields’ => ‘ids’,

) );

foreach ( $posts as $post_id ) {

$revisions = wp_get_post_revisions( $post_id, array( ‘order’ => ‘ASC’ ) );

$revision_count = count( $revisions );

if ( $revision_count > $keep ) {

$revisions_to_delete = array_slice( $revisions, 0, $revision_count – $keep );

foreach ( $revisions_to_delete as $revision_id => $revision_post ) {

wp_delete_post_revision( $revision_id );

$total_deleted++;

}

}

}

}

WP_CLI::success( sprintf( ‘%d old post revisions cleaned.’, $total_deleted ) );

}

}

“`

Now you could run wp my-plugin user list or wp my-plugin post clean-revisions. This makes your command structure much more logical for a larger set of functionalities.

Interacting with the WordPress Environment

Your WP-CLI commands run within the full WordPress environment. This means you have access to:

  • All WordPress functions (get_posts, update_option, wp_users, etc.)
  • Plugin and theme functions (if they are loaded)
  • Global variables like $wpdb.

This is the real power of WP-CLI: you can programmatically do almost anything you’d do through the admin panel or custom code, but from your terminal. Just be mindful of performance for very large operations, and consider using WP_CLI\Utils\iterator_aggregate for processing large datasets in chunks to avoid memory issues.

Testing Your Commands

Once you’ve built your commands, you need to test them to make sure they work as expected and don’t break anything.

Manual Testing

The simplest way to test is to just run your commands from the terminal.

  1. Activate your plugin:

wp plugin activate my-custom-cli

  1. Run your command:
  • wp my-commands clear-transients
  • wp my-commands delete-posts product
  • wp help my-commands clear-transients (to check your DocBlocks)
  • wp my-plugin user list (for grouped commands)

Carefully observe the output and check if the intended actions (e.g., transients cleared, posts deleted) have actually occurred in your database or on your site.

Debugging with WP_CLI::debug()

For more complex commands, WP_CLI::debug() is your best friend. Add WP_CLI::debug("Some variable value: " . $variable); throughout your code, and then run your command with the --debug flag:

wp my-commands my-subcommand --debug

This will show all your debug messages, giving you insight into the execution flow and variable states.

Rollback Strategy (Important!)

When running commands that modify your database (especially deletions), always have a rollback strategy.

  • Local environment: If you’re working locally, a simple database backup before testing is sufficient (wp db export backup.sql).
  • Staging/Production: Never run untested destructive commands on production. Always test thoroughly on a staging environment that mirrors production. Consider a full database backup before running critical commands on staging. Tools like UpdraftPlus or your hosting provider’s backup features can be invaluable here.

Remember, WP-CLI is powerful; with great power comes great responsibility! Be careful what you delete or modify.

Best Practices and Tips

To make your WP-CLI commands robust and maintainable, adhere to a few best practices.

Keep Commands Focused and Modular

Each command method should ideally do one thing well. If a command starts getting too complex, break it down into smaller, more manageable sub-commands or private helper methods within your class. This makes testing and debugging much easier.

Provide Clear Output and Feedback

Users should always know what your command is doing. Use WP_CLI::success, WP_CLI::warning, and WP_CLI::error appropriately. For long-running tasks, consider using WP_CLI::log or a progress bar utility (like WP_CLI\Utils\make_progress_bar) to give real-time updates.

Validate Input Thoroughly

Don’t assume the user will always provide correct input. Validate arguments and flags. Check if post types exist, if user IDs are valid, if paths are correct, etc. Provide clear error messages if validation fails.

Use the @when Hook

The @when after_wp_load annotation is crucial for ensuring the WordPress environment is fully loaded before your command attempts to interact with it. Without this, you might run into errors trying to use WordPress functions that aren’t yet available.

Leverage Existing WP-CLI Commands

Before writing your own, check the existing WP-CLI commands. There might already be a command (or a combination of commands) that does what you need, or at least gets you part of the way there. This saves you development time and leverages well-tested code.

Consider Logging for Auditing

For critical operations, beyond just printed output, consider implementing an internal logging mechanism. This could be a custom post type, a log file, or entries in a logging plugin like WP Activity Log. This provides an audit trail of changes made via your custom commands.

Building custom WP-CLI commands is a game-changer for WordPress development and automation. It allows you to tailor your workflow, automate repetitive tasks, and extend WordPress’s capabilities directly from the command line. With a solid understanding of the basics and adherence to best practices, you’ll be crafting powerful commands in no time!