Skip to content
Renato Alves edited this page Jan 19, 2024 · 3 revisions

Thanks for your interest in the alleyinteractive/wp-bulk-task Composer package. This wiki contains detailed usage information and some tips on how to get the most out of the package.

Basic Usage

Install the package via Composer:

composer require alleyinteractive/wp-bulk-task

Ensure that the Composer autoloader is being included in your project so that the class files can be loaded automatically:

require_once '/path/to/vendor/autoload.php';

Then declare a new bulk task with a unique key and run it with WP_Query search/filter parameters and a callback for each post:

(new \Alley\WP_Bulk_Task\Bulk_Task( 'my-unique-key' ))->run(
    [ 'post_type' => 'post' ],
    function( WP_Post $post ) {
        // Do something with the post object here.
    }
);

You can also declare a new bulk task with a unique key and run it with WP_Term_Query search/filter params and a callback for each term:

(new \Alley\WP_Bulk_Task\Bulk_Task( 'my-unique-key' ))->run(
    [ 'taxonomy' => 'category' ],
    function( WP_Term $term ) {
        // Do something with the term object here.
    },
    'wp_term'
);

Bulk task functionality can be used in any context, but is recommended for systems that are intended to handle long-running processes, such as WP-CLI or WP_Cron.

Usage with WP_CLI

Outputting Progress

You can pass an optional second parameter to the Bulk_Task constructor to output progress of the run. WP Bulk Task ships with a helper class for this called PHP_CLI_Progress_Bar, or you can roll your own using the Progress interface (more on this below).

$bulk_task = new \Alley\WP_Bulk_Task\Bulk_Task(
    'my-unique-key',
    new \Alley\WP_Bulk_Task\Progress\PHP_CLI_Progress_Bar(
        __( 'Bulk Task: my-unique-key', 'my-textdomain' )
    )
);

By passing in an instance of a class that implements the Progress interface, like the PHP_CLI_Progress_Bar class above, progress will automatically be updated and output during the run of the task.

Resetting the Cursor

By default, the cursor will be used and updated whenever the task is run using the same unique key. Resetting the cursor is the responsibility of the implementer.

How and when the cursor is reset can take a number of different forms. However, it is generally encouraged to provide an option that resets the cursor and immediately exits the task, so that resetting the cursor and running the task can be accomplished independently, to accommodate running CLI tasks that will automatically resume if interrupted.

Using a Rewind Flag

A rewind flag is a boolean flag that can be provided when the WP CLI command is run. It is defined in the docblock for the command and can be used like this:

/**
 * Command description.
 *
 * ## OPTIONS
 *
 * [--rewind]
 * : Resets the cursor so the next time the command is run it will start from the beginning.
 */
public function example_command( $args, $assoc_args ) {
    $bulk_task = new \Alley\WP_Bulk_Task\Bulk_Task( 'example_command' );

    // Handle rewind requests.
    if ( ! empty( $assoc_args['rewind'] ) ) {
        $bulk_task->cursor->reset();
        WP_CLI::log( __( 'Rewound the cursor. Run again without the --rewind flag to process posts.', 'my-textdomain' ) );
        return;
    }

    // Run the command.
    $bulk_task->run( /* run command arguments here */ );
}

Using a From Scratch Flag

A from scratch flag is a boolean flag that can be provided when the WP CLI command is run. It differs from a rewind flag in that it will reset the cursor and then run the command, which is useful during local development, but is less ideal for production runs, because it doesn't handle automatic restarts due to connection failures. It is defined in the docblock for the command and can be used like this:

/**
 * Command description.
 *
 * ## OPTIONS
 *
 * [--from-scratch]
 * : Resets the cursor before starting the command.
 */
public function example_command( $args, $assoc_args ) {
    $bulk_task = new \Alley\WP_Bulk_Task\Bulk_Task( 'example_command' );

    // Handle from scratch requests.
    if ( ! empty( $assoc_args['from-scratch'] ) ) {
        $bulk_task->cursor->reset();
    }

    // Run the command.
    $bulk_task->run( /* run command arguments here */ );
}

Commands that implement --from-scratch should also implement --rewind. --rewind should be used on runs on remote systems so that the cursor can behave as intended when the command could be interrupted and would need to be restarted.

Interactively

An alternate option to a --rewind or --from-scratch flag is to ask the user whether they want to start the run over from the beginning or not. This can be accomplished thus:

/**
 * Command description.
 */
public function example_command( $args, $assoc_args ) {
    $bulk_task = new \Alley\WP_Bulk_Task\Bulk_Task( 'example_command' );

    // Ask the user whether to start from scratch.
    fwrite( STDOUT, 'Start the run from scratch? [y/n] ' );
    $from_scratch = strtolower( trim( fgets( STDIN ) ) );
    if ( 'y' === $from_scratch ) {
        $bulk_task->cursor->reset();
    }

    // Run the command.
    $bulk_task->run( /* run command arguments here */ );
}

WP-CLI doesn't provide a helper method for this, but this code was lifted from WP-CLI's confirm method.

Handling Dry Runs

A common pattern when writing WP-CLI commands is to provide support for a --dry-run flag that doesn't make any changes to the database, but outputs detailed logging about what operations would be performed. Here is an example of how to set up a command with a dry run flag that is available to the callback, and that disables the progress bar in favor of detailed logging:

/**
 * Command description.
 *
 * ## OPTIONS
 *
 * [--dry-run]
 * : Resets the cursor before starting the command.
 */
public function example_command( $args, $assoc_args ) {
    $dry_run   = ! empty( $assoc_args['dry-run'] );
    $bulk_task = new \Alley\WP_Bulk_Task\Bulk_Task(
        'example_command',
        $dry_run ? null : new \Alley\WP_Bulk_Task\Progress\PHP_CLI_Progress_Bar(
            __( 'Bulk Task: my-unique-key', 'my-textdomain' )
        )
    );
    // Optionally handle `--rewind`, `--from-scratch`, or user input here.
    $bulk_task->run(
        [ /* WP_Query arguments */ ],
        function( $post ) use ( $dry_run ) {
            $new_value = str_replace( 'apple', 'banana', $post->post_content );
            if ( $dry_run ) {
                WP_CLI::log( 'Old post_content: ' . $post->post_content );
                WP_CLI::log( 'New post_content: ' . $new_value );
            } else {
                $post->post_content = $new_value;
                wp_update_post( $post );
            }
        }
    );
}

Custom Progress Handling

If you are using this class outside of WP-CLI, or otherwise don't want to use the PHP CLI progress bar, you can implement your own progress handler by defining a class that implements \Alley\WP_Bulk_Task\Progress\Progress. For example:

class MyCustomProgress implements \Alley\WP_Bulk_Task\Progress\Progress {
    /**
     * Keeps track of the current progress toward the total.
     *
     * @var int
     */
    private int $current = 0;

    /**
     * Keeps track of the total number of items.
     *
     * @var int
     */
    private int $total = 0;

    /**
     * Sets the current value of the progress tracker.
     *
     * @param int $current The new current value for the progress tracker.
     */
    public function set_current( int $current ): void {
        $this->current = $current;
        printf( 'Processed %d of %d' . "\n", $this->current, $this->total );
    }

    /**
     * Tells the progress tracker that it is finished.
     */
    public function set_finished(): void {
        echo 'Done!' . "\n";
    }

    /**
     * Sets the maximum value of things being counted in the progress tracker.
     *
     * @param int $total The total that is being counted up to.
     */
    public function set_total( int $total ): void {
        $this->total = $total;
    }
}

An instance of this class can be passed to the second parameter of the Bulk_Task constructor and will be automatically used by the task as it runs:

$bulk_task = new \Alley\WP_Bulk_Task\Bulk_Task( 'example_command', new MyCustomProgress() );

Elasticsearch

If your site is using a plugin to offload queries to Elasticsearch, such as SearchPress, WordPress VIP Enterprise Search, or ElasticPress, you will need to disable the query integration before running a bulk task to ensure that the queries are run directly against the database. If you fail to do so and run a query that is offloaded to Elasticsearch, the ID-based pagination will fail, causing the task to run in an infinite loop.

Disabling VIP Enterprise Search and ElasticPress Integration

Add the following before running your bulk task:

add_filter( 'ep_skip_query_integration', '__return_true' );

Since version 0.2.1, this is handled by default.