Skip to content

Best practices for writing WP-CLI commands

By following best practices when writing a custom WP-CLI command, the command is more likely to be performant, and easier to debug and maintain over time.

For additional guidance, best practices, and helpful examples, also refer to the Commands Cookbook section of the WP-CLI handbook.

Comment and provide verbose output

It is important to be very clear about what each part of a custom WP-CLI command is doing and the reasoning behind the logic. Comments are especially helpful when something doesn’t work as intended and needs to be debugged by other team members or by VIP Support.

In addition to defaulting to non-destructive, a descriptive opening line in the script and a descriptive line for every action the command is performing should also be included. Be as verbose as possible in output. It is important when running the command to know that something is happening, what is happening, how far the script has progressed, or when the script is expected to finish.

For example:

<?php
public function __invoke( $args, $assoc_args ) {

	// ...process args

	// Let user know if command is running dry or live
	if ( true === $dry_mode ) {
		WP_CLI::line( '===Dry Run===' );
	} else {
		WP_CLI::line( 'Doing it live!' );
	}

	// ...define $query_args for WP_Query object
		
	// Set variables for holding stats printed on the end of the run
	$updated = $missed = 0;
	
	do {
		// Let user know how many posts are about to be processed
		WP_CLI::line( sprintf( 'Processing %d posts at offset of %d of %d total found posts', count( $query->posts ), $offset, $query->found_posts ) );
		
		// ...do stuff
		
		// Let user know what is happening
		WP_CLI::line( sprintf( 'Updating %s meta for post_id: ' ), 'some_meta_key', $post_id );
		
		// Save result of update/delete functions
		$updated = update_post_meta( $post_id, 'some_meta_key', sanitize_text_field( $some_meta_value ) ); if ( $updated ) {
			// Let user if update was successful
			WP_CLI::line( "Success: Updated post_meta '%s' for post_id %d with value %s", 'some_meta_key', $post_id, serialize( $some_meta_value ) );

			// Count successful updates
			$updated++;
		} else {
			// If not successful, provide some helpful debug info
			WP_CLI::line( "Error: Failed to update post_meta '%s' for post_id %d with value %s", 'some_meta_key', $post_id, serialize( $some_meta_value ) ); // There are some values (eg.: WP_Error object) that should be serialized in order to print something meaningful

			// Count any errors/skips
			$missed++;
			
			// Free up memory
			$this->vip_inmemory_cleanup();
			$query_args['paged']++;
			$query = new WP_Query( $query_args );
		}
	} while( $query->have_posts() );
		
	// Let user know result of the script
	WP_CLI::line( "Finished the script. Updated: %d. Missed: %d", $updated, $missed );
}

Default to non-destructive

When possible, it is recommended that custom WP-CLI commands default to do a dry run that will show what would have changed, without affecting live data. For example, defaulting to --dry-run=true and requiring a user to explicitly pass --dry-run=false to allow a “live” run—this way, allows for a comparison between what the actual impact is versus the expected impact:

<?php
$dry_mode = ! empty ( $assoc_args['dry-run'] );
if ( ! $dry_mode ) {
	WP_CLI::line( " * Removing {$user_login} ( {$user_id} )... " );
	$remove_result = remove_user_from_blog( $user_id, $blog_id );
	if ( is_wp_error( $remove_result ) ) {
		$failed_to_remove[] = $user;
	}
} else {
	WP_CLI::line( " * Will remove {$user_login} ( {$user_id} )... " );
}

Indicate state with WP_IMPORTING

If a custom WP-CLI command is importing posts or calling wp_update_post(), consider adding define( 'WP_IMPORTING', true ); to the top of the related code to ensure that minimal extra actions are fired.

Interacting with the database

Direct SQL database queries can negatively impact a WordPress application in unexpected ways. One of the most common is causing the object cache to be out of sync. It is strongly recommended to use WordPress core functions as often as possible when interacting with database tables and records. The WP-CLI loads WordPress core as well as an application’s theme, plugins, and MU plugins, all of which are available to use inside a custom WP-CLI command.

Note

If using a WordPress core function to add or edit content through a custom WP-CLI command causes an undesired hook to fire, consider using remove_filter() or remove_action(). This can prevent it from firing before querying the database directly.

If a hook cannot be unhooked, or if there is some other limitation that requires a custom WP-CLI command to query the database directly, it is recommended to use the helper functions in the global $wpdb object. When building these queries all variables should be properly sanitized and queries that write should follow up with clean_post_cache() to flush associated cache so updates will be visible on a site before without having to wait for the cache to expire.

<?php
$wpdb->update(
	$wpdb->posts,
	// Table array.
	( 'post_content' => sanitize_text_field( $post_content ) // Data should not be SQL escaped, but sanitized ),
	// Data array( 'ID' => intval( $post_id ) ), // WHERE
	array( '%s' ), // data format
	array( '%d' ) // where format
);

clean_post_cache( $post_id ); // Clean the cache to reflect changes.

If a custom SQL query needs to be run against the database directly, always use the method $wpdb->prepare() to create a parameterized query (also known as a prepared statement) as a safeguard against SQL injection attacks. When dealing with “LIKE” statements, use the $wpdb->esc_like() method with the $wpdb->prepare() method to construct the query.

<?php
global $wpdb;
$results = $wpdb->get_results(
	$wpdb->prepare(
		"SELECT * FROM {$wpdb->posts} WHERE post_title = %s AND ID = %d",
		$post_title,
		$min_post_id
	)
);

$like = '%' . $wpdb->esc_like( $args['search'] ) . '%';
$query = $wpdb->prepare(
	"SELECT * FROM {$wpdb->posts} as p AND ((p.post_title LIKE %s) OR (p.post_name LIKE %s))",
	$like,
	$like
);

See the wpdb class documentation for a full list of available methods.

Provide help documentation for a command

When writing a custom WP-CLI command it is strongly encouraged to provide in-code help documentation for its use. This allows CLI users to learn about a command’s purpose and options within the CLI console by running the WP-CLI global --help parameter.

In-code help documentation can be added by either:

An added benefit of implementing in-code help documentation is that by defining information in the long desc, or synopsis, the command will perform basic argument validation.

This example shows documentation added to in-code help documentation for a custom WP-CLI command wp onboard which accepts a user ID as a parameter and adds useful onboarding capabilities:

<?php
class Onboard_Command {

	/**
	 * Onboards a development user providing useful capabilities.
	 *
	 * ## OPTIONS
	 *
	 * <id>
	 * : The id of the registered WordPress user.
	 *
	 * ## EXAMPLES
	 *
	 *     wp onboard 15
	 */
	public function __invoke( $args, $assoc_args ) {

		$capabilities = array( 'view_query_monitor' );

		if ( ! empty( $args[0] ) ) {
			$user = get_user_by( 'id', $args[0] );
			if ( is_a( $user, 'WP_User' ) ) {
				WP_CLI::log( sprintf( 'Adding capabilities to user %s (%d) ', $user->user_login, $user->id ) );

				foreach ( $capabilities as $cap ) {
					WP_CLI::log( sprintf( ' - Adding capability: %s', $cap ) );
					$user->add_cap( $cap );
				}

				WP_CLI::success( sprintf( 'User %s (%d) onboarded', $user->user_login, $user->id ) );
				return;
			}
		}

		WP_CLI::error( 'No user onboarded' );
	}
}

if ( defined( 'WP_CLI' ) && WP_CLI ) {
	WP_CLI::add_command( 'onboard', 'Onboard_Command' );
}

When a CLI user runs wp --help they will see onboard in the list, with the short description “Onboards a development user providing useful capabilities.” When they run wp onboard --help they will see the more verbose output, including subcommands, if applicable.

$ wp onboard --help
NAME

  wp onboard

DESCRIPTION

  Onboards a development user providing useful capabilities.

SYNOPSIS

  wp onboard <id>

OPTIONS

  <id>
    The id of the registered WordPress user.

EXAMPLES

    wp onboard 15

In this example, if the synopsis for the ID argument had been omitted, the responsibility to handle the validation and response would be handled by the command:

$ wp onboard
Error: No user onboarded

But because of the synopsis definition of the required <id>, omission of the parameter results in a usage suggestion:

$ wp onboard
usage: wp onboard <id>

Debugging with New Relic

The containers that run WP-CLI commands are not monitored by New Relic by default, but this can be enabled by reaching out to VIP Support.

Last updated: December 26, 2023

Relevant to

  • WordPress