felixarntz/wp-psr-cache

View on GitHub
src/ObjectCache.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
/**
 * Class ObjectCache
 *
 * @package LeavesAndLove\WpPsrCache
 * @license GNU General Public License, version 2
 * @link    https://github.com/felixarntz/wp-psr-cache
 */

namespace LeavesAndLove\WpPsrCache;

use LeavesAndLove\WpPsrCache\CacheAdapter\CacheAdapter;
use LeavesAndLove\WpPsrCache\CacheKeyGen\CacheKeyGen;
use LeavesAndLove\WpPsrCache\CacheSelector\CacheSelector;

/**
 * WordPress object cache class.
 *
 * @since 1.0.0
 */
final class ObjectCache
{

    const DEFAULT_GROUP = 'default';

    /** @var CacheSelector The selector to detect which cache to use. */
    private $selector;

    /** @var CacheKeyGen The key generator. */
    private $keygen;

    /** @var int Amount of times the cache data was already stored in the cache. */
    private $cacheHits = 0;

    /** @var int Amount of times the cache data was not stored in the cache. */
    private $cacheMisses = 0;

    /**
     * Constructor.
     *
     * Set the cache adapters to use for persistent and non-persistent caches.
     *
     * @since 1.0.0
     *
     * @param CacheSelector $selector Selector to detect which cache to use.
     * @param CacheKeyGen   $keygen   Key generator.
     */
    public function __construct(CacheSelector $selector, CacheKeyGen $keygen)
    {
        $this->selector = $selector;
        $this->keygen   = $keygen;
    }

    /**
     * Obtain a value from the cache.
     *
     * @since 1.0.0
     *
     * @param string $key    The key of this item in the cache.
     * @param string $group  Optional. The group of this item in the cache. Default 'default'.
     * @param bool   $force  Optional. Whether to force an update of the non-persistent cache
     *                       from the persistent cache. Default false.
     * @param bool   &$found Optional. Whether the key was found in the cache (passed by reference).
     *                       Disambiguates a return of false, a storable value. Default false.
     * @return mixed The value of the item from the cache, or false in case of cache miss.
     */
    public function get(string $key, string $group = self::DEFAULT_GROUP, bool $force = false, bool &$found = false)
    {
        $group = $this->parseDefaultGroup($group);
        $key   = $this->keygen->generate($key, $group);

        $nonPersistentCache = $this->selector->selectNonPersistentCache($group);

        $found = false;

        $nonPersistent = $this->selector->isNonPersistentGroup($group);
        if ($nonPersistent || !$force) {
            if ($nonPersistentCache->has($key)) {
                $this->cacheHits++;
                $found = true;
                return $nonPersistentCache->get($key);
            }

            if ($nonPersistent) {
                $this->cacheMisses++;
                return false;
            }
        }

        $persistentCache = $this->selector->selectPersistentCache($group);

        if ($persistentCache->has($key)) {
            $this->cacheHits++;
            $found = true;
            $value = $persistentCache->get($key);

            $nonPersistentCache->set($key, $value);

            return $value;
        }

        $this->cacheMisses++;
        return false;
    }

    /**
     * Store a value in the cache.
     *
     * @since 1.0.0
     *
     * @param string $key        The key of the item to store.
     * @param mixed  $value      The value of the item to store. Must be serializable.
     * @param string $group      Optional. The group of the item to store. Default 'default'.
     * @param int    $expiration Optional. When to expire the value, passed in seconds. Default 0 (no expiration).
     * @return bool True on success, false on failure.
     */
    public function set(string $key, $value, string $group = self::DEFAULT_GROUP, int $expiration = 0): bool
    {
        $group = $this->parseDefaultGroup($group);
        $key   = $this->keygen->generate($key, $group);

        $nonPersistentCache = $this->selector->selectNonPersistentCache($group);

        if ($this->selector->isNonPersistentGroup($group)) {
            return $nonPersistentCache->set($key, $value, $expiration);
        }

        $persistentCache = $this->selector->selectPersistentCache($group);

        if ($persistentCache->set($key, $value, $expiration)) {
            $nonPersistentCache->set($key, $value, $expiration);

            return true;
        }

        return false;
    }

    /**
     * Store a value in the cache if its key is not already set.
     *
     * @since 1.0.0
     *
     * @param string $key        The key of the item to store.
     * @param mixed  $value      The value of the item to store. Must be serializable.
     * @param string $group      Optional. The group of the item to store. Default 'default'.
     * @param int    $expiration Optional. When to expire the value, passed in seconds. Default 0 (no expiration).
     * @return bool True on success, false on failure.
     */
    public function add(string $key, $value, string $group = self::DEFAULT_GROUP, int $expiration = 0): bool
    {
        $group = $this->parseDefaultGroup($group);
        $key   = $this->keygen->generate($key, $group);

        $nonPersistentCache = $this->selector->selectNonPersistentCache($group);

        if ($this->selector->isNonPersistentGroup($group)) {
            return !$nonPersistentCache->has($key) && $nonPersistentCache->set($key, $value, $expiration);
        }

        $persistentCache = $this->selector->selectPersistentCache($group);

        if (!$persistentCache->has($key) && $persistentCache->set($key, $value, $expiration)) {
            $nonPersistentCache->set($key, $value, $expiration);

            return true;
        }

        return false;
    }

    /**
     * Store a value in the cache if its key is already set.
     *
     * @since 1.0.0
     *
     * @param string $key        The key of the item to store.
     * @param mixed  $value      The value of the item to store. Must be serializable.
     * @param string $group      Optional. The group of the item to store. Default 'default'.
     * @param int    $expiration Optional. When to expire the value, passed in seconds. Default 0 (no expiration).
     * @return bool True on success, false on failure.
     */
    public function replace(string $key, $value, string $group = self::DEFAULT_GROUP, int $expiration = 0): bool
    {
        $group = $this->parseDefaultGroup($group);
        $key   = $this->keygen->generate($key, $group);

        $nonPersistentCache = $this->selector->selectNonPersistentCache($group);

        if ($this->selector->isNonPersistentGroup($group)) {
            return $nonPersistentCache->has($key) && $nonPersistentCache->set($key, $value, $expiration);
        }

        $persistentCache = $this->selector->selectPersistentCache($group);

        if ($persistentCache->has($key) && $persistentCache->set($key, $value, $expiration)) {
            $nonPersistentCache->set($key, $value, $expiration);

            return true;
        }

        return false;
    }

    /**
     * Increment a numeric value in the cache.
     *
     * @since 1.0.0
     *
     * @param string $key    The key of the item to increment its value.
     * @param int    $offset Optional. The amount by which to increment the value. Default 1.
     * @param string $group  Optional. The group of the item to increment. Default 'default'.
     * @return int|bool The item's new value on success, false on failure.
     */
    public function increment(string $key, int $offset = 1, string $group = self::DEFAULT_GROUP)
    {
        $value = $this->get($key, $group, false, $found);

        if (!$found) {
            return false;
        }

        $value = is_numeric($value) ? $value + $offset : 0;

        // A value below 0 is not allowed.
        $value = $value >= 0 ? $value : 0;

        if ($this->set($key, $value, $group)) {
            return $value;
        }

        return false;
    }

    /**
     * Decrement a numeric value in the cache.
     *
     * @since 1.0.0
     *
     * @param string $key    The key of the item to decrement its value.
     * @param int    $offset Optional. The amount by which to decrement the value. Default 1.
     * @param string $group  Optional. The group of the item to decrement. Default 'default'.
     * @return int|bool The item's new value on success, false on failure.
     */
    public function decrement(string $key, int $offset = 1, string $group = self::DEFAULT_GROUP)
    {
        $value = $this->get($key, $group, false, $found);

        if (!$found) {
            return false;
        }

        $value = is_numeric($value) ? $value - $offset : 0;

        // A value below 0 is not allowed.
        $value = $value >= 0 ? $value : 0;

        if ($this->set($key, $value, $group)) {
            return $value;
        }

        return false;
    }

    /**
     * Delete a value from the cache.
     *
     * @since 1.0.0
     *
     * @param string $key   The key of the item to delete.
     * @param string $group Optional. The group of the item to delete. Default 'default'.
     * @return bool True on success, false on failure.
     */
    public function delete(string $key, string $group = self::DEFAULT_GROUP): bool
    {
        $group = $this->parseDefaultGroup($group);
        $key   = $this->keygen->generate($key, $group);

        $nonPersistentCache = $this->selector->selectNonPersistentCache($group);

        if ($this->selector->isNonPersistentGroup($group)) {
            // If the item is not in the cache, return true.
            return !$nonPersistentCache->has($key) || $nonPersistentCache->delete($key);
        }

        $persistentCache = $this->selector->selectPersistentCache($group);

        // If the item is not in the cache, return true.
        if (!$persistentCache->has($key) || $persistentCache->delete($key)) {
            $nonPersistentCache->delete($key);

            return true;
        }

        return false;
    }

    /**
     * Delete all values from the cache.
     *
     * @since 1.0.0
     *
     * @return bool True on success, false on failure.
     */
    public function flush(): bool
    {
        if ($this->selector->clearPersistent()) {
            $this->selector->clearNonPersistent();

            return true;
        }

        return false;
    }

    /**
     * Determine whether a value is present in the cache.
     *
     * @since 1.0.0
     *
     * @param string $key   The key of the item in the cache.
     * @param string $group Optional. The group of the item in the cache. Default 'default'.
     * @return bool True if the value is present, false otherwise.
     */
    public function has(string $key, string $group = self::DEFAULT_GROUP): bool
    {
        $group = $this->parseDefaultGroup($group);
        $key   = $this->keygen->generate($key, $group);

        $nonPersistentCache = $this->selector->selectNonPersistentCache($group);

        if ($this->selector->isNonPersistentGroup($group)) {
            return $nonPersistentCache->has($key);
        }

        $persistentCache = $this->selector->selectPersistentCache($group);

        return $persistentCache->has($key);
    }

    /**
     * Obtain multiple values from the cache.
     *
     * @since 1.0.0
     *
     * @param array  $keys  The list of keys for the items in the cache.
     * @param string $group Optional. The group of the items in the cache. Default 'default'.
     * @param bool   $force Optional. Whether to force an update of the non-persistent cache
     *                      from the persistent cache. Default false.
     * @return array List of key => value pairs. For cache misses, false will be used as value.
     */
    public function getMultiple(array $keys, string $group = self::DEFAULT_GROUP, bool $force = false): array
    {
        $group    = $this->parseDefaultGroup($group);
        $fullKeys = $this->buildKeys($keys, $group);

        $nonPersistentCache = $this->selector->selectNonPersistentCache($group);

        if ($this->selector->isNonPersistentGroup($group)) {
            $values = array_combine($keys, $nonPersistentCache->getMultiple($fullKeys));

            $this->checkMultipleHitsAndMisses($values);
            return $values;
        }

        if (!$force) {
            $values = $nonPersistentCache->getMultiple($fullKeys);
            $needed = array();
            foreach ($values as $fullKey => $value) {
                if (false !== $value) {
                    continue;
                }

                $needed[] = $fullKey;
            }
        } else {
            $values = array();
            $needed = $fullKeys;
        }

        if (!empty($needed)) {
            $persistentCache = $this->selector->selectPersistentCache($group);

            // For cache misses in original lookup, check the persistent cache.
            $persistentValues = $persistentCache->getMultiple($needed);

            $values = array_merge($values, $persistentValues);
        }

        $values = array_combine($keys, $values);

        $this->checkMultipleHitsAndMisses($values);
        return $values;
    }

    /**
     * Store multiple values in the cache.
     *
     * @since 1.0.0
     *
     * @param array  $values     The list of key => value pairs to store.
     * @param string $group      Optional. The group of the items to store. Default 'default'.
     * @param int    $expiration Optional. When to expire the value, passed in seconds. Default 0 (no expiration).
     * @return bool True on success, false on failure.
     */
    public function setMultiple(array $values, string $group = self::DEFAULT_GROUP, int $expiration = 0): bool
    {
        $group      = $this->parseDefaultGroup($group);
        $fullKeys   = $this->buildKeys(array_keys($values), $group);
        $fullValues = array_combine($fullKeys, $values);

        $nonPersistentCache = $this->selector->selectNonPersistentCache($group);

        if ($this->selector->isNonPersistentGroup($group)) {
            return $nonPersistentCache->setMultiple($fullValues, $expiration);
        }

        $persistentCache = $this->selector->selectPersistentCache($group);

        if ($persistentCache->setMultiple($fullValues, $expiration)) {
            $nonPersistentCache->set($fullValues, $expiration);

            return true;
        }

        return false;
    }

    /**
     * Delete multiple values from the cache.
     *
     * @since 1.0.0
     *
     * @param array  $keys  The list of keys for the items in the cache to delete.
     * @param string $group Optional. The group of the items to delete. Default 'default'.
     * @return bool True on success, false on failure.
     */
    public function deleteMultiple(array $keys, string $group = self::DEFAULT_GROUP): bool
    {
        $group    = $this->parseDefaultGroup($group);
        $fullKeys = $this->buildKeys($keys, $group);

        $nonPersistentCache = $this->selector->selectNonPersistentCache($group);

        if ($this->selector->isNonPersistentGroup($group)) {
            return $nonPersistentCache->deleteMultiple($fullKeys);
        }

        $persistentCache = $this->selector->selectPersistentCache($group);

        if ($persistentCache->deleteMultiple($fullKeys)) {
            $nonPersistentCache->deleteMultiple($fullKeys);

            return true;
        }

        return false;
    }

    /**
     * Get the selector used by the object cache.
     *
     * @since 1.0.0
     *
     * @return CacheSelector Selector instance.
     */
    public function getSelector(): CacheSelector
    {
        return $this->selector;
    }

    /**
     * Get the key generator used by the object cache.
     *
     * @since 1.0.0
     *
     * @return CacheKeyGen Key generator instance.
     */
    public function getKeygen(): CacheKeyGen
    {
        return $this->keygen;
    }

    /**
     * Gets the amount of times the cache data was already stored in the cache.
     *
     * @since 1.0.0
     *
     * @return int Amount of cache hits.
     */
    public function getCacheHits(): int
    {
        return $this->cacheHits;
    }

    /**
     * Gets the amount of times the cache data was not stored in the cache.
     *
     * @since 1.0.0
     *
     * @return int Amount of cache misses.
     */
    public function getCacheMisses(): int
    {
        return $this->cacheMisses;
    }

    /**
     * Output cache-related stats.
     *
     * @since 1.0.0
     */
    public function stats()
    {
        $out = array();

        $out[] = '<p>';
        $out[] = '<strong>Cache Hits:</strong> ' . $this->cacheHits . '<br>';
        $out[] = '<strong>Cache Misses:</strong> ' . $this->cacheMisses . '<br>';
        $out[] = '<strong>Persistent Cache Client:</strong> <code>' . get_class( $this->selector->selectPersistentCache( '' )->getClient() ) . '</code><br>';
        $out[] = '<strong>Non-Persistent Cache Client:</strong> <code>' . get_class( $this->selector->selectNonPersistentCache( '' )->getClient() ) . '</code>';
        $out[] = '</p>';

        echo implode( PHP_EOL, $out );
    }

    /**
     * Magic getter.
     *
     * Allows for backward-compatibility with plugins still doing it wrong.
     *
     * @since 1.0.0
     *
     * @param string $name Property to get.
     * @return mixed Property value.
     */
    public function __get(string $name)
    {
        switch($name) {
            case 'cache_hits':
                return $this->cacheHits;
            case 'cache_misses':
                return $this->cacheMisses;
            case 'global_groups':
                return $this->keygen->getGlobalGroups();
            case 'non_persistent_groups':
                return $this->selector->getNonPersistentGroups();
        }
    }

    /**
     * Magic setter.
     *
     * Allows for backward-compatibility with plugins still doing it wrong.
     *
     * @since 1.0.0
     *
     * @param string $name  Property to set.
     * @param mixed  $value Property value.
     */
    public function __set(string $name, $value)
    {
        switch($name) {
            case 'cache_hits':
                $this->cacheHits = (int) $value;
            case 'cache_misses':
                $this->cacheMisses = (int) $value;
            case 'global_groups':
                $this->keygen->addGlobalGroups((array) $value);
            case 'non_persistent_groups':
                $this->selector->addNonPersistentGroups((array) $value);
        }
    }

    /**
     * Magic isset-er.
     *
     * Allows for backward-compatibility with plugins still doing it wrong.
     *
     * @since 1.0.0
     *
     * @param string $name  Property to check if set.
     * @return bool True if property is set, false otherwise.
     */
    public function __isset(string $name): bool
    {
        switch($name) {
            case 'cache_hits':
            case 'cache_misses':
            case 'global_groups':
            case 'non_persistent_groups':
                return true;
        }

        return false;
    }

    /**
     * Get the default group in case the passed group is empty.
     *
     * @since 1.0.0
     *
     * @param string $group A cache group.
     * @return string The value of $group, or the default group.
     */
    private function parseDefaultGroup(string $group)
    {
        return empty($group) ? self::DEFAULT_GROUP : $group;
    }

    /**
     * Builds full cache keys for given keys and a group.
     *
     * @since 1.0.0
     *
     * @param array  $keys  A list of cache keys.
     * @param string $group The cache group for the keys.
     * @return array The list of full cache keys.
     */
    private function buildKeys(array $keys, string $group): array
    {
        $fullKeys = array();

        foreach ($keys as $key) {
            $fullKeys[] = $this->keygen->generate($key, $group);
        }

        return $fullKeys;
    }

    /**
     * Increases the cache hits and misses by evaluating a result for multiple cache keys.
     *
     * @since 1.0.0
     *
     * @param array $values Array of $key => $value pairs as a cache lookup result.
     */
    private function checkMultipleHitsAndMisses(array $values)
    {
        $foundValues = array_filter($values, function($value) {
            return false !== $value;
        });

        $this->cacheHits   += count($foundValues);
        $this->cacheMisses += count($values) - count($foundValues);
    }
}