You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
10 KiB
312 lines
10 KiB
<?php
|
|
/**
|
|
* li₃: the most RAD framework for PHP (http://li3.me)
|
|
*
|
|
* Copyright 2009, Union of RAD. All rights reserved. This source
|
|
* code is distributed under the terms of the BSD 3-Clause License.
|
|
* The full license text can be found in the LICENSE.txt file.
|
|
*/
|
|
|
|
namespace lithium\storage\cache\adapter;
|
|
|
|
use lithium\storage\Cache;
|
|
use lithium\net\HostString;
|
|
use Redis as RedisCore;
|
|
|
|
/**
|
|
* A Redis cache adapter implementation using `phpredis`.
|
|
*
|
|
* This adapter does not aim to provide a full implementation of the Redis API, but rather
|
|
* only a subset of its features that are useful in the context of a semi-persistent cache.
|
|
*
|
|
* This adapter depends on the `phpredis` PHP extension and on a running
|
|
* instance of Redis server being available.
|
|
*
|
|
* This adapter natively handles atomic multi-key reads/writes/deletes and supports atomic
|
|
* increment/decrement operations as well as clearing the entire cache. Scope support is
|
|
* natively available. Delegation of method calls to the connection object is available.
|
|
*
|
|
* Serialization of values is not handled natively, the `Serializer` strategy must be used
|
|
* if you plan to store non-scalar values or need to keep type on values. Full cached item
|
|
* persistence is not guaranteed it depends on the how the Redis server is actually configured
|
|
* and accessed.
|
|
*
|
|
* A simple configuration can be accomplished as follows:
|
|
*
|
|
* ```
|
|
* Cache::config([
|
|
* 'cache-config-name' => [
|
|
* 'adapter' => 'Redis',
|
|
* 'host' => '127.0.0.1:6379',
|
|
* 'strategies => ['Serializer']
|
|
* ]
|
|
* ]);
|
|
* ```
|
|
*
|
|
* The `'host'` key accepts a string argument in the format of ip:port where
|
|
* the Redis server can be found.
|
|
*
|
|
* @link https://github.com/nicolasff/phpredis
|
|
* @see lithium\storage\Cache::key()
|
|
* @see lithium\storage\Cache::adapter()
|
|
*/
|
|
class Redis extends \lithium\storage\cache\Adapter {
|
|
|
|
/**
|
|
* The default host used to connect to the server.
|
|
*/
|
|
const DEFAULT_HOST = '127.0.0.1';
|
|
|
|
/**
|
|
* The default port used to connect to the server.
|
|
*/
|
|
const DEFAULT_PORT = 6379;
|
|
|
|
/**
|
|
* Redis object instance used by this adapter.
|
|
*
|
|
* @var object Redis object
|
|
*/
|
|
public $connection;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @todo Implement configurable & optional authentication
|
|
* @see lithium\storage\Cache::config()
|
|
* @see lithium\storage\cache\adapter\Redis::write()
|
|
* @param array $config Configuration for this cache adapter. These settings are queryable
|
|
* through `Cache::config('name')`. The available options are as follows:
|
|
* - `'scope'` _string_: Scope which will prefix keys; per default not set.
|
|
* - `'expiry'` _mixed_: The default expiration time for cache values, if no value
|
|
* is otherwise set. Can be either a `strtotime()` compatible tring or TTL in
|
|
* seconds. To indicate items should not expire use `Cache::PERSIST`. Defaults
|
|
* to `+1 hour`.
|
|
* - `'host'` _string_: A string in the form of `'<host>'`, `'<host>:<port>'` or
|
|
* `':<port>'` indicating the host and/or port to connect to. When one or both are
|
|
* not provided uses general server defaults.
|
|
* To use Unix sockets specify the path to the socket (i.e. `'/path/to/socket'`).
|
|
* - `'persistent'` _boolean_: Indicates whether the adapter should use a persistent
|
|
* connection when attempting to connect to the Redis server. If `true`, it will
|
|
* attempt to reuse an existing connection when connecting, and the connection will
|
|
* not close when the request is terminated. Defaults to `false`.
|
|
* @return void
|
|
*/
|
|
public function __construct(array $config = []) {
|
|
$defaults = [
|
|
'scope' => null,
|
|
'expiry' => '+1 hour',
|
|
'host' => static::DEFAULT_HOST . ':' . static::DEFAULT_PORT,
|
|
'persistent' => false
|
|
];
|
|
parent::__construct($config + $defaults);
|
|
}
|
|
|
|
/**
|
|
* Initialize the Redis connection object, connect to the Redis server and sets
|
|
* prefix using the scope if provided.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function _init() {
|
|
if (!$this->connection) {
|
|
$this->connection = new RedisCore();
|
|
}
|
|
$method = $this->_config['persistent'] ? 'pconnect' : 'connect';
|
|
|
|
if (HostString::isSocket($this->_config['host'])) {
|
|
$this->connection->{$method}($this->_config['host']);
|
|
} else {
|
|
$host = HostString::parse($this->_config['host']) + [
|
|
'host' => static::DEFAULT_HOST,
|
|
'port' => static::DEFAULT_PORT
|
|
];
|
|
$this->connection->{$method}($host['host'], $host['port']);
|
|
}
|
|
|
|
if ($this->_config['scope']) {
|
|
$this->connection->setOption(RedisCore::OPT_PREFIX, "{$this->_config['scope']}:");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dispatches a not-found method to the connection object. That way, one can
|
|
* easily use a custom method on the adapter. If you want to know, what methods
|
|
* are available, have a look at the documentation of phpredis.
|
|
*
|
|
* ```
|
|
* Cache::adapter('redis')->methodName($argument);
|
|
* ```
|
|
*
|
|
* One use-case might be to query possible keys, e.g.
|
|
*
|
|
* ```
|
|
* Cache::adapter('redis')->keys('*');
|
|
* ```
|
|
*
|
|
* @link https://github.com/nicolasff/phpredis
|
|
* @param string $method Name of the method to call.
|
|
* @param array $params Parameter list to use when calling $method.
|
|
* @return mixed Returns the result of the method call.
|
|
*/
|
|
public function __call($method, $params = []) {
|
|
return call_user_func_array([&$this->connection, $method], $params);
|
|
}
|
|
|
|
/**
|
|
* Determines if a given method can be called.
|
|
*
|
|
* @deprecated
|
|
* @param string $method Name of the method.
|
|
* @param boolean $internal Provide `true` to perform check from inside the
|
|
* class/object. When `false` checks also for public visibility;
|
|
* defaults to `false`.
|
|
* @return boolean Returns `true` if the method can be called, `false` otherwise.
|
|
*/
|
|
public function respondsTo($method, $internal = false) {
|
|
$message = '`' . __METHOD__ . '()` has been deprecated. ';
|
|
$message .= 'Use `is_callable([$adapter->connection, \'<method>\'])` instead.';
|
|
trigger_error($message, E_USER_DEPRECATED);
|
|
|
|
if (parent::respondsTo($method, $internal)) {
|
|
return true;
|
|
}
|
|
return is_callable([$this->connection, $method]);
|
|
}
|
|
|
|
/**
|
|
* Write values to the cache. All items to be cached will receive an
|
|
* expiration time of `$expiry`.
|
|
*
|
|
* @param array $keys Key/value pairs with keys to uniquely identify the to-be-cached item.
|
|
* @param string|integer $expiry A `strtotime()` compatible cache time or TTL in seconds.
|
|
* To persist an item use `\lithium\storage\Cache::PERSIST`.
|
|
* @return boolean `true` on successful write, `false` otherwise.
|
|
*/
|
|
public function write(array $keys, $expiry = null) {
|
|
$expiry = $expiry || $expiry === Cache::PERSIST ? $expiry : $this->_config['expiry'];
|
|
|
|
if (!$expiry || $expiry === Cache::PERSIST) {
|
|
$ttl = null;
|
|
} elseif (is_int($expiry)) {
|
|
$ttl = $expiry;
|
|
} else {
|
|
$ttl = strtotime($expiry) - time();
|
|
}
|
|
|
|
if (count($keys) > 1) {
|
|
if (!$ttl) {
|
|
return $this->connection->mset($keys);
|
|
}
|
|
$transaction = $this->connection->multi();
|
|
|
|
foreach ($keys as $key => $value) {
|
|
if (!$this->connection->setex($key, $ttl, $value)) {
|
|
$transaction->discard();
|
|
return false;
|
|
}
|
|
}
|
|
return $transaction->exec() === array_fill(0, count($keys), true);
|
|
}
|
|
$key = key($keys);
|
|
$value = current($keys);
|
|
|
|
if (!$ttl) {
|
|
return $this->connection->set($key, $value);
|
|
}
|
|
return $this->connection->setex($key, $ttl, $value);
|
|
}
|
|
|
|
/**
|
|
* Read values from the cache. Will attempt to return an array of data
|
|
* containing key/value pairs of the requested data.
|
|
*
|
|
* @param array $keys Keys to uniquely identify the cached items.
|
|
* @return array Cached values keyed by cache keys on successful read,
|
|
* keys which could not be read will not be included in
|
|
* the results array.
|
|
*/
|
|
public function read(array $keys) {
|
|
if (count($keys) > 1) {
|
|
$results = [];
|
|
$data = $this->connection->mGet($keys);
|
|
|
|
foreach ($data as $key => $item) {
|
|
$key = $keys[$key];
|
|
|
|
if ($item === false && !$connection->exists($key)) {
|
|
continue;
|
|
}
|
|
$results[$key] = $item;
|
|
}
|
|
return $results;
|
|
}
|
|
$result = $this->connection->get($key = current($keys));
|
|
return $result === false ? [] : [$key => $result];
|
|
}
|
|
|
|
/**
|
|
* Will attempt to remove specified keys from the user space cache.
|
|
*
|
|
* @param array $keys Keys to uniquely identify the cached items.
|
|
* @return boolean `true` on successful delete, `false` otherwise.
|
|
*/
|
|
public function delete(array $keys) {
|
|
return (boolean) $this->connection->del($keys);
|
|
}
|
|
|
|
/**
|
|
* Performs an atomic decrement operation on specified numeric cache item.
|
|
*
|
|
* Note that if the value of the specified key is *not* an integer, the decrement
|
|
* operation will have no effect whatsoever. Redis chooses to not typecast values
|
|
* to integers when performing an atomic decrement operation.
|
|
*
|
|
* @param string $key Key of numeric cache item to decrement.
|
|
* @param integer $offset Offset to decrement - defaults to `1`.
|
|
* @return integer|boolean The item's new value on successful decrement, else `false`.
|
|
*/
|
|
public function decrement($key, $offset = 1) {
|
|
return $this->connection->decr($key, $offset);
|
|
}
|
|
|
|
/**
|
|
* Performs an atomic increment operation on specified numeric cache item.
|
|
*
|
|
* Note that if the value of the specified key is *not* an integer, the increment
|
|
* operation will have no effect whatsoever. Redis chooses to not typecast values
|
|
* to integers when performing an atomic increment operation.
|
|
*
|
|
* @param string $key Key of numeric cache item to increment.
|
|
* @param integer $offset Offset to increment - defaults to `1`.
|
|
* @return integer|boolean The item's new value on successful increment, else `false`.
|
|
*/
|
|
public function increment($key, $offset = 1) {
|
|
return $this->connection->incr($key, $offset);
|
|
}
|
|
|
|
/**
|
|
* Clears entire database by flushing it. All cache keys using the
|
|
* configuration but *without* honoring the scope are removed.
|
|
*
|
|
* The behavior and result when removing a single key
|
|
* during this process fails is unknown.
|
|
*
|
|
* @return boolean `true` on successful clearing, `false` otherwise.
|
|
*/
|
|
public function clear() {
|
|
return $this->connection->flushdb();
|
|
}
|
|
|
|
/**
|
|
* Determines if the Redis extension has been installed and
|
|
* that there is a Redis server available.
|
|
*
|
|
* @return boolean Returns `true` if the Redis extension is enabled, `false` otherwise.
|
|
*/
|
|
public static function enabled() {
|
|
return extension_loaded('redis');
|
|
}
|
|
}
|
|
|
|
?>
|