class-database-cache.php
9.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
<?php
/**
* Class Database_Cache
*
* @package WooCommerce\Payments
*/
namespace WCPay;
defined( 'ABSPATH' ) || exit; // block direct access.
/**
* A class for caching data as an option in the database.
*/
class Database_Cache {
const ACCOUNT_KEY = 'wcpay_account_data';
const BUSINESS_TYPES_KEY = 'wcpay_business_types_data';
const CURRENCIES_KEY = 'wcpay_multi_currency_cached_currencies';
const CUSTOMER_CURRENCIES_KEY = 'wcpay_multi_currency_customer_currencies';
/**
* Payment methods cache key prefix. Used in conjunction with the customer_id to cache a customer's payment methods.
*/
const PAYMENT_METHODS_KEY_PREFIX = 'wcpay_pm_';
/**
* Dispute status counts cache key.
*
* @var string
*/
const DISPUTE_STATUS_COUNTS_KEY = 'wcpay_dispute_status_counts_cache';
/**
* Cache key for authorization summary data like count, total amount, etc.
*
* @var string
*/
const AUTHORIZATION_SUMMARY_KEY = 'wcpay_authorization_summary_cache';
/**
* Cache key for authorization summary data like count, total amount, etc in test mode.
*
* @var string
*/
const AUTHORIZATION_SUMMARY_KEY_TEST_MODE = 'wcpay_test_authorization_summary_cache';
/**
* Refresh disabled flag, controlling the behaviour of the get_or_add function.
*
* @var bool
*/
private $refresh_disabled;
/**
* Class constructor.
*/
public function __construct() {
$this->refresh_disabled = false;
add_action( 'action_scheduler_before_execute', [ $this, 'disable_refresh' ] );
}
/**
* Gets a value from cache or regenerates and adds it to the cache.
*
* @param string $key The options key to cache the data under.
* @param callable $generator Function/callable regenerating the missing value. If null or false is returned, it will be treated as an error.
* @param callable $validate_data Function/callable validating the data after it is retrieved from the cache. If it returns false, the cache will be refreshed.
* @param boolean $force_refresh Regenerates the cache regardless of its state if true.
* @param boolean $refreshed Is set to true if the cache has been refreshed without errors and with a non-empty value.
*
* @return mixed The cache contents.
*/
public function get_or_add( string $key, callable $generator, callable $validate_data, bool $force_refresh = false, bool &$refreshed = false ) {
$cache_contents = get_option( $key );
$data = null;
$old_data = null;
// If the stored data is valid, prepare it for return in case we don't need to refresh.
// Also initialize old_data in case of errors.
if ( is_array( $cache_contents ) && array_key_exists( 'data', $cache_contents ) && $validate_data( $cache_contents['data'] ) ) {
$data = $cache_contents['data'];
$old_data = $data;
}
if ( $this->should_refresh_cache( $key, $cache_contents, $validate_data, $force_refresh ) ) {
$errored = false;
try {
$data = $generator();
$errored = false === $data || null === $data;
} catch ( \Throwable $e ) {
$errored = true;
}
$refreshed = ! $errored;
if ( $errored ) {
// Still return the old data on error and refresh the cache with it.
$data = $old_data;
}
$cache_contents = $this->write_to_cache( $key, $data, $errored );
}
return $data;
}
/**
* Gets a value from the cache.
*
* @param string $key The key to look for.
* @param bool $force If set, return from the cache without checking for expiry.
*
* @return mixed The cache contents.
*/
public function get( string $key, bool $force = false ) {
$cache_contents = get_option( $key );
if ( is_array( $cache_contents ) && array_key_exists( 'data', $cache_contents ) ) {
if ( ! $force && $this->is_expired( $key, $cache_contents ) ) {
return null;
}
return $cache_contents['data'];
}
return null;
}
/**
* Stores a value in the cache.
*
* @param string $key The key to store the value under.
* @param mixed $data The value to store.
*
* @return void
*/
public function add( string $key, $data ) {
$this->write_to_cache( $key, $data, false );
}
/**
* Deletes a value from the cache.
*
* @param string $key The key to delete.
*
* @return void
*/
public function delete( string $key ) {
delete_option( $key );
}
/**
* Hook function allowing the cache refresh to be selectively disabled in certain situations
* (such as while running an Action Scheduler job). While the refresh is disabled, get_or_add
* will only return the cached value and never regenerate it, even if it's expired.
*
* @return void
*/
public function disable_refresh() {
$this->refresh_disabled = true;
}
/**
* Deletes all saved by looking for cache key prefix. This is useful when you want to cache user related data by
*
* @param string $key Cache key prefix to delete.
*
* @return void
*/
public function delete_by_prefix( string $key ) {
// Protection against accidentally deleting all options or options that are not related to WcPay caching.
// Since only one cache key prefix is supported, we will check only this one by checking does key starts with payment method key prefix.
// Feel free to update this statement if more prefix cache keys you are planning to add.
if ( strncmp( $key, self::PAYMENT_METHODS_KEY_PREFIX, strlen( self::PAYMENT_METHODS_KEY_PREFIX ) ) !== 0 ) {
return; // Maybe throw exception here...
}
global $wpdb;
$plugin_options = $wpdb->get_results( $wpdb->prepare( "SELECT option_name FROM $wpdb->options WHERE option_name LIKE %s", $key . '%' ) );
foreach ( $plugin_options as $option ) {
$this->delete( $option->option_name );
}
}
/**
* Validates the cache contents and, given the passed params and the current application state, determines whether the cache should be refreshed.
* See get_or_add.
*
* @param string $key The cache key.
* @param mixed $cache_contents The cache contents.
* @param callable $validate_data Callback used to validate the cached data by the callee.
* @param boolean $force_refresh Whether a refresh should be forced.
*
* @return boolean True if the cache needs to be refreshed.
*/
private function should_refresh_cache( string $key, $cache_contents, callable $validate_data, bool $force_refresh ): bool {
// Always refresh if the flag is set.
if ( $force_refresh ) {
return true;
}
// Do not refresh if doing ajax or the refresh has been disabled (running an AS job).
if ( defined( 'DOING_CRON' ) || wp_doing_ajax() || $this->refresh_disabled ) {
return false;
}
// The value of false means that there was never an option set in the database.
if ( false === $cache_contents ) {
return true;
}
// Non-array, empty array, or missing expected fields mean corrupted data.
// This also handles potential legacy data, which might have those keys missing.
if ( ! is_array( $cache_contents )
|| empty( $cache_contents )
|| ! array_key_exists( 'data', $cache_contents )
|| ! isset( $cache_contents['fetched'] )
|| ! array_key_exists( 'errored', $cache_contents )
) {
return true;
}
// If the data is not errored and invalid, refresh it.
if (
! $cache_contents['errored'] &&
! $validate_data( $cache_contents['data'] )
) {
return true;
}
// Refresh the expired data.
if ( $this->is_expired( $key, $cache_contents ) ) {
return true;
}
return false;
}
/**
* Wraps the data in the cache metadata and stores it.
*
* @param string $key The key to store the data under.
* @param mixed $data The data to store.
* @param boolean $errored Whether the refresh operation resulted in an error before this has been called.
*
* @return array The cache contents (data and metadata).
*/
private function write_to_cache( string $key, $data, bool $errored ): array {
// Add the data and expiry time to the array we're caching.
$cache_contents = [];
$cache_contents['data'] = $data;
$cache_contents['fetched'] = time();
$cache_contents['errored'] = $errored;
// Create or update the option cache.
if ( false === get_option( $key ) ) {
add_option( $key, $cache_contents, '', 'no' );
} else {
update_option( $key, $cache_contents, 'no' );
}
return $cache_contents;
}
/**
* Checks if the cache contents are expired.
*
* @param string $key The cache key.
* @param array $cache_contents The cache contents.
*
* @return boolean True if the contents are expired.
*/
private function is_expired( string $key, array $cache_contents ): bool {
$ttl = $this->get_ttl( $key, $cache_contents );
$expires = $cache_contents['fetched'] + $ttl;
$now = time();
return $expires < $now;
}
/**
* Given the key and the cache contents, and based on the application state, determines the cache TTL.
*
* @param string $key The cache key.
* @param array $cache_contents The cache contents.
*
* @return integer The cache TTL.
*/
private function get_ttl( string $key, array $cache_contents ): int {
switch ( $key ) {
case self::ACCOUNT_KEY:
if ( is_admin() ) {
// Fetches triggered from the admin panel should be more frequent.
if ( $cache_contents['errored'] ) {
// Attempt to refresh the data quickly if the last fetch was an error.
$ttl = 2 * MINUTE_IN_SECONDS;
} else {
// If the data was fetched successfully, fetch it every 2h.
$ttl = 2 * HOUR_IN_SECONDS;
}
} else {
// Non-admin requests should always refresh only after 24h since the last fetch.
$ttl = DAY_IN_SECONDS;
}
break;
case self::CURRENCIES_KEY:
// Refresh the errored currencies quickly, otherwise cache for 6h.
$ttl = $cache_contents['errored'] ? 2 * MINUTE_IN_SECONDS : 6 * HOUR_IN_SECONDS;
break;
case self::BUSINESS_TYPES_KEY:
// Cache business types for a week.
$ttl = WEEK_IN_SECONDS;
break;
default:
// Default to 24h.
$ttl = DAY_IN_SECONDS;
break;
}
return apply_filters( 'wcpay_database_cache_ttl', $ttl, $key, $cache_contents );
}
}