Test Failed
Pull Request — develop (#85)
by
unknown
08:33
created

Redis   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 278
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 132
dl 0
loc 278
rs 8.96
c 0
b 0
f 0
wmc 43

6 Methods

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

How to fix   Complexity   

Complex Class

Complex classes like 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 Redis, and based on these observations, apply Extract Interface, too.

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

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