├── .gitattributes ├── .gitignore ├── README.md └── object-cache.php /.gitattributes: -------------------------------------------------------------------------------- 1 | # Exclude these files from release archives. 2 | /composer.json export-ignore 3 | /composer.lock export-ignore 4 | 5 | # 6 | # Auto detect text files and perform LF normalization. 7 | # 8 | # http://davidlaing.com/2012/09/19/customise-your-gitattributes-to-become-a-git-ninja/ 9 | # 10 | * text=auto 11 | 12 | # The above will handle all files not found below. 13 | *.md text 14 | *.php text 15 | *.inc text 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Training Object Cache for WordPress 2 | 3 | The Training Object Cache is a tool for learning about and debugging the data stored using the WordPress cache API. 4 | 5 | ## Introduction 6 | 7 | The WordPress cache API, made of up `wp_cache_*` functions like `wp_cache_set()` and `wp_cache_get()`, isn't always easy to learn about. What data are WordPress and your plugins caching, and for how long? When is the cache accessed instead of core database tables? 8 | 9 | By default, data put into the cache is stored in a PHP array that lasts only for the life of the request, so the cache will be empty at the start of the next request no matter for how long the data was supposed to be stored. 10 | 11 | If a persistent object cache plugin is in use, such as [Memcached](https://wordpress.org/plugins/memcached/), the data will be stored in the persistent cache for quick retrieval by WordPress on subsequent requests, but the data [might not be easy to look at within the object cache itself](https://stackoverflow.com/questions/8420776/how-do-i-view-the-data-in-memcache). 12 | 13 | The goal of the Training Object Cache is to make it easier to learn about the data stored in the cache by storing it in a dedicated table in your site's existing database. 14 | 15 | For developers who are new to the cache API, having the data stored in the existing database means it can be easily reviewed alongside the default WordPress tables using a database viewer like phpMyAdmin or Sequel Pro, and it means that cache activity can be reviewed using a debugging plugin that monitors database queries like [Query Monitor](https://wordpress.org/plugins/query-monitor/) or [Debug Bar](https://wordpress.org/plugins/debug-bar/). 16 | 17 | Even experienced developers browsing the cached data might find data being duplicated across keys, entries taking up more space than expected, or data being cached that isn't supposed to be anymore. 18 | 19 | ## Installation 20 | 21 | 1. Clone this repository, or [download the latest version](https://github.com/dlh01/wp-training-object-cache/archive/main.zip). 22 | 23 | 2. Move `object-cache.php` into your `wp-content` directory. 24 | 25 | You should see Training Object Cache listed under `Plugins > Drop-ins` in the Dashboard: `/wp-admin/plugins.php?plugin_status=dropins`. 26 | 27 | ## Usage 28 | 29 | Browse your site, then look for the `wp_training_object_cache` table in your preferred database viewer. 30 | 31 | The table contains the following columns: 32 | 33 | * `cache_group`: The group given for the cached data. Groups allow keys to be reused across groups. 34 | * `cache_key`: The cache key. In multisite installations, the key is given a site-specific prefix to allow keys to be reused across sites. 35 | * `data`: The cached data. Non-scalar data will be serialized as it is in other database tables. 36 | * `TTL`: The [time to live](https://en.wikipedia.org/wiki/Time_to_live) given for the cached data, if any, made readable via `human_time_diff()`. 37 | * `size`: The approximate size of the cached data, made readable via `size_format()`. 38 | * `expires`: Unix timestamp of when the cached data expires, if ever. 39 | 40 | This table is a global table; in a multisite installation, there will not be site-specific instances of it. 41 | 42 | ## Limitations as a teaching tool 43 | 44 | Each object caching drop-in plugin is free to implement caching however it likes. For example, [running `wp_cache_flush()` with the Memcached plugin does not actually flush Memcached](https://plugins.trac.wordpress.org/browser/memcached/tags/3.2.2/object-cache.php#L270). Other caching plugins could choose to modify or ignore data in a manner optimized for their storage mechanisms. Therefore, the behavior of the object cache using this plugin isn't necessarily representative of whether or how data will be stored using other object caching plugins. 45 | 46 | ## Caution 47 | 48 | This plugin is NOT RECOMMENDED for use on a live site! 49 | 50 | Apart from increasing load on the database, it has little protection against race conditions, and because [storing scalar values in the database can be lossy](https://core.trac.wordpress.org/ticket/22192), unexpected behavior can occur relative to other persistent caching backends. 51 | 52 | ## WP-CLI commands 53 | 54 | All built-in `wp cache` WP-CLI commands should work normally with the training cache. The following custom commands are also available: 55 | 56 | * `wp training-object-cache reset`: Delete all cached data, and recreate the database table. 57 | * `wp training-object-cache destroy`: Delete all cached data, and remove the database table. 58 | -------------------------------------------------------------------------------- /object-cache.php: -------------------------------------------------------------------------------- 1 | dbh = $dbh; 117 | $this->dbh->training_object_cache = $this->dbh->base_prefix . 'training_object_cache'; 118 | 119 | $this->multisite = $multisite; 120 | $this->start = $start; 121 | 122 | $this->switch_to_blog( $blog_id ); 123 | } 124 | 125 | /** 126 | * Makes private properties readable for backward compatibility. 127 | * 128 | * @param string $name Property to get. 129 | * @return mixed Property. 130 | */ 131 | public function __get( $name ) { 132 | return $this->$name; 133 | } 134 | 135 | /** 136 | * Makes private properties settable for backward compatibility. 137 | * 138 | * @param string $name Property to set. 139 | * @param mixed $value Property value. 140 | * @return mixed Newly set property. 141 | */ 142 | public function __set( $name, $value ) { 143 | return $this->$name = $value; 144 | } 145 | 146 | /** 147 | * Makes private properties checkable for backward compatibility. 148 | * 149 | * @param string $name Property to check if set. 150 | * @return bool Whether the property is set. 151 | */ 152 | public function __isset( $name ) { 153 | return isset( $this->$name ); 154 | } 155 | 156 | /** 157 | * Makes private properties un-settable for backward compatibility. 158 | * 159 | * @param string $name Property to unset. 160 | */ 161 | public function __unset( $name ) { 162 | unset( $this->$name ); 163 | } 164 | 165 | /** 166 | * Adds data to the cache if it doesn't already exist. 167 | * 168 | * @param int|string $key What to call the contents in the cache. 169 | * @param mixed $data The contents to store in the cache. 170 | * @param string $group Optional. Where to group the cache contents. Default 'default'. 171 | * @param int $expire Optional. When to expire the cache contents. Default 0 (no expiration). 172 | * @return bool True on success, false if cache key and group already exist. 173 | */ 174 | public function add( $key, $data, $group = 'default', $expire = 0 ) { 175 | if ( ! $this->ready ) { 176 | return false; 177 | } 178 | 179 | if ( wp_suspend_cache_addition() ) { 180 | return false; 181 | } 182 | 183 | if ( empty( $group ) ) { 184 | $group = 'default'; 185 | } 186 | 187 | $id = $this->prefixed( $key, $group ); 188 | 189 | if ( $this->exists( $id, $group ) ) { 190 | return false; 191 | } 192 | 193 | return $this->set( $key, $data, $group, (int) $expire ); 194 | } 195 | 196 | /** 197 | * Sets the list of global cache groups. 198 | * 199 | * @param bool[] $groups List of groups that are global. 200 | */ 201 | public function add_global_groups( $groups ) { 202 | $groups = (array) $groups; 203 | 204 | $groups = array_fill_keys( $groups, true ); 205 | $this->global_groups = array_merge( $this->global_groups, $groups ); 206 | } 207 | 208 | /** 209 | * Adds non-persistent groups. 210 | * 211 | * @param string|string[] $groups List of groups that are global. 212 | */ 213 | public function add_non_persistent_groups( $groups ) { 214 | $groups = (array) $groups; 215 | 216 | $this->nonpersistent_groups = array_merge( $this->nonpersistent_groups, $groups ); 217 | $this->nonpersistent_groups = array_unique( $this->nonpersistent_groups ); 218 | } 219 | 220 | /** 221 | * Decrements numeric cache item's value. 222 | * 223 | * @param int|string $key The cache key to decrement. 224 | * @param int $offset Optional. The amount by which to decrement the item's value. Default 1. 225 | * @param string $group Optional. The group the key is in. Default 'default'. 226 | * @return int|false The item's new value on success, false on failure. 227 | */ 228 | public function decr( $key, $offset = 1, $group = 'default' ) { 229 | if ( empty( $group ) ) { 230 | $group = 'default'; 231 | } 232 | 233 | $id = $this->prefixed( $key, $group ); 234 | 235 | if ( ! $this->exists( $id, $group ) ) { 236 | return false; 237 | } 238 | 239 | $current = $this->get( $key, $group ); 240 | 241 | if ( ! is_numeric( $current ) ) { 242 | $current = 0; 243 | } 244 | 245 | $value = $current - (int) $offset; 246 | 247 | if ( $value < 0 ) { 248 | $value = 0; 249 | } 250 | 251 | $this->update_numeric( $id, $group, $value ); 252 | 253 | return $value; 254 | } 255 | 256 | /** 257 | * Removes the contents of the cache key in the group. 258 | * 259 | * If the cache key does not exist in the group, then nothing will happen. 260 | * 261 | * @param int|string $key What the contents in the cache are called. 262 | * @param string $group Optional. Where the cache contents are grouped. Default 'default'. 263 | * @param bool $deprecated Unused. 264 | * @return bool False if the contents weren't deleted and true on success. 265 | */ 266 | public function delete( $key, $group = 'default', $deprecated = false ) { 267 | if ( empty( $group ) ) { 268 | $group = 'default'; 269 | } 270 | 271 | $id = $this->prefixed( $key, $group ); 272 | 273 | if ( ! $this->exists( $id, $group ) ) { 274 | return false; 275 | } 276 | 277 | $this->dbh->delete( 278 | $this->dbh->training_object_cache, 279 | array( 280 | 'cache_key' => $id, 281 | 'cache_group' => $group, 282 | ) 283 | ); 284 | 285 | unset( $this->cache[ $group ][ $id ] ); 286 | 287 | return true; 288 | } 289 | 290 | /** 291 | * Delete all expired cache values. 292 | */ 293 | public function expire() { 294 | if ( ! $this->ready ) { 295 | return; 296 | } 297 | 298 | $this->dbh->query( 299 | $this->dbh->prepare( 300 | "DELETE FROM {$this->dbh->training_object_cache} WHERE expires != 0 AND expires < %d", $this->start->getTimestamp() 301 | ) 302 | ); 303 | } 304 | 305 | /** 306 | * Clears the object cache of all data. 307 | * 308 | * @return true Always returns true. 309 | */ 310 | public function flush() { 311 | if ( ! $this->ready ) { 312 | return true; 313 | } 314 | 315 | $this->dbh->query( "TRUNCATE {$this->dbh->training_object_cache}" ); 316 | 317 | wp_cache_init(); 318 | 319 | return true; 320 | } 321 | 322 | /** 323 | * Retrieves the cache contents, if it exists. 324 | * 325 | * @param int|string $key The key under which the cache contents are stored. 326 | * @param string $group Optional. Where the cache contents are grouped. Default 'default'. 327 | * @param bool $force Unused. 328 | * @param bool $found Optional. Whether the key was found in the cache (passed by reference). 329 | * @return mixed|false The cache contents on success, false on failure to retrieve contents. 330 | */ 331 | public function get( $key, $group = 'default', $force = false, &$found = null ) { 332 | if ( ! $this->ready ) { 333 | return false; 334 | } 335 | 336 | if ( empty( $group ) ) { 337 | $group = 'default'; 338 | } 339 | 340 | $id = $this->prefixed( $key, $group ); 341 | 342 | if ( $this->exists( $id, $group ) ) { 343 | $found = true; 344 | 345 | if ( empty( $this->cache_hits[ $group ] ) ) { 346 | $this->cache_hits[ $group ] = 0; 347 | } 348 | 349 | $this->cache_hits[ $group ] += 1; 350 | 351 | if ( ! isset( $this->cache[ $group ] ) || ! array_key_exists( $id, $this->cache[ $group ] ) ) { 352 | $this->cache[ $group ][ $id ] = maybe_unserialize( 353 | $this->dbh->get_var( 354 | $this->select_data( $id, $group ) 355 | ) 356 | ); 357 | } 358 | 359 | $value = $this->cache[ $group ][ $id ]; 360 | 361 | if ( is_object( $value ) ) { 362 | $value = clone $value; 363 | } 364 | 365 | return $value; 366 | } 367 | 368 | $found = false; 369 | 370 | if ( empty( $this->cache_misses[ $group ] ) ) { 371 | $this->cache_misses[ $group ] = 0; 372 | } 373 | 374 | $this->cache_misses[ $group ] += 1; 375 | 376 | return false; 377 | } 378 | 379 | /** 380 | * Retrieves multiple values from the cache in one call. 381 | * 382 | * @param array $keys Array of keys under which the cache contents are stored. 383 | * @param string $group Optional. Where the cache contents are grouped. Default 'default'. 384 | * @param bool $force Unused. 385 | * @return array Array of values organized into groups. 386 | */ 387 | public function get_multiple( $keys, $group = 'default', $force = false ) { 388 | $values = array(); 389 | 390 | foreach ( $keys as $key ) { 391 | $values[ $key ] = $this->get( $key, $group, $force ); 392 | } 393 | 394 | return $values; 395 | } 396 | 397 | /** 398 | * Caching stats. 399 | * 400 | * @return int[] 401 | */ 402 | public function getStats() { 403 | return array( 404 | 'cache_hits' => array_sum( $this->cache_hits ), 405 | 'cache_misses' => array_sum( $this->cache_misses ), 406 | ); 407 | } 408 | 409 | /** 410 | * Increments numeric cache item's value. 411 | * 412 | * @param int|string $key The cache key to increment 413 | * @param int $offset Optional. The amount by which to increment the item's value. Default 1. 414 | * @param string $group Optional. The group the key is in. Default 'default'. 415 | * @return int|false The item's new value on success, false on failure. 416 | */ 417 | public function incr( $key, $offset = 1, $group = 'default' ) { 418 | if ( empty( $group ) ) { 419 | $group = 'default'; 420 | } 421 | 422 | $id = $this->prefixed( $key, $group ); 423 | 424 | if ( ! $this->exists( $id, $group ) ) { 425 | return false; 426 | } 427 | 428 | $current = $this->get( $key, $group ); 429 | 430 | if ( ! is_numeric( $current ) ) { 431 | $current = 0; 432 | } 433 | 434 | $value = $current + (int) $offset; 435 | 436 | if ( $value < 0 ) { 437 | $value = 0; 438 | } 439 | 440 | $this->update_numeric( $id, $group, $value ); 441 | 442 | return $value; 443 | } 444 | 445 | /** 446 | * Replaces the contents in the cache, if contents already exist. 447 | * 448 | * @param int|string $key What to call the contents in the cache. 449 | * @param mixed $data The contents to store in the cache. 450 | * @param string $group Optional. Where to group the cache contents. Default 'default'. 451 | * @param int $expire Optional. When to expire the cache contents. Default 0 (no expiration). 452 | * @return bool False if not exists, true if contents were replaced. 453 | */ 454 | public function replace( $key, $data, $group = 'default', $expire = 0 ) { 455 | if ( empty( $group ) ) { 456 | $group = 'default'; 457 | } 458 | 459 | $id = $this->prefixed( $key, $group ); 460 | 461 | if ( ! $this->exists( $id, $group ) ) { 462 | return false; 463 | } 464 | 465 | $this->delete( $key, $group ); 466 | 467 | return $this->set( $key, $data, $group, (int) $expire ); 468 | } 469 | 470 | /** 471 | * Resets cache keys. 472 | * 473 | * @deprecated Use switch_to_blog() 474 | */ 475 | public function reset() { 476 | _deprecated_function( __FUNCTION__, '3.5.0', 'switch_to_blog()' ); 477 | } 478 | 479 | /** 480 | * Sets the data contents into the cache. 481 | * 482 | * The cache contents are grouped by the $group parameter followed by the 483 | * $key. This allows for duplicate IDs in unique groups. Therefore, naming of 484 | * the group should be used with care and should follow normal function 485 | * naming guidelines outside of core WordPress usage. 486 | * 487 | * @param int|string $key What to call the contents in the cache. 488 | * @param mixed $data The contents to store in the cache. 489 | * @param string $group Optional. Where to group the cache contents. Default 'default'. 490 | * @param int $expire Optional. When to expire the cache contents. Default 0 (no expiration). 491 | * @return true Always returns true. 492 | */ 493 | public function set( $key, $data, $group = 'default', $expire = 0 ) { 494 | if ( ! $this->ready ) { 495 | return true; 496 | } 497 | 498 | if ( ! is_string( $key ) && ! is_int( $key ) ) { 499 | return true; 500 | } 501 | 502 | if ( empty( $group ) ) { 503 | $group = 'default'; 504 | } 505 | 506 | $id = $this->prefixed( $key, $group ); 507 | 508 | // Reduce chance of duplicate insert from another process by forcing re-check. 509 | unset( $this->not_cached[ $group ][ $id ] ); 510 | 511 | if ( $this->exists( $id, $group ) ) { 512 | $this->replace( $key, $data, $group, $expire ); 513 | } elseif ( empty( $this->nonpersistent_groups[ $group ] ) ) { 514 | $data = maybe_serialize( $data ); 515 | 516 | $expires = $expire ? time() + (int) $expire : 0; 517 | 518 | $this->dbh->insert( 519 | $this->dbh->training_object_cache, 520 | array( 521 | 'cache_key' => $id, 522 | 'cache_group' => $group, 523 | 'data' => $data, 524 | 'TTL' => $expires ? human_time_diff( $expires ) : '-', 525 | 'expires' => $expires, 526 | 'size' => size_format( mb_strlen( $data, '8bit' ) ), 527 | ) 528 | ); 529 | 530 | unset( $this->not_cached[ $group ][ $id ] ); 531 | 532 | // For fidelity with retrieval of serialized string from database. 533 | $this->cache[ $group ][ $id ] = maybe_unserialize( $data ); 534 | } 535 | 536 | return true; 537 | } 538 | 539 | /** 540 | * Echoes the cache hits and cache misses. Also prints every cached group and the size of its data. 541 | */ 542 | public function stats() { 543 | $stats = $this->getStats(); 544 | 545 | echo '
';
546 | echo "Cache Hits: {$stats['cache_hits']}
";
547 | echo "Cache Misses: {$stats['cache_misses']}
";
548 | echo '
Groups:
"; 551 | echo '%s
(%s–%s, %sk)