Blog

Guide to Migrating Your Plugin to WooCommerce 2.0

WooCommerce 2.0 Beta 3

With the release of WooCommerce 2.0 becoming more imminent (well, maybe not quite imminent yet, but certainly drawing nearer), those of us with WooCommerce plugins need to start work on getting them ready for the big release. WooCommerce has never been one to hold back on making plugin-busting implementation changes with a new release, and they’re setting the bar quite high with 2.0. It’s chalk full of code changes and improvements plugin developers must be aware of and potential handle, and I’ve personally spent many hours already preparing my various WooThemes-listed plugins for WC 2.0, with yet more work ahead of me I’m sure. I figure with all the time I’ve already spent researching this stuff, there’s no need for you to have to do the same, and so I’m putting together this guide on migrating existing plugins to WooCommerce 2.0, which I plan to continue to update as I find new issues/fixes, and I hope you dear reader will do the same in the comment section below!

The intended audience for the guide would most likely be developers with custom plugins for clients which they want to get updated ahead of time, or at least be ready to update once 2.0 is released. I’d imagine other developers in the WooCommerce developers program will be aware of most of this already. The majority of the fixes and approaches I describe in this article are from actual code snippets that I’ve used to update my own WooThemes-listed plugins, and hence they are written to be both backwards-compatible with the current WooCommerce 1.6.6, and compatible with the upcoming WooCommerce 2.0. It also means that they cover only those issues I’ve encountered with my own plugins, and thus couldn’t hope to cover all potential issues. If you’re responsible for a custom plugin for a client or small number of clients and have more control over the environment in which they run, you can certainly simplify your plugin by targeting it solely for 2.0; though for the most part it’s not a whole lot more effort to be backwards compatible as well.

As always, I make no claims or guarantees about the correctness or efficacy of the code shown below, and if you use it you do so at your own risk. I won’t be held responsible if something blows up! WooCommerce 2.0 is also still very much a work in progress and a moving target, but I believe the fixes I’ve already come up with should hold as they come to a final release candidate.

Testing

The first and most important step is to test your plugin with the latest beta of WooCommerce, at time of writing called WooCommerce 2.0.0-beta3. Or, if you’re feeling lucky, just use the current head of the WooCommerce repository. With any luck your plugin will work just fine and your job will be done before it’s begun. Crack open a beer! (but don’t forget to continue testing with each beta and especially the final release)

Overview

If you’re still with me, then perhaps some part of your plugin is broken by the 2.0 beta. Well, you’re in good company. We’ll start with a listing of potential issues and then delve into them one-by-one:

  1. Products
  2. Sessions
  3. Order Item Data
  4. Payment Gateways
  5. Odds and Ends

Additional Resources

Also see the WooThemes article WooCommerce 1.6.6 -> 2.0 – Plugin and theme compatibility, which identifies some of the same issues/approaches I outline below, as well as some things I didn’t cover, especially with regards to themes.

Products

The first and most likely spot of trouble is with the product class.

Instantiating WC_Product

Products have received quite an overhaul in WooCommerce 2.0, and if you ever did new WC_Product() in your code you’re going to have to make a few changes. With 2.0 the WC_Product class is made abstract, meaning that to try and instantiate it is to be hit with a Fatal Error. The fix is to use the new product factory class to retrieve a new product instance, which you can do in a forwards/backwards compatible manner like so:

/**
 * Gets the identified product. Compatible with WC 2.0 and backwards
 * compatible with previous versions
 *
 * @param int $product_id the product identifier
 * @param array $args optional array of arguments
 *
 * @return WC_Product the product
 */
function my_plugin_get_product( $product_id, $args = array() ) {

  $product = null;

  if ( version_compare( WOOCOMMERCE_VERSION, "2.0.0" ) >= 0 ) {
    // WC 2.0
    $product = get_product( $product_id, $args );
  } else {

    // old style, get the product or product variation object
    if ( isset( $args['parent_id'] ) && $args['parent_id'] ) {
      $product = new WC_Product_Variation( $product_id, $args['parent_id'] );
    } else {
      // get the regular product, but if it has a parent, return the product variation object
      $product = new WC_Product( $product_id );
      if ( $product->get_parent() ) {
        $product = new WC_Product_Variation( $product->id, $product->get_parent() );
      }
    }
  }

  return $product;
}

As you can see, we check for the current version of WooCommerce: if running under 2.0 or greater, we use the spiffy new get_product() function, otherwise we create a product or product variation as normal. This can be used as simply as:

$product = my_plugin_get_product( 15 );

Product Custom Meta Fields

The next product change we need to concern ourselves with is the handling of custom meta fields. Gone is the old product_custom_fields array (except for product variations!) from which all product wp_postmeta fields were available. In its place is the PHP magic method __get. This means that instead of say $product->product_custom_fields['_my_field'][0] we simply do: $product->my_field. Cool! Still, you have to make sure and make the change, and at least as of beta2 make sure you handle product variations correctly if you have a custom meta that you allow to be set in the parent product and potentially overridden in the child. Here’s a helper function to help you along if you want to support WC 1.6 as well as the new 2.0:

/**
 * Helper function to retrieve a custom field from a product, compatible
 * both with WC < 2.0 and WC >= 2.0
 *
 * @param WC_Product $product the product object
 * @param string $field_name the field name, without a leading underscore
 *
 * @return mixed the value of the member named $field_name, or null
 */
function my_plugin_get_product_meta( $product, $field_name ) {

  if ( version_compare( WOOCOMMERCE_VERSION, "2.0.0" ) >= 0 ) {

    // even in WC >= 2.0 product variations still use the product_custom_fields array apparently
    if ( $product->variation_id && isset( $product->product_custom_fields[ '_' . $field_name ][0] ) && $product->product_custom_fields[ '_' . $field_name ][0] !== '' ) {

      return $product->product_custom_fields[ '_' . $field_name ][0];
    }

    // use magic __get
    return maybe_unserialize( $product->$field_name );

  } else {

    // use product custom fields array

    // variation support: return the value if it's defined at the variation level

    if ( isset( $product->variation_id ) && $product->variation_id ) {
      if ( ( $value = get_post_meta( $product->variation_id, '_' . $field_name, true ) ) !== '' ) return $value;

      // otherwise return the value from the parent
      return get_post_meta( $product->id, '_' . $field_name, true );
    }

    // regular product
    return isset( $product->product_custom_fields[ '_' . $field_name ][0] ) ? $product->product_custom_fields[ '_' . $field_name ][0] : null;
  }
}

As you can see, regardless of the version, if $product is a variation, that variation is checked first, followed by its parent. You use this method in the following way:

$value = my_plugin_get_product_meta( $product, 'my_field' );

Perhaps not as snazzy as $product->my_field, but if you want/need that 1.6 compatibility, that’s the tradeoff.

There are a slew of other product changes of course, but nothing that’s given me problems in the plugins I’ve tested thus far.

Sessions

Next on the list is sessions. WooCommerce 2.0 no longer uses the PHP session_start function, instead it makes use of WordPress’ transients, which is great, unless your code happens to rely on $_SESSION. Of course one of my plugins did, so here’s a couple of functions to ease your migration to 2.0 and still support any 1.6 installs out there:

/**
 * Safely store data into the session. Compatible with WC 2.0 and
 * backwards compatible with previous versions.
 *
 * @param string $name the name
 * @param mixed $value the value to set
 */
private function my_plugin_session_set( $name, $value ) {

  global $woocommerce;

  if ( isset( $woocommerce->session ) ) {
    // WC 2.0
    $woocommerce->session->$name = $value;
  } else {
    // old style
    $_SESSION[ $name ] = $value;
  }
}

/**
 * Safely retrieve data from the session. Compatible with WC 2.0 and
 * backwards compatible with previous versions.
 *
 * @param string $name the name
 * @return mixed the data, or null
 */
function my_plugin_session_get( $name ) {

  global $woocommerce;

  if ( isset( $woocommerce->session ) ) {
    // WC 2.0
    if ( isset( $woocommerce->session->$name ) ) return $woocommerce->session->$name;
  } else {
    // old style
    if ( isset( $_SESSION[ $name ] ) ) return $_SESSION[ $name ];
  }
}

/**
 * Safely remove data from the session. Compatible with WC 2.0 and
 * backwards compatible with previous versions.
 *
 * @param string $name the name
 */
function my_plugin_session_delete( $name ) {

  global $woocommerce;

  if ( isset( $woocommerce->session ) ) {
    // WC 2.0
    unset( $woocommerce->session->$name );
  } else {
    // old style
    unset( $_SESSION[ $name ] );
  }
}

Hopefully you’ll agree these are simple enough that they don’t require a lengthy explanation or usage example.

Order Item Data

Order Item Data is no longer stored directly in the wp_postmeta table as meta attached to the order post row and serialized in an array, which is actually great news and should make for much more efficient and larger possible stores. Order items and item data are now stored in wp_woocommerce_order_items and wp_woocommerce_order_itemmeta respectively. Order items and item meta data can be added using the new functions woocommerce_add_order_item() and woocommerce_add_order_item_meta():

$item_id = woocommerce_add_order_item( $order_id, array(
  'order_item_name' => 'Product Title',
  'order_item_type' => 'line_item'
) );

woocommerce_add_order_item_meta( $item_id, 'my_item_meta', 'my_item_meta_value' );

There’s other new functions to delete items and meta, and update, etc. Also if you need to write custom queries against order items or item meta this will be much more efficient/possible now.

If you used the WC_Order_Item_Meta class previously, there have been a couple of changes therein:

  1. WC_Order_Item_Meta::add() is gone
  2. As is WC_Order_Item_Meta::new_order_item()
  3. The internal meta storage has changed to key-value pairs, rather than pairs of ‘meta_key’ and ‘meta_value’. (take a look at the class if you use this, it’ll make sense)

Finally, of interest if you’re in the business of adding custom item meta to the cart (say with custom configurable products on the frontend) the woocommerce_order_item_meta action is gone, and in its place we have: woocommerce_add_order_item_meta. You can support both actions to get backwards/forwards compatibility:

add_action( 'woocommerce_order_item_meta', 'my_plugin_order_item_meta', 10, 2 ); // WC < 2.0
add_action( 'woocommerce_add_order_item_meta', 'my_plugin_add_order_item_meta', 10, 2 ); // WC >= 2.0

The implementations of my_plugin_order_item_meta() and my_plugin_add_order_item_meta() will probably be largely similar, you’ll just want to use the new woocommerce_add_order_item_meta() function mentioned above in the WC 2.0 version.

Deprecated order_item_meta Class is Gone

The deprecated order_item_meta class is now removed, so if you’re still using that class in your code, make sure to replace it with WC_Order_Item_Meta otherwise you’ll get a fatal error.

Payment Gateways

Don’t know how many of you out there will have payment gateways you’ll be needing to update, but just in case, there have been a few changes to be aware of.

Smart Loading

Payment gateways in WC 2.0 have gotten a treatment similar to what Shipping Methods received in WooCommerce 1.5.7. The big change is that rather than being instantiated on every request, they are now loaded only when needed. This one change is supposedly responsible for quite a saving of resources and server performance. Which is all well and good, unless of course your payment gateway actually needs to take some actions anywhere other than the admin settings page, checkout/pay pages, and API call. The solution I’ve used and developed alongside fellow WC developer Max Rice is to create a lightweight “main” class which is always instantiated and which manages and registers the payment gateway class responsible for communicating with the gateway, and registers any actions or filters where the payment gateway class might be needed.

No real applicable code samples or examples here, however those of you with redirect gateways, or any gateways that registers a WooCommerce API hook, you’ll need to ensure that the action you register contains the gateway class name. Here’s a sample API hook added from within a payment gateway class:

add_action( 'woocommerce_api_' . strtolower( get_class( $this ) ), array( $this, 'gateway_response' ) );

Without this your gateway will not not be created and will not be called to handle that API request. Of course you can use the same “main” class pattern described above to register any arbitrary API request handler you may require, and indeed I have to do this with two of my own gateways.

Saving Settings

The other gateway change is much simpler, there’s a new action required to save the gateway settings. You can easily support both WC 1.6 and 2.0+ by just adding the new hook alongside the existing:

if ( is_admin() ) {
  add_action( 'woocommerce_update_options_payment_gateways',              array( $this, 'process_admin_options' ) );  // WC < 2.0

  add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, array( $this, 'process_admin_options' ) );  // WC >= 2.0
}

Odds and Ends

And then there’s all the little odds and ends we’ve run in to.

Admin Help Tip Image Elements

The Help Tip image has changed, so be sure to add a width/height of 16 otherwise it will look funky and large:

<img class="help_tip" width="16" height="16" ...

Media Browser

Although not strictly related to WooCommerce 2.0 it’s recommended and a good practice to use the new WordPress 3.5 media browser.

Admin Product Data Tab CSS

The product data tabs have changed from a horizontal layout in 1.6:

product-data-tabs-horizontal

To a much more efficient vertical layout in 2.0:

product-data-tabs-vertical

Requiring some annoying CSS changes if you want to have your tabs looking correct in both 1.6 and 2.0+. Here’s the best Max and I have come up with:

add_action( 'woocommerce_product_write_panel_tabs', 'my_plugin_product_tabs_panel_tab' );

/**
 * Adds the "Tabs" tab to the Product Data postbox in the admin product interface
 */
function my_plugin_product_tabs_panel_tab() {
	echo '<li class="my_plugin_tab"><a href="#my_plugin_tabs">' . __( 'My Plugin' ) . '</a></li>';
}

add_action( 'woocommerce_product_write_panels', 'my_plugin_product_rates_panel_content' );

/**
 * Adds the tab panel to the Product Data postbox in the product interface
 */
function my_plugin_product_rates_panel_content() {

  $tab_icon = $this->plugin_url() . '/assets/product-data-icon.png';

  if ( version_compare( WOOCOMMERCE_VERSION, "2.0.0" ) >= 0 ) {
    $style = 'padding:5px 5px 5px 28px;background-image:url(' . $tab_icon . ');background-repeat:no-repeat;background-position:5px 7px;';

    $active_style = '';
  } else {
    $style = 'padding:9px 9px 9px 34px;line-height:16px;border-bottom:1px solid #d5d5d5;text-shadow:0 1px 1px #fff;color:#555555;background-image:url(' . $tab_icon . ');background-repeat:no-repeat;background-position:9px 9px;';

    $active_style = '#woocommerce-product-data ul.product_data_tabs li.my_plugin_tab.active a { border-bottom: 1px solid #F8F8F8; }';
  }
  ?>
  <style type="text/css">
    #woocommerce-product-data ul.product_data_tabs li.my_plugin_tab a { <?php echo $style; ?> }
    <?php echo $active_style; ?>
  </style>
  ...

Conclusion

Well, that about does it. As I said in the intro, any more refinements I come up with I’ll do my best to update/add here, and if you find anything feel free to mention it in the comments. Hopefully WooCommerce 2.0 will be released before all too long and this guide will be one for the ages.

21 Comments

  • Brian Krogsgard 3 months ago Reply

    Justin,

    Excellent post. Thank you very, very much. Bookmarking this and I’m sure I’ll be referencing quite a bit the next couple of weeks!

    • justin 3 months ago Reply

      Great! Hope it helps!

  • Kathy 3 months ago Reply

    Wow, those are some big changes coming. Thanks for sharing your research. I really, really hope I don’t have too much to update yet. ;) I have been developing a custom extension for a client and that’s been explicitly on 2.0beta because it required items that weren’t available yet, so that shouldn’t need any updates, but I should double check the official /for-sale one.

    • justin 3 months ago Reply

      Yeah, hopefully it’s not too bad for you. Definitely make sure and test Name Your Price though

  • Lee 3 months ago Reply

    Great post – really useful!

    I’m trying out your implementation of my_plugin_get_product_meta() and just thought I’d note something to watch out for. If you’re storing serialised data then your function will return the serialised string on WC < 2.0, but the unserialised data (An array in my case) on WC 2.0…

    • justin 3 months ago Reply

      Hey Lee, thanks for stopping by and for the feedback. That’s a good point, and I hadn’t tried retrieving serialized data like that. I’ve added a maybe_unserialize() call to the example function; but perhaps we should try getting that included within the magic __get() in WooCommerce core, that might be a good addition.

      • Lee 3 months ago Reply

        Actually – I think that code path is doing getting unserialized back due to the underlying WP call – not sure off the top of my head though …

  • Daniel Espinoza 3 months ago Reply

    Great post Justin! Thanks for all the ‘meat’ and helpful code examples.

    • justin 3 months ago Reply

      You got it Daniel, hope it helps! If you come across any issues or improvements to what I did, let me know and I’ll incorporate into the article.

  • Ken 3 months ago Reply

    Some other things have run into:

    1) WC_Order get_items() returns items differently. In 1.6 the first item was index 0, now the first item is indexed by some other number (item_id from database table). If iterating this is probably not an issue, but anything that assumed first item in order was 0, second was 1, etc will break.

    2) For order item data, you mention the change in format from meta_name/meta_value to a new key=>value pairing. The value is stored in an array so meta_value will now be value[0]. (This is now consistent with how WC_Order class returns stuff)

    3) Taxes have changed. You mentioned ‘order_item_type’ => ‘line_item’ for how line items are stored. There is also an order_item_type of tax. This is a change from how 1.6 stored tax info on orders (there used to be an _order_taxes meta field).

    4) One pain was that the WC_Order class renamed the product id of the order items from ‘id’ to ‘product_id’. You’ll need a compatibility function.

    • justin 3 months ago Reply

      I haven’t had a chance to go through these points in detail for myself and update the relevant portions of the guide, but it looks like really valuable feedback so I wanted to approve this comment right away in case it helps anyone out. Thanks Ken!

  • razzaq 3 months ago Reply

    Ken,

    thanks for this post.

    I am still not sure, how much time it will take me this. but i am going to start with a dev site.

  • amarstyle 3 months ago Reply

    Hi,

    can you please tell me, Where do I insert the session code?

    • justin 3 months ago Reply

      Hey, (I removed the example session code so your comment wouldn’t be too long). You would add that code to any .php file within the plugin you’re working on and need to use sessions within

  • Tim 3 months ago Reply

    Does anyone have any documentation as to how-to create and validate activation keys with the new extension system in place on woo commerce?

    Thank you for your time,

    Tim

    • justin 3 months ago Reply

      Hey Tim, if you’re referring to the WooThemes license keys that are required to receive updates and support from WooThemes, all you need to do is log into your WooThemes account on http://www.woothemes.com and click the tab “My Licenses” to view your purchased keys. They have a good writeup there explaining how to enter the keys on your WordPress site which can be found at: https://www.woothemes.com/my-account/my-licenses/#faq Hope this helps!

  • felix 2 months ago Reply

    Hi. Can anyone help me (please)? I have a custom plugin, a payment gateway, which will not load in WC 2.0. It was built for 1.6.6. I have spent days trying to understand Section 4 above. I understand that there are two changes regarding payment gateways from the old version to the new. I was able to make the simple change having to do with the Save hook (which enabled me to actually get the plugin installed), but the dynamic load thing has got me stumped. I would post the code, but it’s rather long (can I do that here?).

    • Justin Stern 2 months ago Reply

      Hey Felix, it can be quite a pain trying to migrate to 2.0. If your payment gateway isn’t loading at all then there could be some sort of fatal error within it, try enabling WordPress debugging and watch the error log for clues

  • gimond 2 months ago Reply

    Thanks a lot for all this.
    I’m new to Woocommerce and i’m having a hard time doing complex operations due to the lack of documentation.
    Your get_product function have been incredibly useful to me.
    Thanks again.

  • henri 1 month ago Reply

    Thanks for the great overview. Might be a bit off-topic, but as you guys here seem to lead the way I’ll still shoot it out. Has anyone here tried implementing API calls based products population with Woocommerce? And I don’t mean just making a call, pulling in the products data and updating the database. I’m thinking about different prices for different customers where info is to be pulled from remote system.

    • Justin Stern 30 weeks ago Reply

      Hey Henri, it’s a pretty interesting question, and it can be done, indeed I’ve seen this sort of thing with shops that sell jewelry or precious metal where the product price depends on the current market spot price. Conceptually it’s fairly simple, though the implementation can be complex. WooCommerce provides a filter named 'woocommerce_get_price' that can be used to modify the price of any product “on the fly”. So the question would be whether you wanted to dynamically grab the prices “as needed” through that filter, or have some other process that runs say once a day and modifies the product price in the database (given by the WordPress post meta named _price). Both approaches will have their own benefits and drawbacks, and if you were to go with the “dynamic” approach I’d recommend at least caching the price data (say in a transient so you’re not hitting that external system 15 times for each page load).

Submit a Comment