PluginProbe
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO / 1.4.16
Discount Rules for WooCommerce – Disco | Dynamic Pricing, Conditions, Bulk, Bundle, BOGO v1.4.16
1.4.16 1.4.15 1.4.14 1.4.13 1.4.12 1.4.11 1.4.10 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.54 1.3.53 1.3.52 1.3.51 1.3.50 1.3.49 1.3.48 1.3.47 All 179 releases
← All changes | app/Features/UserLimit.php +135 -14 1.4.21.4.16 View file →
@@ -22,8 +22,23 @@
22 22 */
23 23 class UserLimit {
24 24
25 25 /**
26 + * Object cache group for campaign usage counts.
27 + */
28 + public const CACHE_GROUP = 'disco_user_limit';
29 +
30 + /**
31 + * Transient / cache key prefix for campaign usage counts.
32 + */
33 + public const CACHE_KEY_PREFIX = 'disco_campaign_usage_';
34 +
35 + /**
36 + * Default cache lifetime, in seconds, for campaign usage counts.
37 + */
38 + public const CACHE_TTL = 300;
39 +
40 + /**
26 41 * Disco start session function.
27 42 * Set campaign in WC session as an array.
28 43 *
29 44 * @param int $campaign_id Campaign ID.
@@ -52,15 +67,120 @@
52 67 // Save the updated associative array back to the session
53 68 WC()->session->set( 'disco_campaign', $disco_campaign );
54 69 }
55 70
71 + /**
72 + * Whether a campaign has reached its configured global usage limit.
73 + *
74 + * This is the entry point callers should use instead of fetching the count
75 + * directly: when a campaign has no usage limit configured (the common case)
76 + * it returns early and no order/meta query is issued at all.
77 + *
78 + * @param \Disco\App\Utility\Config|object $campaign Campaign config object.
79 + * @return bool
80 + */
81 + public function disco_is_limit_reached( $campaign ) {
82 + if ( ! $campaign instanceof \Disco\App\Utility\Config || empty( $campaign->id ) ) {
83 + return false;
84 + }
85 +
86 + $limit = $campaign->discount_max_user;
87 +
88 + // No limit configured — never query.
89 + if ( empty( $limit ) || ! is_numeric( $limit ) || (int) $limit < 0 ) {
90 + return false;
91 + }
92 +
93 + return $this->disco_get_total_applied_campaign( (int) $campaign->id ) >= (int) $limit;
94 + }
95 +
56 96 /**
57 97 * Retrieve total applied campaign by campaign id.
58 98 *
99 + * The result is memoized per request and cached (object cache + transient)
100 + * because the underlying COUNT() joins the order and order-meta tables,
101 + * which is expensive on large stores. Usage limits do not need real-time
102 + * precision at cart/fragment-refresh frequency; the cache is invalidated
103 + * when an order gains campaign meta or changes status.
104 + *
59 105 * @param int $campaign_id Campaign ID.
60 106 * @return int
61 107 */
62 - public function disco_get_total_applied_campaign( $campaign_id ) { // phpcs:disable
108 + public function disco_get_total_applied_campaign( $campaign_id ) {
109 + $campaign_id = (int) $campaign_id;
110 + $cache_key = self::CACHE_KEY_PREFIX . $campaign_id;
111 + $cached = wp_cache_get( $cache_key, self::CACHE_GROUP );
112 +
113 + if ( false === $cached ) {
114 + $cached = get_transient( $cache_key );
115 + }
116 +
117 + if ( is_numeric( $cached ) ) {
118 + return (int) $cached;
119 + }
120 +
121 + $count = $this->query_total_applied_campaign( $campaign_id );
122 +
123 + /**
124 + * Filter the cache lifetime, in seconds, of campaign usage counts.
125 + *
126 + * @param int $ttl Lifetime in seconds.
127 + * @param int $campaign_id Campaign ID.
128 + */
129 + $ttl = (int) apply_filters( 'disco_campaign_usage_cache_ttl', self::CACHE_TTL, $campaign_id );
130 +
131 + if ( $ttl > 0 ) {
132 + wp_cache_set( $cache_key, $count, self::CACHE_GROUP, $ttl );
133 + set_transient( $cache_key, $count, $ttl );
134 + }
135 +
136 + return $count;
137 + }
138 +
139 + /**
140 + * Invalidate the cached usage count for a campaign, or for all campaigns.
141 + *
142 + * @param int|null $campaign_id Campaign ID, or null to flush every campaign.
143 + * @return void
144 + */
145 + public static function flush_cache( $campaign_id = null ) {
146 + if ( null !== $campaign_id ) {
147 + $campaign_id = (int) $campaign_id;
148 + $cache_key = self::CACHE_KEY_PREFIX . $campaign_id;
149 +
150 + wp_cache_delete( $cache_key, self::CACHE_GROUP );
151 + delete_transient( $cache_key );
152 +
153 + return;
154 + }
155 +
156 + if ( ! class_exists( \Disco\App\Campaign::class ) ) {
157 + return;
158 + }
159 +
160 + $campaigns = ( new \Disco\App\Campaign )->get_rows();
161 +
162 + if ( ! is_array( $campaigns ) ) {
163 + return;
164 + }
165 +
166 + foreach ( array_keys( $campaigns ) as $id ) {
167 + self::flush_cache( $id );
168 + }
169 + }
170 +
171 + /**
172 + * Run the usage count query against the order tables.
173 + *
174 + * The order-meta table is joined first (via STRAIGHT_JOIN) so MySQL resolves
175 + * the small `meta_key`/`meta_value` set before touching the orders table.
176 + * Letting the optimizer start from the orders table means scanning every
177 + * order on the store, which is what made this query pathologically slow.
178 + *
179 + * @param int $campaign_id Campaign ID.
180 + * @return int
181 + */
182 + private function query_total_applied_campaign( $campaign_id ) { // phpcs:disable
63 183 global $wpdb;
64 184
65 185 // Filter the order statuses
66 186 $status = apply_filters(
@@ -77,20 +197,21 @@
77 197
78 198 // Check HPOS first
79 199 if ( $this->disco_is_hpos_enabled() ) {
80 200 $sql = "
81 - SELECT COUNT(DISTINCT o.id)
82 - FROM {$wpdb->prefix}wc_orders AS o
83 - INNER JOIN {$wpdb->prefix}wc_orders_meta AS om ON o.id = om.order_id
84 - WHERE o.status IN ($placeholders)
85 - AND om.meta_key = %s
201 + SELECT STRAIGHT_JOIN COUNT(DISTINCT om.order_id)
202 + FROM {$wpdb->prefix}wc_orders_meta AS om
203 + INNER JOIN {$wpdb->prefix}wc_orders AS o ON o.id = om.order_id
204 + WHERE om.meta_key = %s
86 205 AND om.meta_value = %s
206 + AND o.type = 'shop_order'
207 + AND o.status IN ($placeholders)
87 208 ";
88 209
89 210 $count = $wpdb->get_var(
90 211 $wpdb->prepare(
91 212 $sql,
92 - ...array_merge( $status, array( 'disco_campaign', (string) $campaign_id ) )
213 + ...array_merge( array( 'disco_campaign', (string) $campaign_id ), $status )
93 214 )
94 215 );
95 216
96 217 return (int) $count;
@@ -97,21 +218,21 @@
97 218 }
98 219
99 220 // Legacy query for posts table (non-HPOS only)
100 221 $sql = "
101 - SELECT COUNT(p.ID)
102 - FROM {$wpdb->posts} AS p
103 - INNER JOIN {$wpdb->postmeta} AS pm ON p.ID = pm.post_id
104 - WHERE p.post_type = 'shop_order'
222 + SELECT STRAIGHT_JOIN COUNT(DISTINCT pm.post_id)
223 + FROM {$wpdb->postmeta} AS pm
224 + INNER JOIN {$wpdb->posts} AS p ON p.ID = pm.post_id
225 + WHERE pm.meta_key = %s
226 + AND pm.meta_value = %s
227 + AND p.post_type = 'shop_order'
105 228 AND p.post_status IN ($placeholders)
106 - AND pm.meta_key = %s
107 - AND pm.meta_value = %s
108 229 ";
109 230
110 231 $count = $wpdb->get_var(
111 232 $wpdb->prepare(
112 233 $sql,
113 - ...array_merge( $status, array( 'disco_campaign', (string) $campaign_id ) )
234 + ...array_merge( array( 'disco_campaign', (string) $campaign_id ), $status )
114 235 )
115 236 );
116 237
117 238 return (int) $count;