Title: Best practices for writing WP-CLI commands
Author: WordPress VIP Documentation
Published: October 25, 2021
Last modified: May 16, 2024

---

 1. [VIP-CLI](https://docs.wpvip.com/vip-cli/)
 2. [WP-CLI with VIP-CLI](https://docs.wpvip.com/vip-cli/wp-cli-with-vip-cli/)
 3. Best practices for writing WP-CLI commands

#  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](https://make.wordpress.org/cli/handbook/guides/commands-cookbook/#anatomy-of-a-command)
of the [WP-CLI handbook](https://make.wordpress.org/cli/).

## 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:

    ```lang-php
    <?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:

    ```lang-php
    <?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](https://developer.wordpress.org/reference/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()](https://developer.wordpress.org/reference/functions/remove_filter/)`
or [`remove_action()`](https://developer.wordpress.org/reference/functions/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](https://developer.wordpress.org/reference/classes/wpdb/).
When building these queries all variables should be properly [sanitized](https://docs.wpvip.com/security/validating-sanitizing-and-escaping/)
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.

    ```lang-php
    <?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()](https://developer.wordpress.org/reference/classes/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()](https://developer.wordpress.org/reference/classes/wpdb/esc_like/)`
method with the `[$wpdb->prepare()](https://developer.wordpress.org/reference/classes/wpdb/prepare/)`
method to construct the query.

    ```lang-php
    <?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](https://developer.wordpress.org/reference/classes/wpdb/)
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:

 * [annotating sub-command functions with PHPDoc](https://make.wordpress.org/cli/handbook/guides/commands-cookbook/#annotating-with-phpdoc)
 * or [providing a third `$args` parameter of configuration options to `WP_CLI::add_command()`](https://make.wordpress.org/cli/handbook/guides/commands-cookbook/#wp_cliadd_commands-third-args-parameter)
   when a WP-CLI command is registered

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:

    ```lang-php
    <?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-block-preformatted
    $ 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-block-preformatted
    $ 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-block-preformatted
    $ wp onboard
    usage: wp onboard <id>
    ```

Last updated: May 16, 2024