WooCommerce Subscriptions v2.0 introduced a major architectural change relating to how subscription data is stored and accessed.
This was the most important change in Subscriptions’ code base since it was first developed more than 3 years prior to the version 2.0 release. It was also the first time a major breaking change to Subscriptions’ code base was introduced.
While all reasonable efforts were undertaken to maintain backward compatibility with API functions and existing actions/filters, custom code written against the Subscriptions v1.5 code base may not be compatible with the v2.0. In particular, if code accesses a subscription’s meta data directly in the database, or against order (i.e. WC_Order
) or cart (i.e. WC()->cart
or WC_Cart
) objects, it will most likely be broken.
This guide explains the history behind the old architecture and the reason changes were necessary. It then provides an overview of the changes to help understand how to update code written to integrate with Subscriptions.
If you have not already, you may wish to read the What’s New in Subscriptions v2.0 and Store Manager Guide to Multiple Subscriptions documents to get a non-technical overview of the changes introduced by Subscriptions v2.0.
A History of Subscriptions
Almost 3 years from the time of writing, Subscriptions v1.0 was first being designed. It was being created to achieve one simple goal – connect WooCommerce with PayPal Standard’s Subscriptions.
Because PayPal offered off-site payment processing and subscription storage, little consideration was given to how to store and manage a subscription after it was purchased within WooCommerce. The most important aspect of the design was how to create a subscription product in WooCommerce and then how to pass the details of that product to PayPal on checkout. It’s no coincidence that the product components of Subscriptions’ architecture are not changing in v2.0. A subscription is still a custom product type that extends from a base WooCommerce product class.
However, because of the focus on PayPal and the features it provided, the original architecture had some major short-comings:
- it was not possible to purchase multiple different subscription products on different billing schedules in the same transaction. PayPal requires a customer to sign up for each subscription individually, so to purchase an annual, monthly and weekly subscription in the same transaction and use PayPal as the payment method, the customer would need to complete checkout with PayPal 3 times.
- Subscriptions only needed to know the PayPal Profile ID for the subscription and handle IPN messages in order to handle recurring payments. Because PayPal took care of all of this, the method by which subscription meta data (such as recurring totals, completed renewal payments etc.) were stored in WooCommerce was an afterthought. It was such an after thought, that renewal orders, now the backbone of a subscriptions record keeping and inventory management, did not exist until version v1.2.
- subscription management within the store, like modifying a subscription’s billing schedule, shipping, taxes or line items was not possible, because PayPal didn’t allow any of these details to be changed. As a result, when interfaces for these tasks were introduced in later versions of Subscriptions, they were also an afterthought that needed to be retrofitted to the original architecture.
Subscriptions v1.5 Architecture
v1.5 Data Storage – the Order
Because Subscriptions was first designed to simply link an order in a WooCommerce store with PayPal, Subscriptions v1.5 stored all meta data relating to a subscription on the original order created to record the purchase of a subscription product.
Because much of a subscription’s data was not even stored in v1.0, this meant that as newer versions of Subscriptions required new meta data to add new features with other payment gateways (like changing payment method or billing/shipping address), this data was retrofitted to the order. As a result, data was often stored in unexpected and disjointed places.
For example, a subscription’s data in v1.5 was stored in the following database tables:
wp_postmeta
: some values designed to mirror the original order’s meta data on the subscription were stored in the post meta table against the original order, like_recurring_payment_method
(which mirrored the original order’s_payment_method
and was the payment method used to process renewals) or_recurring_order_total
(which mirrored the original order’s_order_total
and was the amount charged for renewal payments)wp_woocommerce_order_items
: some values mirroring the original order’s items were stored in the order item table, likerecurring_tax
item/s (which mirrored the original order’stax
item/s for recurring amounts) orrecurring_shipping
item/s (which mirrored the original order’sshipping
item/s for recurring amounts)wp_woocommerce_order_itemmeta
: some values relating to the subscription product’s billing terms were stored in the order item meta table against the subscription product’s line item. For example, a subscription’s billing schedule (_subscription_period
and_subscription_interval
), status (_subscription_status
) and important dates (_subscription_start_date
,_subscription_expiry_date
,_subscription_trial_expiry_date
and_subscription_completed_payments
) were all stored in the order item meta table.
This storage system was:
- inefficient: querying subscriptions almost always required a number of
JOIN
statements and queries on columns of a genericvarchar
type (the type used for meta values). For example, a query to get subscriptions by start date required joining 3 tables:wp_posts
,wp_woocommerce_order_items
andwp_woocommerce_order_itemmeta
to search for a MySQL formatted date stored as avarchar
. This ultimately led to serious scaling issues once a store started to enjoy more than 20,000 subscribers. In v2.0, the same query is on a single table (wp_posts
) in a column with a more accurate type (datetime
) and it uses a schema proven to scale to hundreds of thousands of rows. - cumbersome: to find out any details on a subscription, you needed to know the order ID and often either the user ID and/or product ID for that subscription. Each subscription did not have a simple, unique ID which could be used to access it and pass around its data.
- unintuitive: as a subscription has evolved into an important object, new developers coming to Subscriptions now expect to find a discrete subscription object, not a collection of data attached to an order.
- repetitive: because this schema was completely unique to Subscriptions, it was not possible to take advantage of code already written in WordPress or WooCommerce for querying data. This makes simple feature requests, like improved reporting, difficult to implement because it requires custom database queries compared with using well known WordPress API functions like
get_posts()
.
v1.5 Data Structure – the Array
As a subscription had very little data in v1.0, and that data rarely needed to be accessed or modified, the data structure chosen for accessing a subscription’s details at the application level was the most basic object available: an array
.
As new versions of Subscriptions began to provide features for modifying a subscription, the array structure was maintained and separate functions were implemented to act on the subscription’s data, including the most primitive WordPress functions for acting on data like update_post_meta()
or subscription specific functions like WC_Subscriptions_Manager::update_subscription()
.
Because an array can not have methods, this was the only approach available, despite native methods on the object offering a much more natural approach (e.g. $subscription->update_status()
) and also providing many of the benefits of object oriented programming, like abstraction and encapsulation.
Evolution of the WooCommerce Ecosystem
In addition to the issues outlined above with the existing data storage and structure used for a subscription, the payment gateway and WooCommerce landscape evolved dramatically in the time since Subscriptions 1.0 was designed.
Uptake of Modern Payment Gateway APIs
In the 3 years since first designing Subscriptions, modern payment gateways that provide credit card tokens, like Stripe and Authorize.net CIM, have become more available to non-US markets and easier to access by non-technical store owners. This development has created an opportunity for Subscriptions’ feature set to be designed for these modern, more flexible payment methods, rather than the out-dated APIs offered by PayPal Standard.
Feedback on the Subscriptions feature set has consistently demanded more control over subscription data from within the WooCommerce store. This is possible with these modern payment gateways (but not PayPal Standard).
With Subscriptions 2.0 therefore, there was an opportunity and a need to design a system for these gateways that provides more control to store owners. Fortunately, there was also a recent development in WooCommerce that made such a change possible.
WooCommerce v2.2 and Custom Order Types
WooCommerce v2.2 quietly introduced a new API that could have great implications for plugins like Subscriptions – the custom order types API.
This API allows for more than just the standard simple
order type to exist in a WooCommerce store.
An order is WooCommerce’s snapshot of history. In the case of a simple
order, it is used to record a purchase transaction. In the case of a refund
, it records the return of some or all of an order’s amount to the customer.
Unlike an order, a subscription is not a record of what has happened; instead, it is an agreement for what should happen. However, an order’s meta data, like customer billing and shipping details, product line items, taxes, fees and totals perfectly align with the details required for such an agreement. Therefore, a custom order type is the perfect data store for a subscription.
Store Manager Feedback
The final and most important driving force behind the change was the same factor that drives all of development on Subscriptions – customer feedback.
In addition to technical and market changes, our understanding of what store managers expect from a subscription management plugin evolved in the 3 years since releasing version 1.0.
Although no store manager has ever asked for a subscription to be stored as a custom order type, to support the numerous and diverse requests for all manner of features to be built on top of Subscriptions, the architecture needed to evolve. Many requested features, like upgrades/downgrades, updating payment method, changing shipping address, were implemented, painstakingly in some cases, on the Subscriptions 1.5 architecture.
However, many features could not be feasibly implemented with that architecture, such as the ability for customers to purchase different subscription products in a single transaction. Even simple requests, like gifting subscriptions, improved reporting or having shipping charged only on the initial order were extremely difficult to achieve with the v1.5 data scheme and structure.
The new architecture consolidates 3 years of feedback into a design that can support every feature request and behaviour change we’ve received in that time. Even if these do not make it into Subscriptions core, it will be possible to write them as mini-extensions with significantly less developer time than previously required.
Introducing the Subscription Order Type
The backbone of the v2.0 changes is a new subscription order type.
In much the same way as an order is stored as a 'shop_order'
post type and instantiated as an instance of a WC_Order
class, the subscription order type consists of a 'shop_subscription'
post type and WC_Subscription
class.
The 'shop_subscription'
Post Type
The 'shop_subscription'
post type is a WordPress custom post type created by wc_register_order_type()
to be used for the storage of subscription data.
As this post type is an extension of the 'shop_order'
post type, a subscription’s meta is stored in the same locations as an order’s data, making it possible for developers to learn where an order’s data is stored, and then instantly understand where a subscription’s data will be stored. For example, after being purchased in an order, a subscription product becomes a line item on both that order and a subscription, meaning its data is consistently stored as a line_item
in wp_woocommerce_order_items
table for both the 'shop_order'
and 'shop_subscription'
.
The WC_Subscription
Class
The WC_Subscription
class extends WC_Order
to provide an object for interacting with a subscription’s data.
The object inherits all of the properties and methods of the WC_Order
class (which also includes those of its parent – WC_Abstract_Order)
. These properties and methods make it significantly more intuitive to work with a subscription’s data. For example, in v2.0, changing a subscription’s status can be done with the WC_Abstract_Order->update_status()
method (e.g. $subscription->update_status()
.
In addition, the WC_Subscription
class also adds new properties for storing a subscription’s meta data, mainly relating to billing schedule and important dates, like the next payment date, and new methods for interacting with this data, like WC_Subscription->update_dates()
.
Benefits of the Subscription Order Type
The primary benefits of the subscription order type are:
- WordPress’s post list table: as a subscription is now a custom post type, the custom list table used on the Manage Subscriptions screen has been replaced by a WordPress post list table. This saved hundreds of lines of code and helps provide better future proofing of the list table for updates to WordPress and WooCommerce markup and CSS.
- Scalable storage: now that a subscription’s data is stored in the same way as other WordPress custom post types and WooCommerce orders, Subscriptions can take advantage of trusted API functions, like
get_posts()
, to work with subscription data and be confident that queries will scale. - DRY: by using a custom order type, Subscriptions can take advantage of WooCommerce code to add new features more quickly and with less code. An example of this in v2.0, beyond the obvious example of extending
WC_Order
, is the introduction of API endpoints for subscriptions. This is done by introducing a newWC_API_Subscriptions
class which extendsWC_API_Orders
and benefits from much of the code in that class. - Learn once: developers familiar with working with WooCommerce’s orders can now transfer that knowledge entirely to working with subscriptions. Instead of having to learn an entirely new set of API functions and database schema, developers now need to only learn where the extra meta data is stored and how to access it.
Implications of the Subscription Order Type
By changing the database schema and application level data structure of a subscription, code handling everything from the initial purchase through to renewal and management of a subscription has had to be updated.
This is because old functions expected some combination of an order ID or WC_Order
, subscription key (consisting of the order ID and product ID, not a unique integer ID for that object), user ID and/or product ID. Similarly, the hooks used for existing actions and filters would pass this data to callbacks; where as in v2.0, those callbacks need the WC_Subscriptions
or ID of the 'shop_subscription'
to take advantage of the new architecture.
Backward Compatibility
All reasonable steps have been taken to ensure existing functions and actions/filters will continue to work. However, use of deprecated functions, actions and filters will throw deprecated notices and they will be removed in future versions.
Furthermore, some hooks may have been called on events which no longer occur, like creating a pending subscription by adding an order added via the administration screen.
Finally, any code directly querying the database to read or write subscription data will no longer be compatible with the new schema in Subscriptions v2.0.
Other Changes
This guide focused on the introduction of the 'shop_subscription'
post type and corresponding WC_Subscription
class; however, there are a number of other notable changes introduced in Subscriptions v2.0 that related to developers.
Renewal Order Relationship Change
In Subscriptions v1.5, a subscription was connected to a renewal order using the renewal order’s post_parent
column in the wp_posts
database table.
Specifically, the post_parent
was set to the post ID of the original order used to purchase a subscription (because a subscription in v1.5 did not have its own unique ID).
In v2.0, this relation was changed to use post meta on the subscription post type. A meta_key
of _subscription_renewal
storing a meta_value
of the subscription’s post ID is now used to record this relationship.
This change makes it possible to have a many-to-one relationship between subscriptions and renewal orders. That is to say, a single renewal order can now be related to multiple subscriptions; where as in v1.5, a renewal order could only relate to a single subscription. Although this new capability isn’t being used in any meaningful way with the release of Subscriptions v2.0, it was introduced with v2.0 because the relationship needed to change to use the ID of a 'shop_subscription'
post, not the ID of a 'shop_order'
post.
As the relationship needed to change in v2.0 anyway, this change was implemented now to avoid another breaking change in the future version.
The many-to-one relationship makes it possible to implement advanced features in the future, like batch processing renewal payments for different subscriptions in a single renewal order. Such a feature would save store owners fees as most payment gateways charge a per transaction fee, like Stripe’s $0.20.
Renewal Order Creation Before Scheduled Payment Hook
Subscriptions v1.0 did not create renewal orders because PayPal recorded transaction details and sent store owners and customers the details of those transactions.
Accordingly, when renewal orders were introduced in version v1.2, renewal order creation was retrofitted to the existing codebase and therefore, renewal orders were only created after the payment gateway processed the payment.
This design had a few significant drawbacks:
- payment gateways had to use special Subscription API functions and write special code for handling subscription renewal payments, instead of being able to use code written for WooCommerce’s order object that worked for all payments.
- when WooCommerce v2.2 introduced the
_transaction_id
API for storing a payment gateway’s transaction on a renewal order, it was not possible for payment gateways to add the transaction ID for renewal orders without hacks. - if an unrecoverable error occurred when attempting to process the renewal payment, the renewal order would not be created. This also meant sites which had an unrecoverable error needed to run special code in order to generate missing orders.
Because of these drawbacks, Subscriptions v2.0 changes the flow of operations to create the renewal order before the scheduled subscription payment hook is triggered.
To learn how to take advantage of this change and use the renewal order for processing renewal payments with your payment gateway extension/s, refer to the Payment Gateway Upgrade Guide.
Changes to the Switching Process
If you are not familiar with the switching feature of Subscriptions (i.e. upgrading/downgrading), please read the switching guide before continuing with this section.
In Subscriptions v1.5, switching a subscription from one variation or grouped product to another always created a new subscription and set the status of the old subscription to switched
. This was to ensure compatibility of the switching feature with PayPal, and to always avoid rewriting history and changing subscription related meta data on the original order used to purchase the subscription.
However, due to the architectural changes in v2.0, it is now possible to simply change the details of the subscription object without losing the history of the original order.
Furthermore, because a subscription may now contain multiple line items, switching needed to be updated to be per item instead of per subscription.
In v2.0, the switching process now:
- removes the line item from the old subscription
- adds the new item as a line item on the existing
'shop_subscription'
if the new item is:- on the same billing schedule as the old subscription; or
- on a different billing schedule, but is the only line item on the subscription, and therefore, the subscription’s billing schedule can be changed.
- creates a new
'shop_subscription'
if the new item is on a different billing schedule and the old subscription had more than one line item.
The switching process still uses the cart and checkout as it did in v1.5. A switch order is also still created to record the switch. This order is created regardless of whether proration is enabled and a prorated amount is charged for the switch or the switching cost is $0. Multiple switches can also be completed at the same time, for multiple line items on the same or different subscriptions.
New Pending Cancellation Status
Subscriptions v2.0 introduced a new subscription status: Pending Cancellation.
This status, internally stored as wc-pending-cancel
(because wc-pending-cancellation
is too long for the post_status
column), is given to a subscription to signify that the customer or store manager has cancelled the subscription, but the customer is still entitled to a pre-paid term.
The status will be applied to a subscription whenever a customer or store manage cancels a subscription. It is set automatically when calling WC_Subscription::cancel_order()
, but not when calling WC_Susbcription::update_status()
to provide an API for developers that immediately transitions a subscription to cancelled status (the wc-cancelled
status).
Prior to Subscriptions v2.0, the pre-paid term was signified only by an action triggered at the end of the pre-paid term ('subscription_end_of_prepaid_term'
). It was left up to developers to use it as required, and was not used internally by Subscriptions.
Example of Pending Cancellation
If a customer buys a monthly subscription on the 1st January, then cancels the subscription on 15th January, in Subscriptions v1.5, its status would immediately be changed to wc-cancelled
, the customer’s role would immediately be transitioned to the default inactive role and then on the 1st February, a 'subscription_end_of_prepaid_term'
hook would be triggered.
With v2.0, that subscription’s status will be immediately set to wc-pending-cancel
and the customer will keep the default active subscriber role. On the 1st February, the subscription’s status will be changed to wc-cancelled
and the customer’s role will be changed to the inactive role. The end of prepaid term hook will also be triggered.
Store Manager Guide to v2.0 Changes
A summary of all the changes in relation to how a store manager interacts with the Subscriptions extension can also be found in the Overview of Subscriptions v2.0’s Changes document.