$no_remote_groups */ abstract class ObjectCache implements ObjectCacheInterface { /** * The configuration instance. * * @var \RedisCachePro\Configuration\Configuration */ protected $config; /** * The connection instance. * * @var ?\RedisCachePro\Connections\ConnectionInterface */ protected $connection; /** * The logger instance. * * @var \RedisCachePro\Loggers\LoggerInterface */ protected $log; /** * Holds the objects cached in runtime memory. * * @var array> */ protected $cache = []; /** * Holds the object cache metrics. * * @var \RedisCachePro\ObjectCaches\ObjectCacheMetrics */ protected $metrics; /** * Holds an internal cache copy of the configured prefix. * * @var ?string */ protected $prefix; /** * The blog id used as prefix in network environments. * * @var int */ protected $blogId; /** * Whether the environment is a network. * * @var bool */ protected $isMultisite = false; /** * Holds an internal cache copy of whether the connection is a cluster. * * @var ?bool */ protected $isCluster; /** * The list of global cache groups that are not * blog specific in a network environment. * * @var array */ protected $globalGroups = []; /** * The list of non-persistent groups. * * @var array */ protected $nonPersistentGroups = []; /** * The list of non-persistent group matches for fast lookups. * * @var array */ protected $nonPersistentGroupMatches = []; /** * The list of non-prefetchable groups. * * @var array */ protected $nonPrefetchableGroups = []; /** * The list of non-prefetchable group matches for fast lookups. * * @var array */ protected $nonPrefetchableGroupMatches = []; /** * Set up the object cache instance. * * @param \RedisCachePro\Configuration\Configuration $config * @param \RedisCachePro\Connections\ConnectionInterface $connection * @param ?\RedisCachePro\ObjectCaches\ObjectCacheMetrics $metrics * @return void */ protected function setup( Configuration $config, ?ConnectionInterface $connection = null, ?ObjectCacheMetrics $metrics = null ) { $this->config = $config; $this->log = $config->logger; $this->prefix = $config->prefix ?: ''; $this->isCluster = (bool) $config->cluster; $this->connection = $connection; $this->metrics = $metrics ?: new ObjectCacheMetrics($config, $connection); } /** * Returns the configuration instance. * * @return \RedisCachePro\Configuration\Configuration */ public function config(): Configuration { return $this->config; } /** * Returns the connection instance. * * @return \RedisCachePro\Connections\ConnectionInterface|null */ public function connection(): ?ConnectionInterface // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewNullableTypes.returnTypeFound { return $this->connection; } /** * Returns the connection's client. * * @internal * @return \RedisCachePro\Clients\ClientInterface|null */ public function client(): ?ClientInterface // phpcs:ignore PHPCompatibility.FunctionDeclarations.NewNullableTypes.returnTypeFound { return $this->connection ? $this->connection->client() : null; } /** * Returns the client name the object cache is using. * * @return string */ public function clientName() { $client = $this->client(); if (! $client) { return 'Unknown'; } return (new ReflectionClass($client))->getShortName(); } /** * Returns the logger instance. * * @return \RedisCachePro\Loggers\LoggerInterface */ public function logger(): LoggerInterface { return $this->log; } /** * Set given groups as global. * * @param array $groups * @return void */ public function add_global_groups(array $groups) { $this->globalGroups = \array_unique( \array_merge($this->globalGroups, \array_values($groups)) ); } /** * Set given groups as non-persistent. * * @param array $groups * @return void */ public function add_non_persistent_groups(array $groups) { $this->nonPersistentGroups = \array_unique( \array_merge($this->nonPersistentGroups, \array_values($groups)) ); foreach (\array_values($groups) as $group) { if (\strpos($group, '*') === false) { unset($this->nonPersistentGroupMatches[$group]); } else { foreach (\array_keys($this->nonPersistentGroupMatches) as $nonPersistentGroupMatch) { if (\fnmatch($group, $nonPersistentGroupMatch)) { unset($this->nonPersistentGroupMatches[$nonPersistentGroupMatch]); } } } } } /** * Set given groups as non-prefetchable. * * @param array $groups * @return void */ public function add_non_prefetchable_groups(array $groups) { $this->nonPrefetchableGroups = \array_unique( \array_merge($this->nonPrefetchableGroups, \array_values($groups)) ); } /** * Decrement given value by given offset. * * Forces value to be a signed integer. * * @param int|string $value * @param int $offset * @return int */ protected function decrement($value, int $offset): int { if (! \is_int($value)) { $value = 0; } $value -= $offset; return max(0, $value); } /** * Returns an array of all global groups. * * @return array */ public function globalGroups(): array { return $this->globalGroups; } /** * Returns an array of all non-prefetchable groups. * * @return array */ public function nonPrefetchableGroups(): array { return $this->nonPrefetchableGroups; } /** * Returns an array of all non-persistent groups. * * @return array */ public function nonPersistentGroups(): array { return $this->nonPersistentGroups; } /** * Increment given value by given offset. * * Forces value to be a signed integer. * * @param int|string $value * @param int $offset * @return int */ protected function increment($value, int $offset): int { if (! \is_int($value)) { $value = 0; } $value += $offset; return max(0, $value); } /** * Whether the group is a global group. * * @param string $group * @return bool */ public function isGlobalGroup(string $group): bool { return \in_array($group, $this->globalGroups); } /** * Whether the group is persistent. * * @param string $group * @return bool */ public function isPersistentGroup(string $group): bool { if (isset($this->nonPersistentGroupMatches[$group])) { return ! $this->nonPersistentGroupMatches[$group]; } return ! $this->isNonPersistentGroup($group); } /** * Whether the group is non-persistent. * * @param string $group * @return bool */ public function isNonPersistentGroup(string $group): bool { if (isset($this->nonPersistentGroupMatches[$group])) { return $this->nonPersistentGroupMatches[$group]; } foreach ($this->nonPersistentGroups as $nonPersistentGroup) { if (\strpos($nonPersistentGroup, '*') === false) { if ($group === $nonPersistentGroup) { return $this->nonPersistentGroupMatches[$group] = true; } } else { if (\fnmatch($nonPersistentGroup, $group)) { return $this->nonPersistentGroupMatches[$group] = true; } } } return $this->nonPersistentGroupMatches[$group] = false; } /** * Whether the group is prefetchable. * * @param string $group * @return bool */ public function isPrefetchableGroup(string $group): bool { if (isset($this->nonPrefetchableGroupMatches[$group])) { return ! $this->nonPrefetchableGroupMatches[$group]; } return ! $this->isNonPrefetchableGroup($group); } /** * Whether the group is non-prefetchable. * * @param string $group * @return bool */ public function isNonPrefetchableGroup(string $group): bool { if (isset($this->nonPrefetchableGroupMatches[$group])) { return $this->nonPrefetchableGroupMatches[$group]; } foreach ($this->nonPrefetchableGroups as $nonPrefetchableGroup) { if (\strpos($nonPrefetchableGroup, '*') === false) { if ($group === $nonPrefetchableGroup) { return $this->nonPrefetchableGroupMatches[$group] = true; } } else { if (\fnmatch($nonPrefetchableGroup, $group)) { return $this->nonPrefetchableGroupMatches[$group] = true; } } } return $this->nonPrefetchableGroupMatches[$group] = false; } /** * Whether the environment is a network. * * @return bool */ public function isMultisite(): bool { return (bool) $this->isMultisite; } /** * Returns various information about the object cache. * * @return \RedisCachePro\Support\ObjectCacheInfo */ public function info() { global $wp_object_cache_errors; $info = (object) [ 'status' => false, 'groups' => (object) [ 'global' => $this->globalGroups(), 'non_persistent' => $this->nonPersistentGroups(), 'non_prefetchable' => $this->nonPrefetchableGroups(), ], 'errors' => empty($wp_object_cache_errors) ? null : $wp_object_cache_errors, 'meta' => array_filter([ 'Cache' => (new ReflectionClass($this))->getShortName(), 'Logger' => (new ReflectionClass($this->log))->getShortName(), ]), ]; return $info; // @phpstan-ignore-line } /** * Returns metrics about the object cache. * * @return \RedisCachePro\ObjectCaches\ObjectCacheMetrics */ public function metrics() { return $this->metrics->compute($this->cache); } /** * Set the blog id. * * @param int $blogId * @return void */ public function setBlogId(int $blogId) { $this->blogId = $blogId; } /** * Set whether the environment is a network. * * @param bool $isMultisite * @return void */ public function setMultisite(bool $isMultisite) { $this->isMultisite = $isMultisite; } /** * Whether the key was cached in runtime memory. * * @param string $id * @param string $group * @return bool */ protected function hasInMemory(string $id, string $group = 'default') { return isset($this->cache[$group][$id]); } /** * Retrieves the cache contents from the runtime memory cache. * * @param string $id * @param string $group * @return mixed */ protected function getFromMemory(string $id, string $group = 'default') { if (\is_object($this->cache[$group][$id])) { return clone $this->cache[$group][$id]; } return $this->cache[$group][$id]; } /** * Stores the data in runtime memory. * * @param string $id * @param mixed $data * @param string $group * @return void */ protected function storeInMemory(string $id, $data, string $group = 'default') { $this->cache[$group][$id] = \is_object($data) ? clone $data : $data; } /** * Removes the cache contents matching key and group from the runtime memory cache. * * @param int|string $key * @param string $group * @return bool */ public function deleteFromMemory($key, string $group = 'default') { if (! $id = $this->id($key, $group)) { return false; } if (! $this->hasInMemory($id, $group)) { return false; } unset($this->cache[$group][$id]); return true; } /** * Alias to flush the in-memory runtime cache. * * @return bool */ public function flushRuntime(): bool { return $this->flush_runtime(); } /** * Removes all in-memory cache items for a single blog in multisite environments, * otherwise defaults to flushing the entire in-memory cache. * * Unless the `$network_flush` parameter is given this method * will default to `network_flush` configuration option. * * @param int $siteId * @param ?string $network_flush * * @return bool */ public function flushBlog(int $siteId, ?string $network_flush = null): bool { if (is_null($network_flush)) { $network_flush = $this->config->network_flush; } $originalBlogId = $this->blogId; $this->blogId = $siteId; if ($network_flush === Configuration::NETWORK_FLUSH_GLOBAL) { foreach ($this->globalGroups() as $group) { unset($this->cache[$group]); } } $id = $this->id('*', dechex(3405691582)); $prefix = trim(preg_replace('/:{?cafebabe}?/', '', (string) $id), '*'); $prefixLength = strlen($prefix); foreach ($this->cache as $group => $keys) { foreach (array_keys($keys) as $key) { if (substr_compare((string) $key, $prefix, 0, $prefixLength) === 0) { unset($this->cache[$group][$key]); } } } $this->blogId = $originalBlogId; return true; } /** * Whether `flushBlog()` should be called instead of `flush()`. * * @return bool */ public function shouldFlushBlog(): bool { return $this->isMultisite && in_array($this->config->network_flush, [ $this->config::NETWORK_FLUSH_SITE, $this->config::NETWORK_FLUSH_GLOBAL, ]); } /** * Execute the given closure without data mutations on the connection, * such as serialization and compression algorithms. * * @param callable $callback * @return mixed */ public function withoutMutations(callable $callback) { return $this->connection->withoutMutations( $callback instanceof Closure ? $callback->bindTo($this, $this) : $callback ); } /** * Build cache identifier for given key and group. * * 1. The configured prefix is added to all identifiers * 2. In network environments the `blog_id` is added to the group * 3. On clusters the group is used as the hash slot * * @param int|string $key * @param string $group * @return string|false */ protected function id($key, string $group) { static $cache = []; try { $cacheKey = $this->isMultisite ? "{$this->prefix}:{$this->blogId}:{$group}:{$key}" : "{$this->prefix}:{$group}:{$key}"; if (isset($cache[$cacheKey])) { return $cache[$cacheKey]; } if (! \is_string($key) && ! \is_int($key) || \trim((string) $key) === '') { throw new InvalidCacheKeyTypeException; } } catch (Throwable $th) { $this->error(InvalidCacheKeyTypeException::forKey($key)); return false; } $blogId = ''; if ($this->isMultisite && ! \in_array($group, $this->globalGroups)) { $blogId = "{$this->blogId}:"; } $key = \str_replace(':', '-', (string) $key); $group = \str_replace(':', '-', $group); $group = $this->isCluster ? "{{$group}}" : $group; $id = "{$this->prefix}:{$blogId}{$group}:{$key}"; $id = \str_replace(' ', '-', $id); $id = \trim($id, ':'); $id = \strtolower($id); return $cache[$cacheKey] = $id; } /** * Handles connection errors. * * When WP_DEBUG is enabled, the exception will be re-thrown, * otherwise a critical log entry is emitted. * * @param \Throwable $error * @param array $context * @return void */ protected function error(Throwable $error, array $context = []): void // phpcs:ignore PHPCompatibility { global $wp_object_cache_errors; $wp_object_cache_errors[] = $error->getMessage(); $this->log->error( $error->getMessage(), \array_merge(['exception' => $error], $context) ); if ($this->config->debug) { throw ObjectCacheException::from($error); } } /** * Overload generic properties for compatibility. * * @param string $name * @return mixed */ public function __get($name) { switch ($name) { case 'hits': case 'cache_hits': return $this->metrics->hits; case 'misses': case 'cache_misses': return $this->metrics->misses; case 'no_remote_groups': return $this->nonPersistentGroups; default: trigger_error( sprintf('Undefined property: %s::$%s', get_called_class(), $name), E_USER_WARNING ); } } /** * Overload generic properties for compatibility. * * @param string $name * @return bool */ public function __isset($name) { return in_array($name, [ 'hits', 'misses', 'cache_hits', 'cache_misses', 'no_remote_groups', ]); } }