Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvements: rendering the Subscription Relationship column and checking if order is a renewal, switch etc. #732

Merged
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dd1299a
Introduce new function to check if an order is a subscription parent …
mattallan Nov 20, 2024
44321ec
Refactor our wcs_order_contains_x() functions to be more performant
mattallan Nov 20, 2024
976c961
Update render_contains_subscription_column_content() to accept order …
mattallan Nov 20, 2024
74a9e5a
Add changelog entry
mattallan Nov 20, 2024
fc91135
Replace count(wcs_get_subscriptions_for_order()) with new is_parent_o…
mattallan Nov 20, 2024
b8f94ab
Merge branch 'trunk' into for/731-improve-performance-rendering-order…
mattallan Feb 20, 2025
adeaeca
Dont render the inefficient parent order distinction on orders list t…
mattallan Feb 20, 2025
1122373
Move checking for parent order as the final elseif as it's the most r…
mattallan Feb 20, 2025
ec44e56
Call wcs_is_order() before passing the order to get_related_subscript…
mattallan Feb 25, 2025
de03e43
Introduce new wrapper for WCS_Related_Order_Store get_related_subscri…
mattallan Feb 25, 2025
ad40418
Use new wcs_get_subscription_ids_for_order() in order contains functions
mattallan Feb 25, 2025
7598c36
Update wcs_get_subscriptions_for_order() to use new get_related_subsc…
mattallan Feb 25, 2025
c6b4d21
Add support for fetching parent order types as well
mattallan Feb 25, 2025
ecf3bdf
Add unit tests
mattallan Feb 25, 2025
6621e10
Fix unit tests to match expected array returned (ids in DESC order)
mattallan Feb 25, 2025
71baf7c
Order by IDs
mattallan Feb 25, 2025
5f237ab
Always query for parent order relations if parent is passed - don't m…
mattallan Feb 26, 2025
cd7ac49
Add support for fetching subscription IDs for any order type
mattallan Feb 26, 2025
4051a4c
add default behaviour details to docblock
mattallan Feb 26, 2025
9dd21cf
Add unit tests for a realistic resubscribe case
mattallan Feb 26, 2025
b4bcb76
Sort the returned array of subscription IDs
mattallan Feb 26, 2025
d492796
Ensure backwards compatibility with filter hooks passing WC order obj…
mattallan Feb 26, 2025
1de39ec
Merge branch 'trunk' into for/731-improve-performance-rendering-order…
mattallan Feb 26, 2025
05813de
Update changelog
mattallan Feb 26, 2025
d1ea790
Rename wcs_is_parent_order() to wcs_order_contains_parent() for consi…
mattallan Feb 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
* Fix - Subscription totals not properly updating when customers remove items via the My Account > View Subscription page on some stores with caching enabled.
* Fix - Resolved unexpected errors during the renewal process when a subscription contains metadata with key "id".
* Update - Changed the link on the order thank-you page to take customers directly to their "My Account > Subscriptions" page.
* Update - Improved performance when checking if an order is a subscription renewal, resubscribe or switch order.

= 7.7.2 - 2024-11-26 =
* Fix - Prevents notices being displayed on WordPress 6.7 due to loading translations too early.
Expand Down
18 changes: 12 additions & 6 deletions includes/class-wc-subscriptions-order.php
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ public static function add_contains_subscription_column_content( $column ) {
*/
public static function add_contains_subscription_column_content_orders_table( string $column_name, WC_Order $order ) {
if ( 'subscription_relationship' === $column_name ) {
self::render_contains_subscription_column_content( $order->get_id() );
self::render_contains_subscription_column_content( $order );
}
}

Expand Down Expand Up @@ -2442,14 +2442,20 @@ private static function render_restrict_manage_subscriptions_dropdown() {
*
* @since 6.3.0
*
* @param integer $order_id The ID of the order in the current row.
* @param WC_Order $order The order in the current row.
*/
private static function render_contains_subscription_column_content( int $order_id ) {
if ( wcs_order_contains_subscription( $order_id, 'renewal' ) ) {
private static function render_contains_subscription_column_content( $order ) {
$order = ! is_object( $order ) ? wc_get_order( $order ) : $order;

if ( ! $order ) {
return;
}

if ( wcs_order_contains_renewal( $order ) ) {
echo '<span class="subscription_renewal_order tips" data-tip="' . esc_attr__( 'Renewal Order', 'woocommerce-subscriptions' ) . '"></span>';
} elseif ( wcs_order_contains_subscription( $order_id, 'resubscribe' ) ) {
} elseif ( wcs_order_contains_resubscribe( $order ) ) {
echo '<span class="subscription_resubscribe_order tips" data-tip="' . esc_attr__( 'Resubscribe Order', 'woocommerce-subscriptions' ) . '"></span>';
} elseif ( wcs_order_contains_subscription( $order_id, 'parent' ) ) {
} elseif ( apply_filters( 'woocommerce_subscriptions_orders_list_render_parent_order_relation', false, $order ) && wcs_is_parent_order( $order ) ) {
echo '<span class="subscription_parent_order tips" data-tip="' . esc_attr__( 'Parent Order', 'woocommerce-subscriptions' ) . '"></span>';
} else {
echo '<span class="normal_order">&ndash;</span>';
Expand Down
91 changes: 79 additions & 12 deletions includes/wcs-order-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,63 @@ function wcs_get_subscriptions_for_order( $order, $args = array() ) {

$all_relation_types = WCS_Related_Order_Store::instance()->get_relation_types();
$relation_types = $get_all ? $all_relation_types : array_intersect( $all_relation_types, $args['order_type'] );
$subscription_ids = wcs_get_subscription_ids_for_order( $order, $relation_types );

foreach ( $relation_types as $relation_type ) {

$subscription_ids = WCS_Related_Order_Store::instance()->get_related_subscription_ids( $order, $relation_type );

foreach ( $subscription_ids as $subscription_id ) {
if ( wcs_is_subscription( $subscription_id ) ) {
$subscriptions[ $subscription_id ] = wcs_get_subscription( $subscription_id );
}
foreach ( $subscription_ids as $subscription_id ) {
if ( wcs_is_subscription( $subscription_id ) ) {
$subscriptions[ $subscription_id ] = wcs_get_subscription( $subscription_id );
}
}

return $subscriptions;
}

/**
* Get the subscription IDs for an order.
*
* @param WC_Order $order The order to get the subscription IDs for.
* @param string|array $order_types The order types to get the subscription IDs for.
*
* @return array The subscription IDs.
*/
function wcs_get_subscription_ids_for_order( $order, $order_types = [] ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the intention behind the empty [] $order_types param?

Passing an empty array returns no results. Is it supposed to be interpreted as all order types?

It's probably worth detailing this in the function block param comment too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @james-allan. I think it makes sense for us to return 'any' subscription IDs linked to the given order so I've just implemented the any param here cd7ac49.

I also added these default details to the docblock (see 4051a4c)

$subscription_ids = [];

if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}

if ( ! wcs_is_order( $order ) ) {
return $subscription_ids;
}

if ( ! is_array( $order_types ) ) {
$order_types = [ $order_types ];
}

$valid_order_types = array_intersect( WCS_Related_Order_Store::instance()->get_relation_types(), $order_types );

foreach ( $valid_order_types as $order_type ) {
$subscription_ids += WCS_Related_Order_Store::instance()->get_related_subscription_ids( $order, $order_type );
}

// An order cannot be both a renewal, switch or resubscribe as well as a parent order, so only fetch subscription IDs if we didn't find any in the related order store.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can make this assumption.

If I resubscribe to a cancelled subscription, that order is a parent to the new subscription and a resubscribe to the old subscription.

wcs_get_subscription_ids_for_order( $resubscribe_order, [ 'resubscribe', 'parent' ] )

This should return 2 results, the cancelled subscription and the new one. In it's current form, it would just return the resubscribe.

I understand the intention is to avoid unnecessary expensive queries. I think callers if they were being super performance conscious, they would be be better off doing something like this:

$related_subscriptions = wcs_get_subscription_ids_for_order( $order, [ 'resubscribe', 'renewal', 'switch' ] );

// If the subscription contains a subscription
if ( ! empty( $related_subscriptions ) || wcs_is_parent_order( $order ) {
   ... 

ie separate the related order relations from the parent check.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for flagging this! I've fixed this in 5f237ab

if ( empty( $subscription_ids ) && in_array( 'parent', $order_types, true ) ) {
$subscription_ids = wc_get_orders(
[
'parent' => $order->get_id(),
'type' => 'shop_subscription',
'status' => 'any',
'limit' => -1,
'return' => 'ids',
'orderby' => 'ID',
]
);
}

return $subscription_ids;
}

/**
* Copy the billing, shipping or all addresses from one order or subscription to another.
*
Expand Down Expand Up @@ -365,10 +407,7 @@ function wcs_order_contains_subscription( $order, $order_type = array( 'parent',
$contains_subscription = false;
$get_all = in_array( 'any', $order_type, true );

if ( ( in_array( 'parent', $order_type, true ) || $get_all ) && count( wcs_get_subscriptions_for_order( $order->get_id(), array( 'order_type' => 'parent' ) ) ) > 0 ) {
$contains_subscription = true;

} elseif ( ( in_array( 'renewal', $order_type, true ) || $get_all ) && wcs_order_contains_renewal( $order ) ) {
if ( ( in_array( 'renewal', $order_type, true ) || $get_all ) && wcs_order_contains_renewal( $order ) ) {
$contains_subscription = true;

} elseif ( ( in_array( 'resubscribe', $order_type, true ) || $get_all ) && wcs_order_contains_resubscribe( $order ) ) {
Expand All @@ -377,6 +416,8 @@ function wcs_order_contains_subscription( $order, $order_type = array( 'parent',
} elseif ( ( in_array( 'switch', $order_type, true ) || $get_all ) && wcs_order_contains_switch( $order ) ) {
$contains_subscription = true;

} elseif ( ( in_array( 'parent', $order_type, true ) || $get_all ) && wcs_is_parent_order( $order ) ) {
$contains_subscription = true;
}

return $contains_subscription;
Expand Down Expand Up @@ -1055,3 +1096,29 @@ function wcs_set_recurring_item_total( &$item ) {
]
);
}

/**
* Checks if an order is a Subscriptions parent/initial order.
*
* @param WC_Order|int $order The WC_Order object or ID of a WC_Order order.
*/
function wcs_is_parent_order( $order ) {
$order = ! is_object( $order ) ? wc_get_order( $order ) : $order;

if ( ! $order || ! wcs_is_order( $order ) ) {
return false;
}

// Check if the order ID is the parent of a subscription.
$is_parent_order = wc_get_orders(
[
'parent' => $order->get_id(),
'type' => 'shop_subscription',
'status' => 'any',
'limit' => 1,
'return' => 'ids',
]
);

return apply_filters( 'woocommerce_subscriptions_is_parent_order', ! empty( $is_parent_order ), $order );
}
16 changes: 2 additions & 14 deletions includes/wcs-renewal-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,20 +56,8 @@ function wcs_create_renewal_order( $subscription ) {
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
*/
function wcs_order_contains_renewal( $order ) {

if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}

$related_subscriptions = wcs_get_subscriptions_for_renewal_order( $order );

if ( wcs_is_order( $order ) && ! empty( $related_subscriptions ) ) {
$is_renewal = true;
} else {
$is_renewal = false;
}

return apply_filters( 'woocommerce_subscriptions_is_renewal_order', $is_renewal, $order );
$related_subscription_ids = wcs_get_subscription_ids_for_order( $order, 'renewal' );
return apply_filters( 'woocommerce_subscriptions_is_renewal_order', ! empty( $related_subscription_ids ), $order );
}

/**
Expand Down
16 changes: 2 additions & 14 deletions includes/wcs-resubscribe-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,8 @@
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
*/
function wcs_order_contains_resubscribe( $order ) {

if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}

$related_subscriptions = wcs_get_subscriptions_for_resubscribe_order( $order );

if ( wcs_is_order( $order ) && ! empty( $related_subscriptions ) ) {
$is_resubscribe_order = true;
} else {
$is_resubscribe_order = false;
}

return apply_filters( 'woocommerce_subscriptions_is_resubscribe_order', $is_resubscribe_order, $order );
$related_subscription_ids = wcs_get_subscription_ids_for_order( $order, 'resubscribe' );
return apply_filters( 'woocommerce_subscriptions_is_resubscribe_order', ! empty( $related_subscription_ids ), $order );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This applies to all the contains_x() functions but these filters use to always receive an order object. Given these functions can accept an ID, they will now receive whatever was passed in -- ID or object.

Unfortunately I think this means we will need to return to the

if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
    $order = wc_get_order( $order );
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch @james-allan!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just pushed up d492796 to address this! Going to run through some more tests on these latest changes

}

/**
Expand Down
23 changes: 2 additions & 21 deletions includes/wcs-switch-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,8 @@
* @since 1.0.0 - Migrated from WooCommerce Subscriptions v2.0
*/
function wcs_order_contains_switch( $order ) {

if ( ! is_a( $order, 'WC_Abstract_Order' ) ) {
$order = wc_get_order( $order );
}

if ( ! wcs_is_order( $order ) || wcs_order_contains_renewal( $order ) ) {

$is_switch_order = false;

} else {

$switched_subscriptions = wcs_get_subscriptions_for_switch_order( $order );

if ( ! empty( $switched_subscriptions ) ) {
$is_switch_order = true;
} else {
$is_switch_order = false;
}
}

return apply_filters( 'woocommerce_subscriptions_is_switch_order', $is_switch_order, $order );
$related_subscription_ids = wcs_get_subscription_ids_for_order( $order, 'switch' );
return apply_filters( 'woocommerce_subscriptions_is_switch_order', ! empty( $related_subscription_ids ), $order );
}

/**
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/test-wcs-order-functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -422,4 +422,40 @@ public function test_wcs_set_recurring_item_total() {
$this->assertEquals( 80, $line_item->get_total() );
$this->assertEquals( 80, $line_item->get_subtotal() );
}

/**
* Tests for wcs_get_subscription_ids_for_order()
*/
public function test_wcs_get_subscription_ids_for_order() {
$subscription = WCS_Helper_Subscription::create_subscription();
$parent_order = WCS_Helper_Subscription::create_order( [ 'customer_id' => $subscription->get_customer_id() ] );

$this->assertEquals( [], wcs_get_subscription_ids_for_order( $parent_order, 'parent' ) );

$subscription->set_parent_id( $parent_order->get_id() );
$subscription->save();

$this->assertEquals( [ $subscription->get_id() ], wcs_get_subscription_ids_for_order( $parent_order, 'parent' ) );

$this->assertEquals( [], wcs_get_subscription_ids_for_order( $parent_order, 'renewal' ) );
$this->assertEquals( [], wcs_get_subscription_ids_for_order( $parent_order, 'switch' ) );
$this->assertEquals( [], wcs_get_subscription_ids_for_order( $parent_order, 'resubscribe' ) );

$renewal_order = WCS_Helper_Subscription::create_renewal_order( $subscription );
$switch_order = WCS_Helper_Subscription::create_switch_order( $subscription );
$resubscribe_order = WCS_Helper_Subscription::create_related_order( $subscription, 'resubscribe' );

$this->assertEquals( [ $subscription->get_id() ], wcs_get_subscription_ids_for_order( $renewal_order, 'renewal' ) );
$this->assertEquals( [ $subscription->get_id() ], wcs_get_subscription_ids_for_order( $switch_order, 'switch' ) );
$this->assertEquals( [ $subscription->get_id() ], wcs_get_subscription_ids_for_order( $resubscribe_order, 'resubscribe' ) );

$this->assertEquals( [ $subscription->get_id() ], wcs_get_subscription_ids_for_order( $renewal_order, [ 'renewal', 'parent' ] ) );
$this->assertEquals( [ $subscription->get_id() ], wcs_get_subscription_ids_for_order( $parent_order, [ 'switch', 'parent' ] ) );

$subscription_2 = WCS_Helper_Subscription::create_subscription();
$subscription_2->set_parent_id( $parent_order->get_id() );
$subscription_2->save();

$this->assertEquals( [ $subscription_2->get_id(), $subscription->get_id() ], wcs_get_subscription_ids_for_order( $parent_order, 'parent' ) );
}
}
Loading