WC 2.7 extension compatibility examples

Our team have been looking at updating some of our extensions for 2.7 compatibility and we thought it would be good to share our notes in doing so. The first bunch we reviewed were EU VAT, Shipment Tracking, and Product Add-ons. Our notes are below.

Identifying issues

To test these extensions, we first needed to install them alongside 2.7 beta 1 and ensure WP_DEBUG mode was enabled so notices were logged and visible.

In testing, we were mainly on the look out for notices, warnings, and (unlikely) fatal errors. To capture notices and view them on-site, you can use the Query Monitor plugin.

If you also enable WP_DEBUG_LOG you can also see notices as they appear via your terminal by running the command:

tail -f wp-content/debug.log

Supported versions

We chose to officially provide support for 2.6.x and 2.7.x. 2.6 has been out since June 2016 making it well over 6 months old. Testing 2.6/2.7 in our case was just a case of switching between the master and release/2.6 branches of WooCommerce core.

Updating EU VAT extension

After activating I immediately saw two warnings:

[09-Jan-2017 11:01:33 UTC] PHP Notice:  id was called incorrectly. Order properties should not be accessed directly. Please see Debugging in WordPress for more information. (This message was added in version 2.7.) in /srv/www/wordpress-default/wp-includes/functions.php on line 4091
[09-Jan-2017 11:01:33 UTC] PHP Notice:  billing_country was called incorrectly. Order properties should not be accessed directly. Please see Debugging in WordPress for more information. (This message was added in version 2.7.) in /srv/www/wordpress-default/wp-includes/functions.php on line 4091

These are caused by accessible properties directory on an object rather than going through the CRUD methods.

Query monitor shows the call stack and shows which method in the EU VAT plugin was calling these properties:

This showed that the WC_EU_VAT_Number::formatted_billing_address method needed an update and needed to switch to the get_ methods.

I also noticed this plugin uses update_post_meta calls directly to update the VAT Number. This should also be replaced by CRUD methods.

To avoid lots of conditional logic inline in this case, I decided to put my 2.7.x logic in the same class, but with dedicated methods to keep the source as clean as possible where possible. When I did do things inline, I either use a version_compare statement, or is_callable.

In general, using is_callable is preferred over method_exists calls because is_callable also checks method scope. This ensures the method can actually be called from your code, rather than if the method simply exists.

I’ve included a few examples of how I handled certain cases below.


Throughout the admin class it did checks on the billing country like this:

// We only need this box for EU orders
if ( ! in_array( $theorder->billing_country, WC_EU_VAT_Number::get_eu_countries() ) ) {
echo wpautop( __( 'This order is out of scope for EU VAT.', 'woocommerce-eu-vat-number' ) );
return;
}

I moved the in_array check to a new method called is_eu_order in this case.

https://gist.github.com/mikejolley/b40347215149791a521a3df1a8ad0c91


For order data saving during checkout, the 2.6.x version uses the following hooks:

add_action( 'woocommerce_checkout_update_order_meta', array( __CLASS__, 'update_order_meta' ) );
add_action( 'woocommerce_checkout_update_user_meta', array( __CLASS__, 'update_user_meta' ) );
add_action( 'woocommerce_refund_created', array( __CLASS__, 'refund_vat_number' ) );

These update meta calls I left in place for 2.6, wrapped with a version check to run in 2.6.x. For 2.7, I used newer hooks which allow dealing with the CRUD objects directly:

if ( version_compare( WC_VERSION, '2.7', '<' ) ) {
add_action( 'woocommerce_checkout_update_order_meta', array( __CLASS__, 'update_order_meta' ) );
add_action( 'woocommerce_checkout_update_user_meta', array( __CLASS__, 'update_user_meta' ) );
add_action( 'woocommerce_refund_created', array( __CLASS__, 'refund_vat_number' ) );
} else {
add_action( 'woocommerce_checkout_create_order', array( __CLASS__, 'set_order_data' ) );
add_action( 'woocommerce_checkout_update_customer', array( __CLASS__, 'set_customer_data' ) );
add_action( 'woocommerce_create_refund', array( __CLASS__, 'set_refund_data' ) );
}

The old callbacks could then be left alone. Here is an example of one of the callbacks I updated with the new hooks vs what it was originally.

/**
* Stores VAT Number to customer profile
*
* @param int $user_id
*/
public static function update_user_meta( $user_id ) {
if ( $user_id && self::$data['vat_number'] ) {
update_user_meta( $user_id, 'vat_number', self::$data['vat_number'] );
}
}

/**
* Save VAT Number to the customer during checkout (WC 2.7.x).
*
* @param WC_Customer $customer
*/
public static function set_customer_data( $customer ) {
$customer->update_meta_data( 'vat_number', self::$data['vat_number'] );
}

Notice the set_customer_data method updates the object directly. WordPress passes a reference to the $customer object via the woocommerce_checkout_update_customer action. By setting the meta data here, I don’t need to save the object myself. It’s done by core 🙂


I found several calls in methods to:

WC()->customer->get_country()

This has been deprecated in favour of get_billing_country(). To replace this without lots of conditionals, I just stored it to a variable like this:

$billing_country = is_callable( array( WC()->customer, 'get_billing_country' ) ) ? WC()->customer->get_billing_country() : WC()->customer->get_country();


Finally, I noticed the admin code had lots of get_post_meta calls scattered around, and most of them dealt with the same meta keys and did the same logic. To simply this, and update it for 2.7, I moved them all into a single getter function which returns the data required.

https://gist.github.com/mikejolley/22b7cef11cd2d4660d87bf8ea295ad13

This allows all other methods to use this method rather than using get_post_meta or CRUD objects directly.

Updating Shipment Tracking extension

The following notes are from Justin Shreve.

Before I began testing the shipment tracking extension, I activated a small debugging plugin to provide backtraces when deprecation notices and doing_it_wrong messages are generated. WooCommerce 2.7 uses both of these to log when deprecated code is used. Here is a gist that you can save, upload, and activate. The Query Monitor plugin mentioned above will also show backtraces.

I also noted that the Shipment Tracking extension has some classes that provide compatibility with some of our other extensions, like the CSV Order Exporter, the XML Order Exporter, and our Print Invoices and Packing Lists extension . I installed all of these extensions so I could make sure that I test and update all potential code paths.

I began to test the various features of the extension while keeping my debug log open. I compiled a list of issues to fix based on each notice, backtrace provided.

The notices generated in my debug log were related to accessing the ID property of the $order object. In 2.7, all properties should be accessed with getters and setters. In the case of ID, get_id should be used.

Like in the VAT extension, is_callable is used to ensure the correct method is used, so 2.6 backwards compatibility can be maintained. In future versions, where 2.6 support is no longer needed, this line can be simplified.

As an example, here is the email_display method currently used in the Shipment Tracking extension.

public function email_display( $order, $sent_to_admin, $plain_text = null ) {
if ( $plain_text === true ) {
wc_get_template( 'email/plain/tracking-info.php', array( 'tracking_items' => $this->get_tracking_items( $order->id, true ) ), 'woocommerce-shipment-tracking/', $this->get_plugin_path() . '/templates/' );
} else {
wc_get_template( 'email/tracking-info.php', array( 'tracking_items' => $this->get_tracking_items( $order->id, true ) ), 'woocommerce-shipment-tracking/', $this->get_plugin_path() . '/templates/' );
}
}

To update the code, I defined a new variable holding the value of the order ID:

$order_id = is_callable( array( $order, 'get_id' ) ) ? $order->get_id() : $order->id;

And then I update all the $order->id calls to use it. Here is the finished function:

public function email_display( $order, $sent_to_admin, $plain_text = null ) {
$order_id = is_callable( array( $order, 'get_id' ) ) ? $order->get_id() : $order->id;
if ( true === $plain_text ) {
wc_get_template( 'email/plain/tracking-info.php', array( 'tracking_items' => $this->get_tracking_items( $order_id, true ) ), 'woocommerce-shipment-tracking/', $this->get_plugin_path() . '/templates/' );
} else {
wc_get_template( 'email/tracking-info.php', array( 'tracking_items' => $this->get_tracking_items( $order_id, true ) ), 'woocommerce-shipment-tracking/', $this->get_plugin_path() . '/templates/' );
}
}

I made similar updates around the code base, fixing the notices.

After I was confident I fixed the deprecated code, I began looking for the the following additional problems:

  • get_post_meta and update_post_meta calls.
  • Usage of get_post or get_post_type.
  • Direct SQL queries touching the posts table.

Per our CRUD introduction post, it’s important to standardize accessing and writing data so that we can make improvements in the future, and your extensions will be ready for them. These are not surfaced via notices however, so you will need to scan your code for these.

The tracking extension stores it’s data in a piece of meta, so there were a few get_post_meta calls, and update_post_meta calls to be updated. In 2.7, these custom meta keys (pieces of data that are not properties of the main WC_Order class), can be accessed using the get_meta function available on all CRUD classes, and saved using the update_meta_data and save_meta_data functions available on all CRUD classes. Since these methods were added in 2.7, we need to do a version check here too, to provide backwards compatibility. These statements can also be simplified in future versions.

In the save_tracking_items method of the tracking extension, the following line

became

if ( version_compare( WC_VERSION, '2.7', '<' ) ) { update_post_meta( $order_id, '_wc_shipment_tracking_items', $tracking_items ); } else { $order = new WC_Order( $order_id ); $order->update_meta_data( '_wc_shipment_tracking_items', $tracking_items );
$order->save_meta_data();
}

In the example above, we can still access the post meta directly orders are still in the posts table for the time being. For 2.7 and above, we will use the new class – preparing our extension for the future.

The shipment tracking extension exposes a new set of REST API endpoints for getting, creating, and deleting tracking info for orders. Some of the REST API code contained a few uses of get_post to check if an order ID is valid before trying to return tracking numbers for it. The WC_Order class in 2.7 will thrown an exception if an order is invalid, so this can be used instead.

I wrote a helper function for the API class that would allow me to do the check easier in multiple places:


/**
* Checks if an order ID is a valid order.
*
* @param int $order_id
* @return bool
*/
public function is_valid_order_id( $order_id ) {
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '2.7', '<' ) ) {
$order = get_post( $order_id );
if ( empty( $order->post_type ) || $this->post_type !== $order->post_type ) {
return false;
}
} else {
try {
new WC_Order( $order_id );
} catch ( Exception $e ) {
return false;
}
}
return true;
}

Using this helper function, I was able to update the multiple checks that looked like

$order = get_post( (int) $request['order_id'] );

if ( empty( $order->post_type ) || $this->post_type !== $order->post_type ) {
return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce-shipment-tracking' ), array( 'status' => 404 ) );
}

to


$order_id = (int) $request['order_id'];

if ( ! $this->is_valid_order_id( $order_id ) ) {
return new WP_Error( 'woocommerce_rest_order_invalid_id', __( 'Invalid order ID.', 'woocommerce-shipment-tracking' ), array( 'status' => 404 ) );
}

I also made sure that the REST API endpoints exposed were running on WP-API like the WooCommerce endpoints as of 2.6, and not the older legacy WooCommerce REST API. They were, so no updates were needed.

The extension didn’t make any direct queries, so all compatibility updates are finished.

I retested the extension on both 2.7 AND 2.6, to make sure my changes functioned correctly in both versions.

When adding support for a new version, don’t forget to test the old version again afterwards to ensure you’ve not used any non-existant methods! This is called regression testing.

There were no issues in the debug log, and the extension still worked correctly. Success!

Updating Product Add-ons extension

The following notes are from Claudio Sanches.

Since this extension handles meta data for Products and Orders, I invested time in creating new classes to handle compatibility for WooCommerce 2.6, thereby making modifications simpler and easier in the future when 2.6 support may be removed.

So I started testing and tracking the log file on my terminal, creating a new class for the legacy support, this is how I included the classes into the main Product Addons class:


/**
* Initializes plugin classes.
*/
public function init_classes() {
if ( is_admin() ) {
$this->init_admin();
}

include_once( dirname( __FILE__ ) . '/classes/class-product-addon-display.php' );
include_once( dirname( __FILE__ ) . '/classes/class-product-addon-cart.php' );
include_once( dirname( __FILE__ ) . '/classes/class-product-addon-ajax.php' );

// Handle WooCommerce 2.7 compatibility.
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '2.7.0', '<' ) ) {
include_once( dirname( __FILE__ ) . '/classes/legacy/class-product-addon-display-legacy.php' );
include_once( dirname( __FILE__ ) . '/classes/legacy/class-product-addon-cart-legacy.php' );
include_once( dirname( __FILE__ ) . '/classes/legacy/class-wc-addons-ajax.php' );

$GLOBALS['Product_Addon_Display'] = new Product_Addon_Display_Legacy();
$GLOBALS['Product_Addon_Cart'] = new Product_Addon_Cart_Legacy();
new WC_Addons_Ajax();
} else {
$GLOBALS['Product_Addon_Display'] = new Product_Addon_Display();
$GLOBALS['Product_Addon_Cart'] = new Product_Addon_Cart();
new Product_Addon_Cart_Ajax();
}
}

/**
* Initializes plugin admin.
*/
protected function init_admin() {
include_once( dirname( __FILE__ ) . '/admin/class-product-addon-admin.php' );

// Handle WooCommerce 2.7 compatibility.
if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '2.7.0', '<' ) ) {
include_once( dirname( __FILE__ ) . '/admin/legacy/class-product-addon-admin-legacy.php' );

$GLOBALS['Product_Addon_Admin'] = new Product_Addon_Admin_Legacy();
} else {
$GLOBALS['Product_Addon_Admin'] = new Product_Addon_Admin();
}
}

This makes the task of handling backward compatibility easier because it’s all dealt with in one place , and you can do any changes that you need inside the current class. You just need to copy and paste the old methods no longer needed to the *_Legacy class.

Here an example with the Product_Addon_Cart_Legacy class:


/**
* Product_Addon_Cart_Legacy class.
*/
class Product_Addon_Cart_Legacy extends Product_Addon_Cart {

/**
* Constructor.
*/
function __construct() {
parent::__construct();

// Add meta to order.
add_action( 'woocommerce_add_order_item_meta', array( $this, 'order_item_meta' ), 10, 2 );
}

/**
* add_cart_item function.
*
* @param array $cart_item
*/
public function add_cart_item( $cart_item ) {
// Adjust price if addons are set
if ( ! empty( $cart_item['addons'] ) && apply_filters( 'woocommerce_product_addons_adjust_price', true, $cart_item ) ) {

$extra_cost = 0;

foreach ( $cart_item['addons'] as $addon ) {
if ( $addon['price'] > 0 ) {
$extra_cost += $addon['price'];
}
}

$cart_item['data']->adjust_price( $extra_cost );
}

return $cart_item;
}

/**
* Add meta to orders.
*
* @param int $item_id
* @param array $values
*/
public function order_item_meta( $item_id, $values ) {
if ( ! empty( $values['addons'] ) ) {
foreach ( $values['addons'] as $addon ) {

$name = $addon['name'];

if ( $addon['price'] > 0 && apply_filters( 'woocommerce_addons_add_price_to_name', true ) ) {
$name .= ' (' . strip_tags( wc_price( get_product_addon_price_for_display ( $addon['price'], $values[ 'data' ], true ) ) ) . ')';
}

wc_add_order_item_meta( $item_id, $name, $addon['value'] );
}
}
}

In this legacy class  constructor I first called the parent class constructor (main class), and then I used the woocommerce_add_order_item_meta hook that was deprecated in 2.7 with the Product_Addon_Cart_Legacy->order_item_meta() callback for 2.6.x.

This legacy pattern allows us to reuse as much code across the 2.6.x and 2.7.x versions of the class as possible. Reuse code where possible; not only does this make supporting multiple versions more straightforward, it also makes your code more DRY, easier to test, and easier to read.

I found that Product_Addon_Cart_Legacy->add_cart_item() was also throwing deprecated messages because it uses WC_Product->adjust_price(). To resolve this, in the Product_Addon_Cart class I modified the following code:

/**
* Product_Addon_Cart class.
*/
class Product_Addon_Cart {

/**
* Constructor.
*/
function __construct() {
// ...

// Add meta to order.
add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'order_line_item' ), 10, 3 );

// ...
}

/**
* Adjust add-on proce if set on cart.
*
* @param array $cart_item Cart item data.
* @return array
*/
public function add_cart_item( $cart_item ) {
if ( ! empty( $cart_item['addons'] ) && apply_filters( 'woocommerce_product_addons_adjust_price', true, $cart_item ) ) {
$price = $cart_item['data']->get_price();

foreach ( $cart_item['addons'] as $addon ) {
if ( $addon['price'] > 0 ) {
$price += (float) $addon['price'];
}
}

$cart_item['data']->set_price( $price );
}

return $cart_item;
}

// ...

/**
* Include add-ons line item meta.
*
* @param WC_Order_Item_Product $item Order item data.
* @param string $cart_item_key Cart item key.
* @param array $values Order item values.
*/
public function order_line_item( $item, $cart_item_key, $values ) {
if ( ! empty( $values['addons'] ) ) {
foreach ( $values['addons'] as $addon ) {
$key = $addon['name'];

if ( $addon['price'] > 0 && apply_filters( 'woocommerce_addons_add_price_to_name', true ) ) {
$key .= ' (' . strip_tags( wc_price( get_product_addon_price_for_display( $addon['price'], $values['data'], true ) ) ) . ')';
}

$item->add_meta_data( $key, $addon['value'] );
}
}
}

// ...
}

In this new code it’s possible to see the use of woocommerce_checkout_create_order_line_item hook new in 2.7 and inside Product_Addon_Cart->order_line_item() using WC_Order->add_meta_data to include and save into the Orders CRUD.

For the Product_Addon_Cart_Legacy->add_cart_item() I had to change a little of the logic to be able to use the new WC_Product->set_price().

I did the same kind of thing for the rest of the classes in the plugin, then I cleaned all warnings from my debug.log, switched back to WooCommerce 2.6, and tested everything again to make sure that I had not introduced any extra bug or broken backwards compatibility.


Keep yourself in the loop!

This field is hidden when viewing the form
This field is hidden when viewing the form
This field is hidden when viewing the form


10 responses to “WC 2.7 extension compatibility examples”

  1. What is the recommended way to handle the Lightbox replacement?

    1. Can you elaborate? It’s a straight swap in core – I don’t think extending lightboxes is common? Usually it’s just replaced (dequeue).

  2. […] 2.6.x and 2.7.x, display no notices, and use the CRUD abstraction to future-proof our code. Like in our original 2.7 extension compatibility post, I used the WordPress debug log and the Query Monitor plugin to any errors and deprecated […]

  3. This is a really useful post. Would love to see a similar one detailing the specifics of getting the Bookings extension working with the CRUD methods in WC 2.7

    1. I’m writing that 🙂

      1. Nice. I’m guessing that one will take a bit longer – Bookings is a beast!

  4. […] WC 2.7 extension compatibility examples […]

  5. […] WC 2.7 extension compatibility examples […]

  6. […] More extension compatibility examples […]

  7. […] More extension compatibility examples […]

Leave a Reply

Your email address will not be published. Required fields are marked *