config->getValue('memcache.kvstore', []) !== []; } /** * Get the (lazily connecting) predis client for the configured topology. * * @throws \RuntimeException if the cache is not configured */ public function getInstance(): Client { if ($this->client === null) { if (!$this->isAvailable()) { throw new \RuntimeException('Key-value store cache is not available'); } $this->client = $this->create(); } return $this->client; } private function create(): Client { /** @var array $config */ $config = $this->config->getValue('memcache.kvstore', []); $this->eventLogger->start('connect:kvstore', 'Connect to the key-value store cache server'); [$parameters, $options] = $this->buildConnectionConfig($config); $client = new Client($parameters, $options); $this->eventLogger->end('connect:kvstore'); return $client; } /** * Translate the `memcache.kvstore` configuration into predis connection * parameters and client options. * * This method is pure (it does not connect) so that the mapping can be * verified without a running cache server. * * @param array $config The `memcache.kvstore` configuration * @return array{0: array, 1: array} tuple of `[$parameters, $options]` * @throws \RuntimeException on invalid configuration */ public function buildConnectionConfig(array $config): array { if (isset($config['sentinel'])) { return $this->buildSentinelConfig($config); } if (isset($config['seeds'])) { return $this->buildClusterConfig($config); } return $this->buildSingleServerConfig($config); } /** * @return array{0: array, 1: array} */ private function buildSingleServerConfig(array $config): array { if (!isset($config['server']) || !is_array($config['server'])) { throw new \RuntimeException('memcache.kvstore is missing the "server" configuration'); } $parameters = $this->createServerParameters($config['server'], $config); // Numbered databases are only supported on single servers for now, as // predis does not support selecting a database on a cluster yet. if (isset($config['dbindex'])) { $parameters['database'] = (int)$config['dbindex']; } return [$parameters, []]; } /** * @return array{0: array, 1: array} */ private function buildClusterConfig(array $config): array { $seeds = $config['seeds']; if (!is_array($seeds) || $seeds === []) { throw new \RuntimeException('memcache.kvstore cluster configuration is missing the "seeds" attribute'); } $parameters = array_map( fn (array $seed): array => $this->createServerParameters($seed, $config), array_values($seeds), ); $options = ['cluster' => 'redis']; $nodeParameters = $this->createSharedParameters($config); if ($nodeParameters !== []) { // applied to the nodes discovered while talking to the cluster $options['parameters'] = $nodeParameters; } return [$parameters, $options]; } /** * @return array{0: array, 1: array} */ private function buildSentinelConfig(array $config): array { $sentinel = $config['sentinel']; if (!is_array($sentinel) || empty($sentinel['service'])) { throw new \RuntimeException('memcache.kvstore sentinel configuration is missing the "service" attribute'); } $seeds = $sentinel['seeds'] ?? []; if (!is_array($seeds) || $seeds === []) { throw new \RuntimeException('memcache.kvstore sentinel configuration is missing the "seeds" attribute'); } $parameters = array_map( fn (array $seed): array => $this->createServerParameters($seed, $config), array_values($seeds), ); $options = [ 'replication' => 'sentinel', 'service' => (string)$sentinel['service'], ]; $nodeParameters = $this->createSharedParameters($config); if (isset($config['dbindex'])) { $nodeParameters['database'] = (int)$config['dbindex']; } if ($nodeParameters !== []) { // applied to the master / replica connections resolved via Sentinel $options['parameters'] = $nodeParameters; } return [$parameters, $options]; } /** * Build the predis parameters for a single server entry, merging in the * shared authentication, TLS and timeout options. */ public function createServerParameters(array $server, array $shared = []): array { $host = (string)($server['host'] ?? '127.0.0.1'); $protocol = $server['protocol'] ?? null; if ($protocol === null) { // A leading slash indicates a Unix domain socket $protocol = ($host !== '' && $host[0] === '/') ? 'unix' : 'tcp'; } $parameters = ['scheme' => (string)$protocol]; if ($protocol === 'unix') { $parameters['path'] = $host; } else { $parameters['host'] = $host; $parameters['port'] = (int)($server['port'] ?? 6379); } if ($protocol === 'tls' && isset($shared['ssl_context'])) { // SSL context options, see https://www.php.net/manual/en/context.ssl.php $parameters['ssl'] = $shared['ssl_context']; } return $parameters + $this->createSharedParameters($shared); } /** * Authentication, timeout and persistence parameters shared by every node. */ private function createSharedParameters(array $config): array { $parameters = []; if (isset($config['password']) && (string)$config['password'] !== '') { $parameters['password'] = (string)$config['password']; // A username is only sent when ACLs are in use if (isset($config['user']) && (string)$config['user'] !== '') { $parameters['username'] = (string)$config['user']; } } if (isset($config['timeout'])) { $parameters['timeout'] = (float)$config['timeout']; } if (isset($config['read_timeout'])) { $parameters['read_write_timeout'] = (float)$config['read_timeout']; } if (isset($config['persistent'])) { $parameters['persistent'] = (bool)$config['persistent']; } return $parameters; } }