├── .editorconfig ├── .github └── workflows │ └── continuous-integration.yml ├── .psalm └── constants.php ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── CachePool.php ├── CachePoolFactory.php ├── CachePoolFactoryInterface.php ├── Exception ├── CacheException.php └── InvalidArgumentException.php ├── SilentPool.php └── SilentPoolFactory.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [*.js] 10 | indent_size = 2 11 | 12 | [*.json] 13 | indent_size = 2 14 | 15 | [*.yml] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | php-versions: 9 | - '7.4' 10 | - '8.0' 11 | - '8.1' 12 | - '8.2' 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php-versions }} 21 | 22 | - name: Analysing source code 23 | run: find ./src/ ./inc/ ./tests/ -type f -name '*.php' -print0 | xargs -0 -L 1 -P 4 -- php -l 24 | 25 | - name: Validate composer.json and composer.lock 26 | run: composer validate 27 | 28 | - name: Install dependencies 29 | uses: ramsey/composer-install@v1 30 | with: 31 | dependency-versions: highest 32 | composer-options: "--prefer-dist" 33 | 34 | - name: PHPUnit 35 | run: ./vendor/bin/phpunit 36 | 37 | - name: Psalm 38 | run: ./vendor/bin/psalm --show-info=false --threads=8 --diff 39 | 40 | - name: PHPCS 41 | run: ./vendor/bin/phpcs -s --report-source --runtime-set ignore_warnings_on_exit 1 42 | -------------------------------------------------------------------------------- /.psalm/constants.php: -------------------------------------------------------------------------------- 1 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WP Transient Cache 2 | 3 | [![Build Status](https://travis-ci.com/wp-oop/transient-cache.svg?branch=develop)](https://travis-ci.org/wp-oop/transient-cache) 4 | [![Latest Stable Version](https://poser.pugx.org/wp-oop/transient-cache/version)](https://packagist.org/packages/wp-oop/transient-cache) 5 | [![Latest Unstable Version](https://poser.pugx.org/wp-oop/transient-cache/v/unstable)](//packagist.org/packages/wp-oop/transient-cache) 6 | 7 | A fully compliant [PSR-16][] wrapper for WP transients. 8 | 9 | ## Details 10 | A common means of caching values in WordPress is by using [transients][transients-api]. However, this approach suffers 11 | from several problems: 12 | 13 | 1. Coupling to WordPress. You can't just suddenly substitute the caching mechanism you are using for another mechanism, 14 | and everything still works. 15 | 2. No namespacing. All transients live in the same namespace, and independent consumers cannot reliably use 16 | arbitrary keys without the risk of possible conflict. 17 | 3. No true modularity. Due to the above, if your application is [modular][`dhii/module-interface`], it cannot 18 | decide which caching mechanisms to use for what, because that would have already been decided by your modules. 19 | 4. Missing features. For example, it is not possible to clear all values related to a particular thing in one go. 20 | Exceptions are missing too, and you have to rely on ambiguous return values. 21 | 22 | This standards-compliant wrapper addresses all of the above. It is a true PSR-16 cache, which uses WordPress 23 | transients as storage. Exceptions are raised, interfaces implemented, and true false-negative detection is in place. 24 | Each instance of the cache pool is logically independent from other instances, provided that it is given a unique 25 | name. The application is once again in control, and modules that use cache can become platform agnostic. 26 | 27 | ### Compatibility 28 | - `CachePool` and `CachePoolFactory` offer best-practices error handling, throwing meaningful exceptions 29 | when something goes wrong. This violates PSR-16, but allows you to know what is failing. 30 | - `SilentPool` and `SilentPoolFactory` offer PSR-16 compatibility at the cost of error handling, 31 | hiding exceptions, and returning standards-compatible values. This complies with PSR-16, but at the cost of 32 | clarity and verbosity. 33 | 34 | ### Usage 35 | ```php 36 | /* 37 | * Set up the factory - usually in a service definition 38 | */ 39 | use wpdb; 40 | use Psr\SimpleCache\CacheInterface; 41 | use WpOop\TransientCache\CachePoolFactory; 42 | use WpOop\TransientCache\SilentPoolFactory; 43 | 44 | /* @var $wpdb wpdb */ 45 | $factory = new CachePoolFactory($wpdb); 46 | // Optionally hide exceptions for PSR-16 compatibility 47 | $factory = new SilentPoolFactory($factory); // Optional, and not recommended for testing environments! 48 | 49 | /* 50 | * Create cache pools - usually somewhere else 51 | */ 52 | // Same wpdb instance used, default value generated automatically 53 | $pool1 = $factory->createCachePool('client-access-tokens'); 54 | $pool2 = $factory->createCachePool('remote-api-responses'); 55 | $pool3 = $factory->createCachePool('other-stuff'); 56 | 57 | /* 58 | * Use cache pools - usually injected into a client class 59 | */ 60 | 61 | // No collision of key between different pools 62 | $pool1->set('123', $someToken); 63 | $pool2->set('123', $someResponseBody); 64 | $pool3->set('123', false); 65 | 66 | // Depend on an interop standard 67 | (function (CacheInterface $cache) { 68 | // False negative detection: correctly determines that the value is actually `false` 69 | $cache->has('123'); // true 70 | $cache->get('123', uniqid('default')) === false; // true 71 | })($pool3); 72 | 73 | // Clear all values within a pool 74 | $pool2->clear(); 75 | $pool2->has('123'); // false 76 | $pool1->has('123'); // true 77 | ``` 78 | 79 | ### Limitations 80 | #### Key Length 81 | Due to the way the underlying backend (the WordPress transients via options) works, **the combined length of the 82 | pool name and cache key MUST NOT exceed a 171 char limit**. This is because (at least in WP 5.0+) 83 | the [length of the `option_name` field of the `options` table is 191 chars][1], and transients require the longest 84 | prefix of `_transient_timeout_` to the option name, which together with the 1-char separator is 20 chars. Using 85 | anything greater than this length will result in potentially devastating behaviour described in [Trac #15058][]. 86 | 87 | In any case, the general recommendation is that **consumers SHOULD NOT use cache keys longer than 64 chars**, 88 | as this is the minimal length required for support by the PSR-16 spec. Using anything longer than that will 89 | cause consumers to become dependent on implementation detail, which breaks interoperability. 90 | Given that, **the cache pool name SHOULD NOT exceed 107 chars**. 91 | 92 | #### Value Length 93 | The storage backend (WP options) [declares][2] the corresponding field to be of type [`LONGTEXT`][], which 94 | [allows][3] up to **4 GB** (232) of data. This is therefore the limit on cache values. 95 | 96 | 97 | [transients-api]: https://codex.wordpress.org/Transients_API 98 | [`dhii/module-interface`]: https://github.com/Dhii/module-interface 99 | 100 | [PSR-16]: https://www.php-fig.org/psr/psr-16/ 101 | [`LONGTEXT`]: https://dev.mysql.com/doc/refman/8.0/en/blob.html 102 | 103 | [1]: https://github.com/WordPress/WordPress/blob/5.0-branch/wp-admin/includes/schema.php#L142 104 | [2]: https://github.com/WordPress/WordPress/blob/master/wp-admin/includes/schema.php#L144 105 | [3]: https://dev.mysql.com/doc/refman/8.0/en/storage-requirements.html#data-types-storage-reqs-strings 106 | [Trac #15058]: https://core.trac.wordpress.org/ticket/15058 107 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wp-oop/transient-cache", 3 | "description": "A PSR-16 wrapper for WP transients.", 4 | "minimum-stability": "dev", 5 | "license": "GPL-2.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "Anton Ukhanev", 9 | "email": "xedin.unknown@gmail.com", 10 | "role": "Developer" 11 | } 12 | ], 13 | "require": { 14 | "php": "^7.4 | ^8.0", 15 | "psr/simple-cache": "^1.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^9.0", 19 | "johnpbloch/wordpress-core": "^5.0", 20 | "php-stubs/wordpress-stubs": "6.3", 21 | "brain/monkey": "^2.3", 22 | "vimeo/psalm": "^5.0", 23 | "slevomat/coding-standard": "^6.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "WpOop\\TransientCache\\": "src" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "WpOop\\TransientCache\\Tests\\Func\\": "tests/functional" 33 | } 34 | }, 35 | "extra": { 36 | "branch-alias": { 37 | "dev-develop": "0.1.x-dev" 38 | } 39 | }, 40 | "config": { 41 | "allow-plugins": { 42 | "dealerdirect/phpcodesniffer-composer-installer": true 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/CachePool.php: -------------------------------------------------------------------------------- 1 | wpdb = $wpdb; 107 | $this->poolName = $poolName; 108 | $this->defaultValue = $defaultValue; 109 | $this->defaultTtl = $defaultTtl; 110 | } 111 | 112 | /** 113 | * @inheritDoc 114 | * 115 | * @throws CacheException If problem retrieving. 116 | */ 117 | public function get($key, $default = null) 118 | { 119 | $this->validateKey($key); 120 | $transientKey = $this->prepareKey($key); 121 | 122 | try { 123 | $value = $this->getTransient($transientKey); 124 | } catch (RangeException $e) { 125 | return $default; 126 | } catch (RuntimeException $e) { 127 | $message = sprintf('Could not retrieve cache for key "%1$s": %2$s', $key, $e->getMessage()); 128 | throw new CacheException($message, 0, $e); 129 | } 130 | 131 | return $value; 132 | } 133 | 134 | /** 135 | * @inheritDoc 136 | * 137 | * @throws CacheException If TTL cannot be normalized to a number of seconds. 138 | * @throws InvalidArgumentException If TTL is invalid. 139 | */ 140 | public function set($key, $value, $ttl = null) 141 | { 142 | $this->validateKey($key); 143 | $origKey = $key; 144 | $key = $this->prepareKey($key); 145 | 146 | $ttl = is_null($ttl) ? $this->defaultTtl : $ttl; 147 | 148 | try { 149 | $ttl = $ttl instanceof DateInterval 150 | ? $this->getIntervalDuration($ttl) 151 | : $ttl; 152 | } catch (Exception $e) { 153 | throw new CacheException(sprintf('Could not normalize cache TTL: %s', $e->getMessage())); 154 | } 155 | 156 | if (!is_int($ttl)) { 157 | throw new InvalidArgumentException('The specified cache TTL is invalid'); 158 | } 159 | 160 | try { 161 | $this->setTransient($key, $value, $ttl); 162 | } catch (RuntimeException $e) { 163 | $message = sprintf('Could not write value for key "%1$s" to cache: %2$s', $origKey, $e->getMessage()); 164 | throw new CacheException($message, 0, $e); 165 | } 166 | 167 | return true; 168 | } 169 | 170 | /** 171 | * @inheritDoc 172 | * 173 | * @throws CacheException If problem deleting. 174 | */ 175 | public function delete($key) 176 | { 177 | $this->validateKey($key); 178 | $origKey = $key; 179 | $key = $this->prepareKey($key); 180 | 181 | try { 182 | $this->deleteTransient($key); 183 | } catch (Exception $e) { 184 | $message = sprintf('Failed to delete cache for key "%1$s": %2$s', $origKey, $e->getMessage()); 185 | throw new CacheException($message, 0, $e); 186 | } 187 | 188 | return true; 189 | } 190 | 191 | /** 192 | * @inheritDoc 193 | * 194 | * @throws CacheException If problem clearing. 195 | */ 196 | public function clear() 197 | { 198 | /** @psalm-suppress InvalidCatch */ 199 | try { 200 | $keys = $this->getAllKeys(); 201 | $this->deleteMultiple($keys); 202 | } catch (Exception | InvalidArgumentExceptionInterface $e) { 203 | throw new CacheException(sprintf('Failed to clear cache: %s', $e->getMessage()), 0, $e); 204 | } 205 | 206 | return true; 207 | } 208 | 209 | /** 210 | * @inheritDoc 211 | * 212 | * @param iterable $keys 213 | * 214 | * @throws CacheException If problem retrieving. 215 | * 216 | * @psalm-suppress MoreSpecificImplementedParamType 217 | */ 218 | public function getMultiple($keys, $default = null) 219 | { 220 | if (!is_iterable($keys)) { 221 | throw new InvalidArgumentException('List of keys is not an iterable value'); 222 | } 223 | 224 | $entries = []; 225 | foreach ($keys as $key) { 226 | $value = $this->get($key, $default); 227 | $entries[$key] = $value; 228 | } 229 | 230 | return $entries; 231 | } 232 | 233 | /** 234 | * @inheritDoc 235 | * 236 | * @param iterable $values 237 | * @param null|int|DateInterval $ttl 238 | * 239 | * @throws CacheException If problem persisting. 240 | * 241 | * @psalm-suppress MoreSpecificImplementedParamType 242 | */ 243 | public function setMultiple($values, $ttl = null) 244 | { 245 | if (!is_iterable($values)) { 246 | throw new InvalidArgumentException('List of keys is not an iterable value'); 247 | } 248 | 249 | try { 250 | $ttl = $ttl instanceof DateInterval 251 | ? $this->getIntervalDuration($ttl) 252 | : $ttl; 253 | } catch (Exception $e) { 254 | throw new CacheException(sprintf('Could not normalize cache TTL: %s', $e->getMessage())); 255 | } 256 | 257 | foreach ($values as $key => $value) { 258 | $this->set($key, $value, $ttl); 259 | } 260 | 261 | return true; 262 | } 263 | 264 | /** 265 | * @inheritDoc 266 | * 267 | * @param iterable $keys 268 | * 269 | * @throws CacheException If problem deleting. 270 | * 271 | * @psalm-suppress MoreSpecificImplementedParamType 272 | */ 273 | public function deleteMultiple($keys) 274 | { 275 | if (!is_iterable($keys)) { 276 | throw new InvalidArgumentException('List of keys is not an iterable value'); 277 | } 278 | 279 | foreach ($keys as $key) { 280 | $this->delete($key); 281 | } 282 | 283 | return true; 284 | } 285 | 286 | /** 287 | * @inheritDoc 288 | * 289 | * @throws CacheException If problem determining. 290 | */ 291 | public function has($key) 292 | { 293 | $default = $this->defaultValue; 294 | $value = $this->get($key, $default); 295 | 296 | return $value !== $default; 297 | } 298 | 299 | /** 300 | * Retrieves a transient value, by key. 301 | * 302 | * @param string $key The transient key. 303 | * 304 | * @return mixed The transient value. 305 | * 306 | * @throws RangeException If transient for key not found. 307 | * @throws RuntimeException If problem retrieving. 308 | */ 309 | protected function getTransient(string $key) 310 | { 311 | $value = $this->getTransientOriginal($key); 312 | 313 | if ($value !== false) { 314 | return $value; 315 | } 316 | 317 | $prefix = static::OPTION_NAME_PREFIX_TRANSIENT; 318 | $optionKey = "{$prefix}{$key}"; 319 | 320 | try { 321 | $this->getOption($optionKey); 322 | } catch (RangeException $e) { 323 | throw new RangeException(sprintf('Transient for key "%1$s" does not exist', $key), 0, $e); 324 | } catch (RuntimeException $e) { 325 | throw new RuntimeException(sprintf('Could not verify existence of transient "%1$s"', $key), 0, $e); 326 | } 327 | 328 | return $value; 329 | } 330 | 331 | /** 332 | * Retrieves a transient value as is. 333 | * 334 | * @param string $key The transient key. 335 | * 336 | * @return mixed The transient value. 337 | */ 338 | protected function getTransientOriginal(string $key) 339 | { 340 | $value = get_transient($key); 341 | 342 | return $value; 343 | } 344 | 345 | /** 346 | * Assigns a transient value, by key. 347 | * 348 | * @param string $key The transient key. 349 | * @param mixed $value The transient value. Any serializable object. 350 | * @param int $ttl The amount of seconds after which the transient will expire. 351 | * 352 | * @throws RangeException If key invalid. 353 | * @throws RuntimeException If problem setting. 354 | */ 355 | protected function setTransient(string $key, $value, int $ttl): void 356 | { 357 | $this->validateTransientKey($key); 358 | 359 | if (!set_transient($key, $value, $ttl)) { 360 | throw new RuntimeException(sprintf('set_transient() failed with key "%1$s" with TTL %2$ss', $key, $ttl)); 361 | } 362 | } 363 | 364 | /** 365 | * Retrieves an option value by name. 366 | * 367 | * @param string $key The option name. 368 | * 369 | * @return mixed The option value. 370 | * 371 | * @throws RangeException If option value does not exist. 372 | * @throws RuntimeException If problem retrieving option. 373 | */ 374 | protected function getOption(string $key) 375 | { 376 | $errorValue = $this->defaultValue; 377 | $value = $this->getOptionOriginal($key, $errorValue); 378 | 379 | if ($value === $errorValue) { 380 | throw new RangeException(sprintf('Option for key "%1$s" does not exist', $key)); 381 | } 382 | 383 | return $value; 384 | } 385 | 386 | /** 387 | * Retrieves an option value by name. 388 | * 389 | * @param string $key The option key. 390 | * @param mixed $default The value to return if option not found. 391 | * 392 | * @return mixed The option value. 393 | */ 394 | protected function getOptionOriginal(string $key, $default = null) 395 | { 396 | return get_option($key, $default); 397 | } 398 | 399 | /** 400 | * Deletes a transient with the specified key. 401 | * 402 | * @param string $key The key to delete a transient for. 403 | * 404 | * @throws RuntimeException If problem deleting. 405 | */ 406 | protected function deleteTransient(string $key): void 407 | { 408 | if (!delete_transient($key)) { 409 | throw new RuntimeException(sprintf('delete_transient() failed for key "%1$s"', $key)); 410 | } 411 | } 412 | 413 | /** 414 | * Validates a cache key. 415 | * 416 | * @param string $key The key to validate. 417 | * 418 | * @throws InvalidArgumentException If key is invalid. 419 | */ 420 | protected function validateKey(string $key): void 421 | { 422 | $prefix = $this->getTimeoutOptionNamePrefix(); 423 | if (strlen("{$prefix}{$key}") > static::OPTION_NAME_MAX_LENGTH) { 424 | throw new InvalidArgumentException(sprintf( 425 | 'Given the %1$d char length of this cache pool\'s name, the key length must not exceed %2$d chars', 426 | strlen($this->poolName), 427 | static::OPTION_NAME_MAX_LENGTH - strlen($prefix) 428 | )); 429 | } 430 | 431 | $reservedSymbols = str_split(static::RESERVED_KEY_SYMBOLS, 1); 432 | 433 | foreach ($reservedSymbols as $symbol) { 434 | if (strpos($key, $symbol) !== false) { 435 | throw new InvalidArgumentException(sprintf('Cache key "%1$s" is invalid', $key)); 436 | } 437 | } 438 | } 439 | 440 | /** 441 | * Validates a transient key. 442 | * 443 | * @param string $key The key to validate. 444 | * 445 | * @throws RangeException If key is invalid. 446 | */ 447 | protected function validateTransientKey(string $key): void 448 | { 449 | $maxLength = $this->getTransientKeyMaxLength(); 450 | $keyLength = strlen($key); 451 | if ($keyLength > $maxLength) { 452 | throw new RangeException( 453 | sprintf( 454 | 'Transient key "%1$s" length is %2$d chars, which exceeds max length of %3$d chars', 455 | $key, 456 | $keyLength, 457 | $maxLength 458 | ) 459 | ); 460 | } 461 | } 462 | 463 | /** 464 | * Retrieves the amount of characters at most allowed in a transient key. 465 | * 466 | * @return int The amount of characters. 467 | */ 468 | protected function getTransientKeyMaxLength(): int 469 | { 470 | $longestPrefix = $this->getTransientTimeoutOptionNamePrefix(); 471 | $keyMaxLength = static::OPTION_NAME_MAX_LENGTH - strlen($longestPrefix); 472 | 473 | return $keyMaxLength; 474 | } 475 | 476 | /** 477 | * Prepares a cache key, giving it a namespace. 478 | * 479 | * @param string $key The key to prepare. 480 | * 481 | * @return string The prepared key. 482 | */ 483 | protected function prepareKey(string $key): string 484 | { 485 | $namespace = $this->poolName; 486 | $separator = static::NAMESPACE_SEPARATOR; 487 | return "{$namespace}{$separator}{$key}"; 488 | } 489 | 490 | /** 491 | * Retrieves all keys that correspond to this cache pool. 492 | * 493 | * @throws Exception If problem retrieving. 494 | * 495 | * @return iterable A list of keys. 496 | */ 497 | protected function getAllKeys(): iterable 498 | { 499 | $tableName = $this->getTableName(static::TABLE_NAME_OPTIONS); 500 | $fieldName = static::FIELD_NAME_OPTION_NAME; 501 | $prefix = $this->getOptionNamePrefix(); 502 | $query = "SELECT `$fieldName` FROM `$tableName` WHERE `$fieldName` LIKE '$prefix%'"; 503 | $results = $this->selectColumn($query, $fieldName); 504 | $keys = $this->getCacheKeysFromOptionNames($results); 505 | 506 | return $keys; 507 | } 508 | 509 | /** 510 | * Runs a SELECT query, and retrieves a list of values for a field with the specified name. 511 | * 512 | * @param string $query The SELECT query. 513 | * @param string $columnName The name of the field to retrieve. 514 | * @param array $args Query parameters. 515 | * 516 | * @return iterable The list of values for the specified field. 517 | */ 518 | protected function selectColumn(string $query, string $columnName, array $args = []): iterable 519 | { 520 | $query = $this->prepareQuery($query, $args); 521 | /** @var list> $results */ 522 | $results = $this->wpdb->get_results($query, ARRAY_A); 523 | 524 | return array_map(function ($row) use ($columnName) { 525 | return $row[$columnName] ?? null; 526 | }, $results); 527 | } 528 | 529 | /** 530 | * Retrieve the name of a DB table by its identifier. 531 | * 532 | * @param string $identifier The table identifier. 533 | * 534 | * @return string The table name in the DB. 535 | */ 536 | protected function getTableName(string $identifier): string 537 | { 538 | $prefix = $this->wpdb->prefix; 539 | $tableName = "{$prefix}{$identifier}"; 540 | 541 | return $tableName; 542 | } 543 | 544 | /** 545 | * Prepares a parameterized query. 546 | * 547 | * @param string $query The query to prepare. May include placeholders. 548 | * @param array $params The parameters that will replace corresponding placeholders in the query. 549 | * 550 | * @return string The prepared query. Parameters will be interpolated. 551 | */ 552 | protected function prepareQuery(string $query, array $params = []): string 553 | { 554 | if (empty($params)) { 555 | return $query; 556 | } 557 | 558 | $prepared = $this->wpdb->prepare($query, ...$params); 559 | 560 | return $prepared; 561 | } 562 | 563 | /** 564 | * Retrieves all cache keys that correspond to the given list of option names 565 | * 566 | * @param iterable $optionNames 567 | * 568 | * @throws Exception If problem retrieving. 569 | * 570 | * @return iterable A list of cache keys. 571 | */ 572 | protected function getCacheKeysFromOptionNames(iterable $optionNames): iterable 573 | { 574 | $keys = []; 575 | 576 | foreach ($optionNames as $name) { 577 | $key = $this->getCacheKeyFromOptionName($name); 578 | $keys[] = $key; 579 | } 580 | 581 | return $keys; 582 | } 583 | 584 | /** 585 | * Retrieves the prefix of option names that represent transients of this cache pool. 586 | * 587 | * @return string The prefix. 588 | */ 589 | protected function getOptionNamePrefix(): string 590 | { 591 | $transientPrefix = static::OPTION_NAME_PREFIX_TRANSIENT; 592 | $separator = static::NAMESPACE_SEPARATOR; 593 | $namespace = $this->poolName; 594 | $prefix = "{$transientPrefix}{$namespace}{$separator}"; 595 | 596 | return $prefix; 597 | } 598 | 599 | /** 600 | * Retrieves the prefix of option names that represent transient timeouts of this cache pool. 601 | * 602 | * @return string The prefix. 603 | */ 604 | protected function getTimeoutOptionNamePrefix(): string 605 | { 606 | $transientPrefix = $this->getTransientTimeoutOptionNamePrefix(); 607 | $separator = static::NAMESPACE_SEPARATOR; 608 | $namespace = $this->poolName; 609 | $prefix = "{$transientPrefix}{$namespace}{$separator}"; 610 | 611 | return $prefix; 612 | } 613 | 614 | /** 615 | * Retrieves the prefix of an option name that represents a transient timeout. 616 | * 617 | * This is the longest prefix of transient options. 618 | * 619 | * @return string The prefix. 620 | */ 621 | protected function getTransientTimeoutOptionNamePrefix(): string 622 | { 623 | return static::OPTION_NAME_PREFIX_TRANSIENT . static::OPTION_NAME_PREFIX_TIMEOUT; 624 | } 625 | 626 | /** 627 | * Retrieves the cache key that corresponds to the specified option name. 628 | * 629 | * @param string $name The option name. 630 | * 631 | * @return string The cache key. 632 | * 633 | * @throws Exception If problem determining key. 634 | */ 635 | protected function getCacheKeyFromOptionName(string $name): string 636 | { 637 | $prefix = $this->getOptionNamePrefix(); 638 | 639 | if (strpos($name, $prefix) !== 0) { 640 | throw new RangeException(sprintf('Option name "%1$s" is not formed according to this cache pool', $name)); 641 | } 642 | 643 | $key = substr($name, strlen($prefix)); 644 | 645 | if ($key === false) { 646 | throw new UnexpectedValueException( 647 | sprintf('Could not extract key with prefix "%1$s" from option name "%2$s', $prefix, $name) 648 | ); 649 | } 650 | 651 | return $key; 652 | } 653 | 654 | /** 655 | * Retrieves the total duration from an interval. 656 | * 657 | * @param DateInterval $interval The interval. 658 | * 659 | * @throws Exception If problem retrieving. 660 | * 661 | * @return int The duration in seconds. 662 | */ 663 | protected function getIntervalDuration(DateInterval $interval): int 664 | { 665 | $reference = new DateTimeImmutable(); 666 | $endTime = $reference->add($interval); 667 | 668 | return $endTime->getTimestamp() - $reference->getTimestamp(); 669 | } 670 | } 671 | -------------------------------------------------------------------------------- /src/CachePoolFactory.php: -------------------------------------------------------------------------------- 1 | wpdb = $wpdb; 34 | $this->defaultTtl = $defaultTtl; 35 | } 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | public function createCachePool(string $poolName): CacheInterface 41 | { 42 | $default = uniqid('default'); 43 | $pool = new CachePool($this->wpdb, $poolName, $default, $this->defaultTtl); 44 | 45 | return $pool; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/CachePoolFactoryInterface.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 29 | } 30 | 31 | /** 32 | * @inheritDoc 33 | */ 34 | public function get($key, $default = null) 35 | { 36 | try { 37 | return $this->cache->get($key); 38 | } catch (Exception $e) { 39 | if ($e instanceof InvalidArgumentExceptionInterface) { 40 | throw $e; 41 | } 42 | 43 | return $default; 44 | } 45 | } 46 | 47 | /** 48 | * @inheritDoc 49 | */ 50 | public function set($key, $value, $ttl = null) 51 | { 52 | try { 53 | return $this->cache->set($key, $value, $ttl); 54 | } catch (Exception $e) { 55 | if ($e instanceof InvalidArgumentExceptionInterface) { 56 | throw $e; 57 | } 58 | 59 | return false; 60 | } 61 | } 62 | 63 | /** 64 | * @inheritDoc 65 | */ 66 | public function delete($key) 67 | { 68 | try { 69 | return $this->cache->delete($key); 70 | } catch (Exception $e) { 71 | if ($e instanceof InvalidArgumentExceptionInterface) { 72 | throw $e; 73 | } 74 | 75 | return false; 76 | } 77 | } 78 | 79 | /** 80 | * @inheritDoc 81 | */ 82 | public function clear() 83 | { 84 | try { 85 | return $this->cache->clear(); 86 | } catch (Exception $e) { 87 | return false; 88 | } 89 | } 90 | 91 | /** 92 | * @inheritDoc 93 | */ 94 | public function getMultiple($keys, $default = null) 95 | { 96 | try { 97 | return $this->cache->getMultiple($keys, $default); 98 | } catch (Exception $e) { 99 | if ($e instanceof InvalidArgumentExceptionInterface) { 100 | throw $e; 101 | } 102 | 103 | return []; 104 | } 105 | } 106 | 107 | /** 108 | * @inheritDoc 109 | */ 110 | public function setMultiple($values, $ttl = null) 111 | { 112 | try { 113 | return $this->cache->setMultiple($values, $ttl); 114 | } catch (Exception $e) { 115 | if ($e instanceof InvalidArgumentExceptionInterface) { 116 | throw $e; 117 | } 118 | 119 | return false; 120 | } 121 | } 122 | 123 | /** 124 | * @inheritDoc 125 | */ 126 | public function deleteMultiple($keys) 127 | { 128 | try { 129 | return $this->cache->deleteMultiple($keys); 130 | } catch (Exception $e) { 131 | if ($e instanceof InvalidArgumentExceptionInterface) { 132 | throw $e; 133 | } 134 | 135 | return false; 136 | } 137 | } 138 | 139 | /** 140 | * @inheritDoc 141 | */ 142 | public function has($key) 143 | { 144 | try { 145 | return $this->cache->has($key); 146 | } catch (Exception $e) { 147 | if ($e instanceof InvalidArgumentExceptionInterface) { 148 | throw $e; 149 | } 150 | 151 | return false; 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/SilentPoolFactory.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | public function createCachePool(string $poolName): CacheInterface 31 | { 32 | return new SilentPool($this->factory->createCachePool($poolName)); 33 | } 34 | } 35 | --------------------------------------------------------------------------------