Blog

How to make bulk changes to WordPress Woocommerce product attributes with PHP

Hello!

Sometimes its necessary to make site-wide changes to WordPress posts in order to save time and programmatically propagate changes without having to edit each post one at a time.

For bulk manipulation of WordPress data, it is sometimes effective to write a command line PHP script to hook into your WordPress environment and execute changes. One of the main advantages with executing these types of functions on the command line as opposed to a web based interaction is you are not subject to the same execution and other timeout restrictions for web based requests. This means that a command line PHP solution can run longer, which is sometimes needed for complicated efforts.

In our example, we will break down the PHP Command line script, how to safely and securely hook into WordPress to take advantage of all the built in functions, and update all the products in our WordPress Woocommerce store. This script could be easily modified to accommodate regular WordPress posts or perhaps even custom post types.

Initialize WordPress environment with PHP on the command line

What we want to do is initialize the WordPress environment in the script. This will allow us to perform actions and trigger many of the built-in WordPress functions that we normally would enjoy. Additionally we want to place this script outside of the web-accessible environment, for security.

error_reporting(E_ALL); ini_set('display_errors', 1);
if( php_sapi_name() !== 'cli' ) {
    die("Meant to be run from command line");
}

First of all we are forcing error reporting. This will allow for more visibility when improving and testing the script. Second we want the script to flat out exit if it were accessed via the web. Since we moved the script outside of the web accessible directory this isn’t possible anyways, but the if-condition above makes sure of that. Sometimes its not possible to place files outside of the web document root (perhaps a restriction of your web hosting provider).

define( 'BASE_PATH', find_wordpress_base_path()."/" );
define('WP_USE_THEMES', false);
global $wp, $wp_query, $wp_the_query, $wp_rewrite, $wp_did_header;
require(BASE_PATH . 'wp-load.php');

The above snippet is how we initialize the WordPress environment. You can see on the first line we are triggering a function find_wordpress_base_path(). This function, described below, is designed to locate and return the WordPress directory.

    function find_wordpress_base_path() {
        $dir = dirname(__FILE__) . '/public_html';
        do {
            //it is possible to check for other files here
            if( file_exists($dir."/wp-config.php") ) {
                return $dir;
            }
        } while( $dir = realpath("$dir/..") );
        return null;
    }

If the script is going to be located one level above the public_html directory, line 2 of the function should have that set. Change public_html to whatever the directory name is in order for it to work.

Use PHP to loop through all Woocommerce products

Now that we have initialized the WordPress environment, we want to build an array of all the products and loop through each one :

$args = array(
        'post_type' => 'product',
        'posts_per_page' => -1,
    );
$products_array = get_posts($args);

if (!empty($products_array)) {
    echo 'doing!';
    // loop through each product
    foreach ($products_array as $product)
    {
        echo "doing product : " . $product->ID . "\n";
        shift8_update_price($product->ID);
    }

}

The arguments and the get_posts hopefully seems familiar to you. We are building an array of all the posts of type product and storing it in an array called products_array.

If the array isn’t empty, we want to loop through each item in the array with a foreach loop. We then are triggering a custom function, described below called shift8_update_price.

This is where you can hopefully see that we can modify the arguments passed to get_posts with basically anything we want. Custom post types would simply need the post_type argument adjusted. Additional filters can be passed to the arguments array if needed. You can read more about the get_posts function to learn more about what you can do.

Bulk edit Woocommerce product attributes

Woocommerce product attributes, like other things in WordPress such as Widget data, are stored as serialized data. If you were to save the data you want to apply to all products once, then view the post meta content as it is in the database , it might look something like this :

a:11:{s:15:"calculator_type";s:4:"area";s:9:"dimension";a:4:{s:7:"pricing";a:7:{s:7:"enabled";s:2:"no";s:5:"label";s:0:"";s:4:"unit";s:2:"cm";s:10:"calculator";a:1:{s:7:"enabled";s:2:"no";}s:9:"inventory";a:1:{s:7:"enabled";s:2:"no";}s:6:"weight";a:1:{s:7:"enabled";s:2:"no";}

This means you need to copy the data, saved to that one product, and store it in a variable to be applied across the board to all products with our script :

$data_save = unserialize('a:11:{s:15:"calculator_type";s:4:"area";s:9:"dimension";a:4:{s:7:"pricing";a:7:{s:7:"enabled";s:2:"no";s:5:"label";s:0:"";s:4:"unit";s:2:"cm";s:10:"calculator";a:1:{s:7:"enabled";s:2:"no";}s:9:"inventory";a:1:{s:7:"enabled";s:2:"no";}";

This is a snippet of the data we want to save. Lets run through the entire function that is triggered on each product in the above foreach loop :

function shift8_update_price($post_id) {
    $post_type = get_post_type($post_id);

    if ($post_type == 'product') {
        $product = wc_get_product($post_id);
        $data_save = unserialize('a:11:{s:15:"calculator_type";s:4:"area";s:9:"dimension";a:4:{s:7:"pricing";a:7:{s:7:"enabled";s:2:"no";s:5:"label";s:0:"";s:4:"unit";s:2:"cm";s:10:"calculator";a:1:{s:7:"enabled";s:2:"no";}s:9:"inventory";a:1:{s:7:"enabled";s:2:"no";}');
        $data_save2 = unserialize('a:0:{}');

        if (!WC_Price_Calculator_Product::pricing_calculator_enabled( $product )) {
            update_post_meta($post_id, '_area', '');
            update_post_meta($post_id, '_volume', '');
            update_post_meta($post_id, '_wc_measurement_price_calculator_min_price', '');
            update_post_meta($post_id, '_wc_price_calculator', $data_save);
            update_post_meta($post_id, '_wc_price_calculator_pricing_rules', $data_save2);
        }
    }
}

In the above example, we are essentially setting all of our products to use the pricing calculator to calculate the unit of measurement applied to each product. In this case, we are selling tiles that are sold by the square foot. You can see that we are triggering the update_post_meta function 5 times, each to update different aspects of this requirement in order for it to work.

The key to remember here is you should be analyzing the data stored in the database when you manually update a product to accommodate this to see what happens via WordPress’ admin interface. Note the post_id you are editing, and then run the following mysql command before and after saving the change to note the differences and additional rows added :

select * from wp_postmeta where post_id = 4009\G;

Where 4009 is the post_id. You can then note the meta_key and meta_value of the items that changed after saving, and apply accordingly as we did in the update_post_meta function calls earlier.

Hopefully this post will get you more familiar with bulk data manipulation in WordPress, specifically Woocommerce. Again this type of logic and interaction can apply throughout WordPress, not just Woocommerce. Dealing with thousands of WordPress posts and custom post types can be cumbersome. Batch data manipulation is definitely something that would make anyone’s life easier!