Writing Bin Scripts

Occasionally, you may find you need to access or transform data on your WordPress.com site. If it’s more than a dozen posts affected, it’s often more efficient to write what we call a “bin script.” In writing a bin script, you can easily change strings, assign categories, or add post meta across hundreds or thousands of posts. However, with great power comes great responsibility — any small mistake you make with your logic could have negative repercussions across your entire dataset.

Here are some guidelines we’d encourage you to follow when writing a bin script.

Use WP-CLI

To keep your scripts as lean and mean as possible, we highly encourage you to leverage WP-CLI, an awesome community framework.

When using WP-CLI, all you’ll need to write for your bin script is what’s called a “command.” Your command accepts zero or more arguments and performs a bit of logic. You won’t need to switch to the proper blog, handle arguments, etc., as all of that is done automatically.

Check out the great documentation on how to write a command. When you write commands for VIP, there are a few things to keep in mind:

  • You should extend the WPCOM_VIP_CLI_Command class provided in the development helpers, which includes helper functions like stop_the_insanity(). Do this instead of extending WP_CLI_Command.
  • Make sure you require the file that contains your new command in your functions.php file
  • Make sure you only include the command if WP_CLI is defined and true

Here’s an example of what might be in your functions file:

// CLI scripts
if ( defined( 'WP_CLI' ) && WP_CLI ) {
	require_once MY_THEME_DIR . '/inc/class-mycommand1-cli.php';
	require_once MY_THEME_DIR . '/inc/class-mycommand2-cli.php';
}

Once you’ve written your command and tested it throughly in your local environment, you can commit it to your theme.

When you’ve done so, open a ticket with us with explanation of what you’re trying to accomplish. We’ll review, test, and run it.

Best Practices

Again, it can be easy to make a minor mistake with your script that causes a lot of pain. We encourage you to do the following:

  • Comment well and provide clear usage instructions. It’s important to be very clear about what each part is doing and why — commenting each step of your logic is a good sanity check. Comments are especially helpful when something maybe doesn’t work as intended and we need to debug to figure out why.
  • If your script is calling wp_update_post() or importing posts, make sure to define( 'WP_IMPORTING', true ); at the top of your subcommand. This will ensure only the minimum of extra actions are fired.
  • Allow for varying levels of verbosity. Similarly, provide a summary at the end with all results of the script.
  • It’s a good idea to default your script to do a test run without affecting live data. Add an argument to allow a “live” run. This way, we can compare what the actual impact is versus the expected impact.
    A good way to do this is to do:

    $dry_mode = ! empty ( $assoc_args['dry-run'] );
    if( ! $dry_mode ) {
    	WP_CLI::line( " * Removing {$user->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->user_login} ( {$user->ID} )... " );
    }

    If your code modifies existing data we will ask for a dry run option so that we can confirm with you that things are good

  • Check your CLI methods have the necessary arguments. WP CLI passes 2 arguments, $args and $assoc_args to each command, you’ll need these to implement dry run options
  • If you’re modifying lots of data on a live site, make sure to include sleep() in key places. This will help with load associated with cache invalidation and replication. We also recommend using the WPCOM_VIP_CLI_Command methods stop_the_insanity() to clear memory after having processed 100 posts. If you are processing a large number of posts using the start_bulk_operation() and end_bulk_operation() class methods to disable functionality that is often problematic with large write operations.
  • Direct Database Queries will probably break in unexpected ways. Use core functions as much as possible, using direct SQL queries, (specifically those that do UPDATES or DELETES) will cause the caches to be invalid. In some cases if a direct SQL query is required, only do SELECT. Do any write operation using the core WordPress functionality. You may want to remove certain hooks from wp_update_post or do other actions to get the desired behaviour. In some rare contexts, a direct SQL query could be a better choice, but it must be followed by clean_post_cache().
  • When in doubt, ask us!

FAQ

How do I modify all the posts?

Without a no-LIMIT query, it can be confusing how you would modify all your posts. The problem is that a no-LIMIT query just won’t work in most situations. If the query takes longer than 30 seconds, it will timeout and fail. The solution is use smaller queries and page through the results.

For example:

<?php

class Test_CLI_Command extends WPCOM_VIP_CLI_Command {

    /**
     * CLI command that takes a metakey (required) and post category (optional)
     * and publishes all pending posts once they have have had their metakeys updated.
     *
     * @subcommand update-metakey
     * @synopsis --meta-key=<metakey> [--category=<category>] [--dry-run]
     *
     */
    public function update_metakey( $args, $assoc_args ) {
        $this->start_bulk_operation(); // Disable term counting, Elasticsearch indexing, and PushPress

        $posts_per_page = 100;
        $paged = 1;
        $count = 0;

        // Meta key value is required, otherwise an error will be returned.
        if ( isset( $assoc_args['meta-key'] ) ) {
            $meta_key = $assoc_args['meta-key'];
        } else {
            /*
             * Caution: calling WP_CLI::error stops the execution of the command.
             * Use WP_CLI::error only in case you want to stop the execution. Use
             * WP_CLI::warning or WP_CLI::line for non-blocking errors.
             */
            WP_CLI::error( 'Must have --meta-key attached.' );
        }

        // Category value is optional.
        if ( isset( $assoc_args['category'] ) ) {
            $cat = $assoc_args['category'];
        } else {
            $cat = '';
        }

        // If --dry-run is not set, then it will default to true.
        // Must set --dry-run explicitly to false to run this command.
        if ( isset( $assoc_args['dry-run'] ) ) {
            /*
             * passing `--dry-run=false` to the command leads to the `false` value being
             * set to string `'false'`, but casting `'false'` to bool produces `true`.
             * Thus the special handling.
             */
            if ( 'false' === $assoc_args['dry-run'] ) {
                $dry_run = false;
            } else {
                $dry_run = (bool) $assoc_args['dry-run'];
            }
        } else {
            $dry_run = true;
        }

        // Let the user know in what mode the command runs.
        if ( $dry_run ) {
            WP_CLI::line( 'Running in dry-run mode.' );
        } else {
            WP_CLI::line( 'We're doing it live!' );
        }

        do {
            $posts = get_posts( array(
                'posts_per_page' => $posts_per_page,
                'paged'          => $paged,
                'category'       => $cat,
                'post_status'    => 'pending',
            ));

            foreach ( $posts as $post ) {
                if ( ! $dry_run ) {
                    update_post_meta( $post->ID, $meta_key, 'true' );
                    wp_update_post( array( 'post_status' => 'publish' ) );
                }
                $count++;
            }

            // Pause.
            WP_CLI::line( 'Pausing for a breath...' );
            sleep(3);

            // Free up memory.
            $this->stop_the_insanity();
            /*
             * At this point, we have to decide whether or not to increase the value of $paged
             * variable. In case a value which is being used for querying the posts (like post_status
             * in our example) is being changed via the command, we should keep the WP_Query starting
             * from the beginning in every iteration. If the any value used for querying the posts
             * is not being changed, then we need to update the value in order to walk through all the posts.
             */
            // $paged++;

        } while ( count( $posts ) );

        if ( false === $dry_run ) {
            WP_CLI::success( sprintf( '%d posts have successfully been published and had their metakeys updated.', $count ) );
        } else {
            WP_CLI::success( sprinf( '%d posts will be published and have their metakeys updated.', $count ) );
        }
        $this->end_bulk_operation(); // Trigger a term count as well as trigger bulk indexing of Elasticsearch site.
    }

    /**
     * CLI command that takes a taxonomy (required) and updates terms in that
     * taxonomy by removing the "test-" prefix.
     *
     * @subcommand update-terms
     * @synopsis --taxonomy=<taxonomy> [--dry_run]
     *
     */
    public function update_terms( $args, $assoc_args ) {
        $count = 0;

        $this->start_bulk_operation(); // Disable term counting, Elasticsearch indexing, and PushPress.

        // Taxonomy value is required, otherwise an error will be returned.
        if ( isset( $assoc_args['taxonomy'] ) ) {
            $taxonomy = $assoc_args['taxonomy'];
        } else {
            /*
             * Caution: calling WP_CLI::error stops the execution of the command.
             * Use WP_CLI::error only in case you want to stop the execution. Use
             * WP_CLI::warning or WP_CLI::line for non-blocking errors.
             */
            WP_CLI::error( 'Must have a --taxonomy attached.' );
        }

        // If --dry-run is not set, then it will default to true.
        // Must set --dry-run explicitly to false to run this command.
        if ( isset( $assoc_args['dry-run'] ) ) {
            /*
             * passing `--dry-run=false` to the command leads to the `false` value being
             * set to string `'false'`, but casting `'false'` to bool produces `true`.
             * Thus the special handling.
             */
            if ( 'false' === $assoc_args['dry-run'] ) {
                $dry_run = false;
            } else {
                $dry_run = (bool) $assoc_args['dry-run'];
            }
        } else {
            $dry_run = true;
        }

        // Let he user know in what mode the command runs.
        if ( $dry_run ) {
            WP_CLI::line( 'Running in dry-run mode.' );
        } else {
            WP_CLI::line( 'We're doing it live!' );
        }

        $terms = get_terms( array( 'taxonomy' => $taxonomy ) );

        foreach ( $terms as $term ) {
            if ( ! $dry_run ) {
                wp_update_term( $term->term_id, $term->taxonomy, array(
                    'name' => str_replace( 'test ', '', $term->name ),
                    'slug' => str_replace( 'test-', '', $term->slug ),
                ) );
            }
            $count++;
        }

        $this->end_bulk_operation(); // Trigger a term count as well as trigger bulk indexing of Elasticsearch site

        if ( false === $dry_run ) {
            WP_CLI::success( sprintf( '%d terms were updated.', $count ) );
        } else {
            WP_CLI::success( sprintf( '%d terms will be updated.', $count ) );
        }
    }
}

WP_CLI::add_command( 'test-command', 'Test_CLI_Command' );

JavaScript

Tips and best practices for incorporating JavaScript files into your site:

  • Use wp_register_script() and wp_enqueue_script() to initialize your Javascript files. This ensures compatibility with other plugins and avoids conflicts.
  • As with everything, prefix your script slugs when registering or enqueuing them.
  • WordPress core includes jQuery so it’s not necessary to include it in your plugin or theme. Doing so can lead to conflicts and unexpected bugs.
  • The jQuery version that is packaged with WordPress is in compatibility mode. This means that jQuery() needs to be explicitly used, not the $() short form.
  • Avoid usage of scripts not hosted on WordPress.com. There is a comprehensive list of scripts included. All WordPress.com scripts are served by a CDN.
  • If you need to register a script that is not part of WordPress, or your theme, make sure to use a packed version if available and make sure that their servers are up for the traffic you will request from them. Fail gracefully.
  • Adding a version to the script when enqueueing isn’t necessary. When we concatenate files, we append the filemtime of the last updated file as a cache buster. If the file isn’t concatenated for some reason, we just use the file’s mtime.

Debugging JavaScript

If you encounter a problem related to JavaScript, there are a number of recommendations and available resources for troubleshooting the issue and making sure you have complete information if you open a ticket to report it to us.

  • If you’re using any kind of ad blocker or other browser plugin that might modify JS, try turning it off and seeing if the results change
  • Use your browser’s built-in developer tools, or freely available developer tool add-ons, to view details of the JS running on your site and any errors it might be producing:
  • Add debugging code to your JavaScript to confirm it’s working as expected; display the contents of key variables, alert when the code reaches a critical point in its logic, etc.
  • The Google Chrome Developer Tools in particular have some very useful tools for debugging JavaScript issues: inspecting sources, setting breakpoints, controlling execution, etc.

If you do report the issue to us, please make sure to include as much of the information revealed by these tools as possible in your request.

Ready to get started?

Tell us about your needs

Let us lead the way. We’ll help you select a top tier development partner. We’ll train your developers, operations, infrastructure, and editorial teams. We’ll coarchitect your deployment processes. We will provide live support for peak events. We’ll help your people avoid dark alleys and blind corners, and reduce wasted cycles.