PluginProbe
Media Cloud Sync / 1.4.1
Media Cloud Sync v1.4.1
1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 All 35 releases
← All changes | includes/base/cache.php +282 -47 1.0.11.4.1 View file →
@@ -2,8 +2,18 @@
2 2 namespace Dudlewebs\WPMCS;
3 3
4 4 defined('ABSPATH') || exit;
5 5
6 +/**
7 + * Plugin object cache wrapper.
8 + *
9 + * Supports all common WordPress environments on PHP 8.1+ and WordPress 5.9+:
10 + * 1. Default WordPress object cache (in-memory, per request — no Redis/Memcached plugin).
11 + * 2. Persistent object cache drop-ins (Redis Object Cache, Memcached, etc.).
12 + *
13 + * Reads and writes always go through wp_cache_* so behaviour is consistent;
14 + * flush strategy differs based on whether a persistent drop-in is active.
15 + */
6 16 class Cache {
7 17 private static $instance = null;
8 18 private $assets_url;
9 19 private $version;
@@ -59,29 +69,183 @@
59 69 return true;
60 70 }
61 71
62 72 /**
73 + * Whether a persistent object cache drop-in is active (Redis, Memcached, etc.).
74 + */
75 + private static function uses_persistent_object_cache() {
76 + return function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache();
77 + }
78 +
79 + /**
80 + * Build a consistent object cache key for get/set/delete.
81 + */
82 + private static function build_cache_key($key, $post_id, $meta_name, $meta_type = 'post') {
83 + $prefix = is_array($meta_type)
84 + ? (isset($meta_type['prefix']) ? $meta_type['prefix'] : 'custom')
85 + : $meta_type;
86 +
87 + return ('post' === $prefix ? '' : $prefix . '_')
88 + . ($post_id == false ? '' : $post_id . '_')
89 + . $meta_name
90 + . (!Utils::is_empty($key) ? '_' . $key : '');
91 + }
92 +
93 + /**
94 + * Read the raw meta array for a given storage backend.
95 + *
96 + * $meta_type is 'post' (default) or 'user' for WP's own dedicated helpers
97 + * (options table when $post_id is false); a plain string is passed straight
98 + * to WP's generic get_metadata() — correct for any standard WP object type
99 + * whose meta table uses WP's usual 'meta_id' primary key (post/user/comment/
100 + * term). Third-party meta tables that don't follow that convention (e.g.
101 + * BuddyBoss's groupmeta, which uses 'id') aren't safe to read/write via
102 + * get_metadata() directly — pass an array of ['get'=>, 'update'=>, 'delete'=>]
103 + * callables (each shaped like get_post_meta($id,$key,true)/update_post_meta(
104 + * $id,$key,$value)/delete_post_meta($id,$key)) so the caller's own,
105 + * already-correct meta functions are used instead.
106 + */
107 + private static function read_meta($post_id, $meta_name, $meta_type) {
108 + if (is_array($meta_type)) {
109 + // Callability is already verified by is_backend_available() before
110 + // any of get/set/delete_object_cache() reach this point.
111 + return call_user_func($meta_type['get'], $post_id, $meta_name, true);
112 + }
113 + if ('post' === $meta_type) {
114 + return $post_id == false ? get_option($meta_name, []) : get_post_meta($post_id, $meta_name, true);
115 + }
116 + if ('user' === $meta_type) {
117 + return get_user_meta($post_id, $meta_name, true);
118 + }
119 + return get_metadata($meta_type, $post_id, $meta_name, true);
120 + }
121 +
122 + /**
123 + * Write the raw meta array for a given storage backend. See read_meta().
124 + */
125 + private static function write_meta($post_id, $meta_name, $meta_data, $meta_type) {
126 + if (is_array($meta_type)) {
127 + return call_user_func($meta_type['update'], $post_id, $meta_name, $meta_data);
128 + }
129 + if ('post' === $meta_type) {
130 + return $post_id == false ? update_option($meta_name, $meta_data) : update_post_meta($post_id, $meta_name, $meta_data);
131 + }
132 + if ('user' === $meta_type) {
133 + return update_user_meta($post_id, $meta_name, $meta_data);
134 + }
135 + return update_metadata($meta_type, $post_id, $meta_name, $meta_data);
136 + }
137 +
138 + /**
139 + * Whether $meta_type can actually be used right now. A custom backend's
140 + * callables may not be defined yet (e.g. the plugin/component that owns
141 + * them is currently inactive) — in that case the whole Cache layer must
142 + * be skipped entirely (no wp_cache_* writes either), so the caller falls
143 + * through to its own non-cached path exactly as it would for 'table' =>
144 + * false. Partially engaging the cache here (writing the intended value to
145 + * wp_cache_* while the real read/update/delete silently no-ops) would
146 + * leave a phantom entry that get/delete can never find to invalidate.
147 + */
148 + private static function is_backend_available($meta_type) {
149 + if (!is_array($meta_type)) {
150 + return true;
151 + }
152 + return isset($meta_type['get'], $meta_type['update'], $meta_type['delete'])
153 + && is_callable($meta_type['get'])
154 + && is_callable($meta_type['update'])
155 + && is_callable($meta_type['delete']);
156 + }
157 +
158 + /**
159 + * Delete the entire meta entry for a given storage backend. See read_meta().
160 + */
161 + private static function delete_meta_entirely($post_id, $meta_name, $meta_type) {
162 + if (is_array($meta_type)) {
163 + return call_user_func($meta_type['delete'], $post_id, $meta_name);
164 + }
165 + if ('post' === $meta_type) {
166 + return $post_id == false ? delete_option($meta_name) : delete_post_meta($post_id, $meta_name);
167 + }
168 + if ('user' === $meta_type) {
169 + return delete_user_meta($post_id, $meta_name);
170 + }
171 + return delete_metadata($meta_type, $post_id, $meta_name);
172 + }
173 +
174 + /**
175 + * Whether the active object cache supports a feature (WP 6.1+ or drop-in).
176 + */
177 + private static function cache_supports($feature) {
178 + return function_exists('wp_cache_supports') && wp_cache_supports($feature);
179 + }
180 +
181 + /**
182 + * Read from object cache with WP 5.5+ $found support.
183 + */
184 + private static function get_cached_value($cache_key, &$found) {
185 + $group = Schema::getConstant('CACHE_GROUP');
186 + $found = false;
187 +
188 + if (!function_exists('wp_cache_get')) {
189 + return false;
190 + }
191 +
192 + // wp_cache_get( ..., &$found ) is available from WordPress 5.5 (plugin requires 5.9).
193 + return wp_cache_get($cache_key, $group, false, $found);
194 + }
195 +
196 + /**
197 + * Write a value to the plugin object cache group.
198 + */
199 + private static function sync_object_cache($key, $data, $post_id, $meta_name, $cache_expire, $meta_type = 'post') {
200 + if (!function_exists('wp_cache_set') || !function_exists('wp_cache_delete')) {
201 + return;
202 + }
203 +
204 + $group = Schema::getConstant('CACHE_GROUP');
205 + $cache_key = self::build_cache_key($key, $post_id, $meta_name, $meta_type);
206 +
207 + wp_cache_delete($cache_key, $group);
208 + wp_cache_set($cache_key, $data, $group, $cache_expire);
209 + }
210 +
211 + /**
63 212 * Get Object Cache
213 + *
214 + * @param string $meta_type 'post' (default; uses options table when $post_id is false),
215 + * 'user', or any object type WP's generic get_metadata() recognizes
216 + * (e.g. 'group', registered by BuddyBoss).
64 217 */
65 - public static function get_object_cache($key, $post_id = false, $meta_name = false, $cache_expire = false) {
218 + public static function get_object_cache($key = '', $post_id = false, $meta_name = false, $cache_expire = false, $meta_type = 'post') {
219 + if (!self::is_backend_available($meta_type)) {
220 + return false;
221 + }
222 +
66 223 $meta_name = $meta_name == false ? Schema::getConstant('META_KEY') : $meta_name;
67 224 $cache_expire = $cache_expire == false ? Schema::getConstant('CACHE_EXPIRE') : $cache_expire;
225 + $cache_key = self::build_cache_key($key, $post_id, $meta_name, $meta_type);
226 + $found = false;
68 227
69 - $cache_enabled = wp_using_ext_object_cache();
70 - $cache_key = ($post_id == false ? '' : $post_id) . $meta_name . '_' . $key;
71 -
72 - if ($cache_enabled) {
73 - $data = wp_cache_get($cache_key, Schema::getConstant('CACHE_GROUP'));
74 - if ($data !== false) return $data;
228 + $data = self::get_cached_value($cache_key, $found);
229 + if ($found) {
230 + return $data;
75 231 }
76 232
77 - $meta_data = $post_id == false ? get_option($meta_name, []) : get_post_meta($post_id, $meta_name, true);
78 - $data = isset($meta_data[$key]) ? $meta_data[$key] : false;
233 + $meta_data = self::read_meta($post_id, $meta_name, $meta_type);
79 234
80 - if ($data && $cache_enabled) {
81 - wp_cache_set($cache_key, $data, Schema::getConstant('CACHE_GROUP'), $cache_expire);
235 + if( !Utils::is_empty($key) && is_array($meta_data) && array_key_exists($key, $meta_data) ) {
236 + // If key exists in meta data, return it
237 + $data = $meta_data[$key];
238 + } elseif (is_array($meta_data) && Utils::is_empty($key)) {
239 + // If no key is provided, return the entire meta data
240 + $data = $meta_data;
241 + } else {
242 + // If key does not exist, return false
243 + $data = false;
82 244 }
83 245
246 + self::sync_object_cache($key, $data, $post_id, $meta_name, $cache_expire, $meta_type);
247 +
84 248 return $data;
85 249 }
86 250
87 251 /**
@@ -86,70 +250,141 @@
86 250
87 251 /**
88 252 * Set Object Cache
89 253 */
90 - public static function set_object_cache($key, $data, $post_id = false, $meta_name = false, $cache_expire = false) {
254 + public static function set_object_cache($key, $data, $post_id = false, $meta_name = false, $cache_expire = false, $meta_type = 'post') {
255 + if (!self::is_backend_available($meta_type)) {
256 + return false;
257 + }
258 +
91 259 $meta_name = $meta_name == false ? Schema::getConstant('META_KEY') : $meta_name;
92 260 $cache_expire = $cache_expire == false ? Schema::getConstant('CACHE_EXPIRE') : $cache_expire;
93 261
94 - $meta_data = $post_id == false ? get_option($meta_name, []) : get_post_meta($post_id, $meta_name, true);
262 + $meta_data = self::read_meta($post_id, $meta_name, $meta_type);
95 263
96 - if (is_array($meta_data)) {
97 - if(isset($meta_data[$key])){
98 - if($meta_data[$key] == $data) {
99 - return true;
100 - }
101 - }
264 + // If data is not an array, convert it to an array
265 + if (!is_array($meta_data)) {
266 + $meta_data = [];
267 + }
268 +
269 + if(Utils::is_empty($key)) {
270 + // If key is empty, set the entire meta data
271 + $meta_data = $data;
272 + $cache_value = $data;
273 + } else {
102 274 $meta_data[$key] = $data;
103 - } else {
104 - $meta_data = [$key => $data];
275 + $cache_value = $data;
105 276 }
106 277
107 - $update_result = $post_id == false ? update_option($meta_name, $meta_data) : update_post_meta($post_id, $meta_name, $meta_data);
108 -
109 - // Set cache only if data update was successful
110 - if (wp_using_ext_object_cache() && $update_result) {
111 - $cache_key = ($post_id == false ? '' : $post_id) . $meta_name . '_' . $key;
112 - wp_cache_set($cache_key, $data, Schema::getConstant('CACHE_GROUP'), Schema::getConstant('CACHE_EXPIRE'));
278 + // Update the meta data
279 + $update_result = self::write_meta($post_id, $meta_name, $meta_data, $meta_type);
280 +
281 + // Always sync object cache so Redis and default WP cache stay current.
282 + self::sync_object_cache($key, $cache_value, $post_id, $meta_name, $cache_expire, $meta_type);
283 +
284 + if ($update_result) {
285 + return true;
113 286 }
114 287
115 - // Return success if data update was successful
116 - return $update_result;
288 + // update_option/update_meta return false when the value is unchanged — treat as success once cache is synced.
289 + $stored = self::read_meta($post_id, $meta_name, $meta_type);
290 +
291 + return maybe_serialize($stored) === maybe_serialize($meta_data);
117 292 }
118 293
119 -
120 294
295 +
121 296 /**
122 297 * Delete object cache
123 298 */
124 - public static function delete_object_cache($key = false, $post_id = false, $meta_name = false) {
299 + public static function delete_object_cache($key = false, $post_id = false, $meta_name = false, $meta_type = 'post') {
300 + if (!self::is_backend_available($meta_type)) {
301 + return false;
302 + }
303 +
125 304 $meta_name = $meta_name == false ? Schema::getConstant('META_KEY') : $meta_name;
126 305 $update_result = false;
127 - $meta_data = $post_id == false ? get_option($meta_name, []) : get_post_meta($post_id, $meta_name, true);
128 306
129 - if (is_array($meta_data)) {
130 - if($key == false) {
131 - $update_result = $post_id == false ? delete_option($meta_name) : delete_post_meta($post_id, $meta_name);
132 - } else if (array_key_exists($key, $meta_data)) {
133 - unset($meta_data[$key]);
134 - if(empty($meta_data)) {
135 - $update_result = $post_id == false ? delete_option($meta_name) : delete_post_meta($post_id, $meta_name);
136 - } else {
137 - $update_result = $post_id == false ? update_option($meta_name, $meta_data) : update_post_meta($post_id, $meta_name, $meta_data);
138 - }
307 + $meta_data = self::read_meta($post_id, $meta_name, $meta_type);
308 + if (is_array($meta_data) && array_key_exists($key, $meta_data)) {
309 + unset($meta_data[$key]);
310 + if (empty($meta_data)) {
311 + $update_result = self::delete_meta_entirely($post_id, $meta_name, $meta_type);
312 + } else {
313 + $update_result = self::write_meta($post_id, $meta_name, $meta_data, $meta_type);
139 314 }
140 315 }
141 316
142 - // Delete from object cache if caching is enabled
143 - if (wp_using_ext_object_cache() && $update_result && $key) {
144 - $cache_key = ($post_id == false ? '' : $post_id) . $meta_name . '_' . $key;
317 + if ($update_result && !Utils::is_empty($key) && function_exists('wp_cache_delete')) {
318 + $cache_key = self::build_cache_key($key, $post_id, $meta_name, $meta_type);
145 319 wp_cache_delete($cache_key, Schema::getConstant('CACHE_GROUP'));
146 320 }
147 321
148 - // Return success if data delete was successful
149 322 return $update_result;
150 323 }
151 324
325 +
326 + /**
327 + * Flush Cache
328 + * @since 1.3.2
329 + */
330 + public static function flush_object_cache() {
331 + $group = Schema::getConstant('CACHE_GROUP');
332 +
333 + if (self::uses_persistent_object_cache()) {
334 + // Redis / Memcached drop-in: flush only this plugin's cache group.
335 + self::flush_cache_group($group);
336 + } else {
337 + // Default WordPress in-memory cache: safe to flush the runtime cache.
338 + self::flush_runtime_cache();
339 + }
340 +
341 + self::$cached_data = [];
342 + return true;
343 + }
344 +
345 + /**
346 + * Flush the in-memory runtime cache (default WordPress, no drop-in plugin).
347 + */
348 + private static function flush_runtime_cache() {
349 + if (self::cache_supports('flush_runtime') && function_exists('wp_cache_flush_runtime')) {
350 + wp_cache_flush_runtime();
351 + return;
352 + }
353 +
354 + if (function_exists('wp_cache_flush')) {
355 + wp_cache_flush();
356 + }
357 + }
358 +
359 + /**
360 + * Flush all keys in the plugin cache group when the drop-in supports it.
361 + */
362 + private static function flush_cache_group($group) {
363 + if (!function_exists('wp_cache_flush_group')) {
364 + return false;
365 + }
366 +
367 + // Prefer the supported API so core never falls back to a full cache flush.
368 + if (self::cache_supports('flush_group')) {
369 + return (bool) wp_cache_flush_group($group);
370 + }
371 +
372 + // Persistent drop-ins (e.g. Redis Object Cache on WP 5.9) may provide flush_group
373 + // without wp_cache_supports() being available in core.
374 + if (self::uses_persistent_object_cache()) {
375 + return (bool) wp_cache_flush_group($group);
376 + }
377 +
378 + return false;
379 + }
380 +
381 +
382 + /**
383 + * Gets the instance of this class.
384 + *
385 + * @return Cache
386 + */
152 387 public static function instance(){
153 388 if (is_null(self::$instance)) {
154 389 self::$instance = new self();
155 390 }
@@ -154,5 +389,5 @@
154 389 self::$instance = new self();
155 390 }
156 391 return self::$instance;
157 392 }
158 -}
393 +}