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:
- annotating sub-command functions with PHPDoc
- or providing a third
$args
parameter of configuration options toWP_CLI::add_command()
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:
<?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>
Last updated: May 16, 2024