Resque_Redis   A
last analyzed

Complexity

Total Complexity 42

Size/Duplication

Total Lines 274
Duplicated Lines 0 %

Test Coverage

Coverage 82.86%

Importance

Changes 14
Bugs 0 Features 2
Metric Value
eloc 130
c 14
b 0
f 2
dl 0
loc 274
ccs 58
cts 70
cp 0.8286
rs 9.0399
wmc 42

6 Methods

Rating   Name   Duplication   Size   Complexity  
F parseDsn() 0 69 20
A __call() 0 15 5
A getPrefix() 0 3 1
A removePrefix() 0 8 2
C __construct() 0 36 11
A prefix() 0 6 3

How to fix   Complexity   

Complex Class

Complex classes like Resque_Redis often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Resque_Redis, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Wrap Credis to add namespace support and various helper methods.
5
 *
6
 * @package		Resque/Redis
7
 * @author		Chris Boulton <[email protected]>
8
 * @license		http://www.opensource.org/licenses/mit-license.php
9
 */
10
class Resque_Redis
11
{
12
	/**
13
	 * Redis namespace
14
	 * @var string
15
	 */
16
	private static $defaultNamespace = 'resque:';
17
18
	/**
19
	 * A default host to connect to
20
	 */
21
	const DEFAULT_HOST = 'localhost';
22
23
	/**
24
	 * The default Redis port
25
	 */
26
	const DEFAULT_PORT = 6379;
27
28
	/**
29
	 * The default Redis Database number
30
	 */
31
	const DEFAULT_DATABASE = 0;
32
33
	/**
34
	 * Connection driver
35
	 * @var mixed
36
	 */
37
	private $driver;
38
39
	/**
40
	 * @var array List of all commands in Redis that supply a key as their
41
	 *	first argument. Used to prefix keys with the Resque namespace.
42
	 */
43
	private $keyCommands = array(
44
		'exists',
45
		'del',
46
		'type',
47
		'keys',
48
		'expire',
49
		'ttl',
50
		'move',
51
		'set',
52
		'setex',
53
		'get',
54
		'getset',
55
		'setnx',
56
		'incr',
57
		'incrby',
58
		'decr',
59
		'decrby',
60
		'rpush',
61
		'lpush',
62
		'llen',
63
		'lrange',
64
		'ltrim',
65
		'lindex',
66
		'lset',
67
		'lrem',
68
		'lpop',
69
		'blpop',
70
		'rpop',
71
		'sadd',
72
		'srem',
73
		'spop',
74
		'scard',
75
		'sismember',
76
		'smembers',
77
		'srandmember',
78
		'zadd',
79
		'zrem',
80
		'zrange',
81
		'zrevrange',
82
		'zrangebyscore',
83
		'zcard',
84
		'zscore',
85
		'zremrangebyscore',
86
		'sort',
87
		'rename',
88
		'rpoplpush'
89
	);
90
	// sinterstore
91
	// sunion
92
	// sunionstore
93
	// sdiff
94
	// sdiffstore
95
	// sinter
96
	// smove
97
	// mget
98
	// msetnx
99 2
	// mset
100
	// renamenx
101 2
102 2
	/**
103
	 * Set Redis namespace (prefix) default: resque
104 2
	 * @param string $namespace
105 2
	 */
106
	public static function prefix($namespace)
107
	{
108
		if (substr($namespace, -1) !== ':' && $namespace != '') {
109
			$namespace .= ':';
110
		}
111
		self::$defaultNamespace = $namespace;
112
	}
113 71
114
	/**
115
	 * @param string|array $server A DSN or array
116 71
	 * @param int $database A database number to select. However, if we find a valid database number in the DSN the
117 2
	 *                      DSN-supplied value will be used instead and this parameter is ignored.
118
	 * @param object $client Optional Credis_Cluster or Credis_Client instance instantiated by you
119 70
	 */
120
	public function __construct($server, $database = null, $client = null)
121
	{
122 70
		try {
123
			if (is_object($client)) {
124
				$this->driver = $client;
125
			} elseif (is_object($server)) {
0 ignored issues
show
introduced by
The condition is_object($server) is always false.
Loading history...
126 70
				$this->driver = $server;
127
			} elseif (is_array($server)) {
128
				$this->driver = new Credis_Cluster($server);
0 ignored issues
show
Deprecated Code introduced by
The class Credis_Cluster has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

128
				$this->driver = /** @scrutinizer ignore-deprecated */ new Credis_Cluster($server);
Loading history...
129
			} else {
130 70
				list($host, $port, $dsnDatabase, $user, $password, $options) = self::parseDsn($server);
131 70
				// $user is not used, only $password
132 70
133
				// Look for known Credis_Client options
134 70
				$timeout = isset($options['timeout']) ? intval($options['timeout']) : null;
135 70
				$persistent = isset($options['persistent']) ? $options['persistent'] : '';
136 70
				$maxRetries = isset($options['max_connect_retries']) ? $options['max_connect_retries'] : 0;
137
138
				$this->driver = new Credis_Client($host, $port, $timeout, $persistent);
139
				$this->driver->setMaxConnectRetries($maxRetries);
140
				if ($password) {
141
					$this->driver->auth($password);
142 70
				}
143
144
				// If we have found a database in our DSN, use it instead of the `$database`
145
				// value passed into the constructor.
146
				if ($dsnDatabase !== false) {
147 71
					$database = $dsnDatabase;
148 71
				}
149
			}
150
151 2
			if ($database !== null) {
152 2
				$this->driver->select($database);
153
			}
154 70
		} catch (CredisException $e) {
155
			throw new Resque_RedisException('Error communicating with Redis: ' . $e->getMessage(), 0, $e);
156
		}
157
	}
158
159
	/**
160
	 * Parse a DSN string, which can have one of the following formats:
161
	 *
162
	 * - host:port
163
	 * - redis://user:pass@host:port/db?option1=val1&option2=val2
164
	 * - tcp://user:pass@host:port/db?option1=val1&option2=val2
165
	 * - unix:///path/to/redis.sock
166
	 *
167
	 * Note: the 'user' part of the DSN is not used.
168
	 *
169
	 * @param string $dsn A DSN string
170 91
	 * @return array An array of DSN compotnents, with 'false' values for any unknown components. e.g.
171
	 *               [host, port, db, user, pass, options]
172 91
	 */
173
	public static function parseDsn($dsn)
174 1
	{
175
		if ($dsn == '') {
176 91
			// Use a sensible default for an empty DNS string
177
			$dsn = 'redis://' . self::DEFAULT_HOST;
178
		}
179
		if (substr($dsn, 0, 7) === 'unix://') {
180
			return array(
181
				$dsn,
182
				null,
183
				false,
184
				null,
185
				null,
186 91
				null,
187
			);
188
		}
189 91
		$parts = parse_url($dsn);
190 91
191 3
		// Check the URI scheme
192
		$validSchemes = array('redis', 'tcp');
193
		if (isset($parts['scheme']) && ! in_array($parts['scheme'], $validSchemes)) {
194
			throw new \InvalidArgumentException("Invalid DSN. Supported schemes are " . implode(', ', $validSchemes));
195 88
		}
196 1
197 1
		// Allow simple 'hostname' format, which `parse_url` treats as a path, not host.
198
		if (! isset($parts['host']) && isset($parts['path'])) {
199
			$parts['host'] = $parts['path'];
200
			unset($parts['path']);
201 88
		}
202
203
		// Extract the port number as an integer
204 88
		$port = isset($parts['port']) ? intval($parts['port']) : self::DEFAULT_PORT;
205 88
206
		// Get the database from the 'path' part of the URI
207 7
		$database = false;
208
		if (isset($parts['path'])) {
209
			// Strip non-digit chars from path
210
			$database = intval(preg_replace('/[^0-9]/', '', $parts['path']));
211 88
		}
212 88
213
		// Extract any 'user' values
214
		$user = isset($parts['user']) ? $parts['user'] : false;
215 88
216 88
		// Convert the query string into an associative array
217
		$options = array();
218 6
		if (isset($parts['query'])) {
219
			// Parse the query string into an array
220
			parse_str($parts['query'], $options);
221
		}
222 88
223 88
		//check 'password-encoding' parameter and extracting password based on encoding
224 88
		if ($options && isset($options['password-encoding']) && $options['password-encoding'] === 'u') {
225 88
			//extracting urlencoded password
226 88
			$pass = isset($parts['pass']) ? urldecode($parts['pass']) : false;
227 88
		} elseif ($options && isset($options['password-encoding']) && $options['password-encoding'] === 'b') {
228
			//extracting base64 encoded password
229
			$pass = isset($parts['pass']) ? base64_decode($parts['pass']) : false;
230
		} else {
231
			//extracting pass directly since 'password-encoding' parameter is not present
232
			$pass = isset($parts['pass']) ? $parts['pass'] : false;
233
		}
234
235
		return array(
236
			$parts['host'],
237
			$port,
238
			$database,
239 76
			$user,
240
			$pass,
241 76
			$options,
242 76
		);
243 1
	}
244 1
245
	/**
246
	 * Magic method to handle all function requests and prefix key based
247
	 * operations with the {self::$defaultNamespace} key prefix.
248 76
	 *
249
	 * @param string $name The name of the method called.
250
	 * @param array $args Array of supplied arguments to the method.
251
	 * @return mixed Return value from Resident::call() based on the command.
252 76
	 */
253
	public function __call($name, $args)
254
	{
255
		if (in_array($name, $this->keyCommands)) {
256
			if (is_array($args[0])) {
257
				foreach ($args[0] as $i => $v) {
258
					$args[0][$i] = self::$defaultNamespace . $v;
259 13
				}
260
			} else {
261 13
				$args[0] = self::$defaultNamespace . $args[0];
262
			}
263
		}
264
		try {
265
			return $this->driver->__call($name, $args);
266
		} catch (CredisException $e) {
267
			throw new Resque_RedisException('Error communicating with Redis: ' . $e->getMessage(), 0, $e);
268
		}
269
	}
270
271
	public static function getPrefix()
272
	{
273
		return self::$defaultNamespace;
274
	}
275
276
	public static function removePrefix($string)
277
	{
278
		$prefix = self::getPrefix();
279
280
		if (substr($string, 0, strlen($prefix)) == $prefix) {
281
			$string = substr($string, strlen($prefix), strlen($string));
282
		}
283
		return $string;
284
	}
285
}
286