Commit 56d0a8dc9e1 for woocommerce

commit 56d0a8dc9e140f95e753bb1bd7db9bae35a6cf1c
Author: Seun Olorunsola <30554163+triple0t@users.noreply.github.com>
Date:   Tue Mar 31 12:25:30 2026 +0100

    Performance: lazy-load _used_by meta in WC_Coupon to fix memory exhaustion on high-usage coupons (#63755)

    * Performance: lazy-load _used_by meta in WC_Coupon to fix memory exhaustion on high-usage coupons

    On stores where a coupon has been used hundreds of thousands of times,
    WC_Coupon::__construct() was calling get_post_meta($id, '_used_by') which
    returns every usage row as a PHP array. This caused hundreds of MB of memory
    allocation on every WC_Coupon instantiation — including order status changes,
    cart validation, and admin edits that never need the used_by list.

    Fix: defer the _used_by fetch until get_used_by() is actually called. The
    default is now null (not loaded) instead of an empty array. get_used_by()
    populates $this->data['used_by'] directly on first access, bypassing
    set_prop() so the lazy fetch does not mark the object dirty. get_data() also
    triggers the lazy load before returning the raw data array, so REST API
    responses continue to include the full used_by list.

    * Fix: use edit context in get_data() to avoid filter recursion in WC_Coupon::get_used_by()

diff --git a/plugins/woocommerce/changelog/wooplug-6370-wc_coupon-construct-and-_used_by-meta b/plugins/woocommerce/changelog/wooplug-6370-wc_coupon-construct-and-_used_by-meta
new file mode 100644
index 00000000000..b9371c6fcac
--- /dev/null
+++ b/plugins/woocommerce/changelog/wooplug-6370-wc_coupon-construct-and-_used_by-meta
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Lazy-load _used_by post meta in WC_Coupon to avoid loading all usage records into memory on construction.
diff --git a/plugins/woocommerce/includes/class-wc-coupon.php b/plugins/woocommerce/includes/class-wc-coupon.php
index 51ecd525958..32caf04a12c 100644
--- a/plugins/woocommerce/includes/class-wc-coupon.php
+++ b/plugins/woocommerce/includes/class-wc-coupon.php
@@ -50,7 +50,7 @@ class WC_Coupon extends WC_Legacy_Coupon {
 		'minimum_amount'              => '',
 		'maximum_amount'              => '',
 		'email_restrictions'          => array(),
-		'used_by'                     => array(),
+		'used_by'                     => null,
 		'virtual'                     => false,
 	);

@@ -152,6 +152,9 @@ class WC_Coupon extends WC_Legacy_Coupon {
 	 * @return array
 	 */
 	public function get_data() {
+		// Ensure used_by is populated before the raw data array is returned, since
+		// parent::get_data() reads $this->data directly and bypasses get_used_by().
+		$this->get_used_by( 'edit' );
 		$data = parent::get_data();
 		if ( '' === $data['minimum_amount'] ) {
 			$data['minimum_amount'] = '0';
@@ -455,11 +458,22 @@ class WC_Coupon extends WC_Legacy_Coupon {
 	/**
 	 * Get records of all users who have used the current coupon.
 	 *
+	 * The list is loaded lazily on first access rather than during object construction,
+	 * because a coupon with hundreds of thousands of usages would otherwise allocate an
+	 * enormous PHP array on every WC_Coupon instantiation — even for code paths such as
+	 * order-status changes or cart validation that never need this data.
+	 *
 	 * @since  3.0.0
 	 * @param  string $context What the value is for. Valid values are 'view' and 'edit'.
 	 * @return array
 	 */
 	public function get_used_by( $context = 'view' ) {
+		if ( is_null( $this->data['used_by'] ) ) {
+			// Bypass set_prop() so the lazy fetch does not mark the object as dirty.
+			$this->data['used_by'] = $this->get_id()
+				? array_filter( (array) get_post_meta( $this->get_id(), '_used_by', false ) )
+				: array();
+		}
 		return $this->get_prop( 'used_by', $context );
 	}

diff --git a/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php
index e8e3a41d6a6..86c909651c8 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-coupon-data-store-cpt.php
@@ -144,7 +144,6 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
 				'minimum_amount'              => get_post_meta( $coupon_id, 'minimum_amount', true ),
 				'maximum_amount'              => get_post_meta( $coupon_id, 'maximum_amount', true ),
 				'email_restrictions'          => array_filter( (array) get_post_meta( $coupon_id, 'customer_email', true ) ),
-				'used_by'                     => array_filter( (array) get_post_meta( $coupon_id, '_used_by' ) ),
 			)
 		);
 		$coupon->read_meta_data();