Objective
Currently, most logic and data needed for "converting" WC_Product
objects to subscription-enabled objects are scattered all over the place, for instance:
- scheme data is only accessible from post meta, while
- the active subscription scheme id is stored only in the cart item array
- subscription details are stored as object properties by WCS
(1) and (2), as well as all associated logic for "converting" a product to a subscription in the cart is currently implemented using static functions, simply organized in suitably named classes.
Obviously, we need a more formal, object-oriented way to:
- manage the subscription schemes available with a WC product,
- access its subscription status/details (regardless of its type) and
- convert its subscription state,
not only in cart context, but in a way that's more tightly integrated with the WC_Product
class.
Ideal Case
Ideally, if this would all be part of WC core, the Abstract Product class would include properties and methods to handle the "Subscription Nature" of a product internally, and set any properties associated with it (billing periods, intervals, trial, prices, sign up prices, etc) when converting from/to a non-recurring billing state to a recurring one.
This would allow a product to freely switch between states, while existing methods could ideally take this into account and modify their output, for instance:
get_price
and other price getters could modify the returned value depending on the price(s) associated with the active scheme, if set, otherwise return the standard non-recurring (one-off) price.
get_price_html
could try to generate an output based on the active subscription (if set), otherwise return something meaningful based on a stored min_price_subscription_scheme
.
From a cart perspective, this core-embedded handling would allow WC to set the active subscription scheme with the item session data, and "convert" it when instantiating the product class, similar to how products are instantiated using their product_id
alone - only we'd have an additional subscription_scheme_id
to pass to the constructor.
From an order perspective, get_product_from_item
would use a similar method to instantiate the object using a subscription_scheme_id
field stored in the line item.
This would allow products to have a dual nature - the Subscription and the Product, distinct from one another but unified in the same object.
(Do we agree with this as a utopian solution to start with?)
Possible Solutions
What's the closest we can get to our Utopia, if we agree we accept it as such?
- Somehow dynamically add properties and methods to all instantiated products on init. If PHP was JS, we would have likely intervened with the Abstract Product prototype, or appended "methods" and "properties" at some point during run time. I don't think we have this option.
- Rely on our ability to dynamically add properties to the Abstract Product (magic methods are already there). Use a separate object to manage the Subscription nature of the product, possibly embedded in every product instance as a property (we'll need a
wc_get_product
filter to do this), or follow a "hybrid" approach, with static functions under a WCS_ATT_Product
class accepting the product as a parameter with all "Subscription"-related data stored as properties of the WC product object (much like how WC Subs does it, I think?).
- Instead of adding properties to existing WC Product objects, somehow come up with a way to "wrap" the WC Product objects into a new
WCS_ATT_Product
object on instantiation, and then 1) add all management logic on the wrapper and 2) override the behaviour of existing product methods at will, possibly depending on the type of the wrapped product. I have just started to explore the idea in https://github.com/Prospress/woocommerce-subscribe-all-the-things/tree/oop - it's far from being implemented to actually do the heavy-lifting, but the "wrapping" idea seems to be a viable workaround (unless I am missing something). There are some caveats, for instance if you have code relying on things like the instanceof
an instantiated WC product I suspect lots of things might break. Similarly, we might have issues with cloning of such products.
The advantage of the 1st (impossible with PHP?) and 3rd solutions is that we can do away with the need to declare filters in order to change the output of WC product methods, since the wrapper object operates as a filter always firing last, which is what we want here. The oop
branch explores this concept by embedding get_price_html
into the wrapper object.
We are still some distance away from the Utopia, in the sense that we'll still need to "manually" call methods on our wrapper object when cart session data has loaded, in order to set the object subscription state and use get_product_from_item
to do the same with products instantiated from order items. But overall, the "wrapper object" solution is a possible way to unify the Subscription + Product natures in a true OOP manner.
Scheme Storage
We currently store schemes as serialized meta, which is probably far from ideal. One solution we could explore is to use a custom post type for this, possibly also using the parent_id
db field to point to the product id that a subscription scheme belongs to. All scheme data, such as billing details, prices and other options could be stored as post meta of the new subscription_scheme
post type.
@thenbrent