Passed
Push — development ( 8f3e46...8433eb )
by Spuds
01:17 queued 31s
created

Redis   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 326
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 41
eloc 105
dl 0
loc 326
rs 9.1199
c 3
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getServers() 0 10 2
A settings() 0 21 3
A clean() 0 4 1
A exists() 0 3 1
A isConnected() 0 12 2
A put() 0 24 5
A details() 0 5 1
A getStats() 0 30 3
A get() 0 19 4
A isAvailable() 0 3 1
A __construct() 0 11 2
A addServers() 0 34 6
A setOptions() 0 17 4
A setSerializerValue() 0 15 4
A _is_persist() 0 5 2

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
/**
4
 * This file contains functions that deal with getting and setting Redis cache values.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * @version 2.0 dev
11
 *
12
 */
13
14
namespace ElkArte\Cache\CacheMethod;
15
16
use ElkArte\Helper\HttpReq;
17
18
/**
19
 * Redis
20
 */
21
class Redis extends AbstractCacheMethod
22
{
23
	/** {@inheritDoc} */
24
	protected $title = 'Redis';
25
26
	/** @var \Redis instance representing the connection to the redis servers. */
27
	protected $obj;
28
29
	/** @var bool If the connection to the server is successful */
30
	protected $isConnected = false;
31
32
	/**
33
	 * {@inheritDoc}
34
	 */
35
	public function __construct($options)
36
	{
37
		parent::__construct($options);
38
39
		if ($this->isAvailable())
40
		{
41
			$this->obj = new \Redis();
42
			$this->addServers();
43
			$this->setOptions();
44
			$this->setSerializerValue();
45
			$this->isConnected();
46
		}
47
	}
48
49
	/**
50
	 * {@inheritDoc}
51
	 */
52
	public function isAvailable()
53
	{
54
		return class_exists(\Redis::class);
55
	}
56
57
	/**
58
	 * Check if the connection to Redis server is active.
59
	 *
60
	 * @return bool Returns true if the connection is active, false otherwise.
61
	 */
62
	public function isConnected()
63
	{
64
		try
65
		{
66
			$this->isConnected = $this->obj->ping();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->obj->ping() can also be of type string. However, the property $isConnected is declared as type boolean. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
67
		}
68
		catch (\RedisException $e)
69
		{
70
			$this->isConnected = false;
71
		}
72
73
		return $this->isConnected;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->isConnected also could return the type string which is incompatible with the documented return type boolean.
Loading history...
74
	}
75
76
	/**
77
	 * If this should be done as a persistent connection
78
	 *
79
	 * @return string|null
80
	 */
81
	private function _is_persist()
82
	{
83
		global $db_persist;
84
85
		return empty($db_persist) ? null : $this->prefix . '_redis';
86
	}
87
88
	/**
89
	 * {@inheritDoc}
90
	 */
91
	protected function setOptions()
92
	{
93
		try
94
		{
95
			if (!empty($this->_options['cache_password']))
96
			{
97
				$this->obj->auth($this->_options['cache_password']);
98
			}
99
100
			if (!empty($this->_options['cache_uid']))
101
			{
102
				$this->obj->select($this->_options['cache_uid']);
103
			}
104
		}
105
		catch (\RedisException $e)
106
		{
107
			$this->isConnected = false;
108
		}
109
	}
110
111
	/**
112
	 * Returns the redis serializer value based on certain conditions.
113
	 */
114
	private function setSerializerValue()
115
	{
116
		$serializer = $this->obj::SERIALIZER_PHP;
117
		if (defined('Redis::SERIALIZER_IGBINARY') && extension_loaded('igbinary'))
118
		{
119
			$serializer = $this->obj::SERIALIZER_IGBINARY;
120
		}
121
122
		try
123
		{
124
			$this->obj->setOption($this->obj::OPT_SERIALIZER, $serializer);
125
		}
126
		catch (\RedisException $e)
127
		{
128
			$this->isConnected = false;
129
		}
130
	}
131
132
	/**
133
	 * Add redis server.  Currently, does not support RedisArray / RedisCluster
134
	 *
135
	 * @return bool True if there are servers in the daemon, false if not.
136
	 */
137
	protected function addServers()
138
	{
139
		$retVal = false;
140
141
		$server = reset($this->_options['servers']);
142
		if ($server !== false)
143
		{
144
			$server = explode(':', trim($server));
145
			$host = empty($server[0]) ? 'localhost' : $server[0];
146
			$port = empty($server[1]) ? 6379 : (int) $server[1];
147
148
			set_error_handler(static function () { /* ignore php_network_getaddresses errors */ });
149
			try
150
			{
151
				if ($this->_is_persist())
152
				{
153
					$retVal = $this->obj->pconnect($host, $port, 0.0, $this->_is_persist());
154
				}
155
				else
156
				{
157
					$retVal = $this->obj->connect($host, $port, 0.0);
158
				}
159
			}
160
			catch (\RedisException $e)
161
			{
162
				$retVal = false;
163
			}
164
			finally
165
			{
166
				restore_error_handler();
167
			}
168
		}
169
170
		return $retVal;
171
	}
172
173
	/**
174
	 * Get redis servers.
175
	 *
176
	 * @return string A server name if we are attached.
177
	 */
178
	protected function getServers()
179
	{
180
		$server = '';
181
182
		if ($this->isConnected())
183
		{
184
			$server = reset($this->_options['servers']);
185
		}
186
187
		return $server;
188
	}
189
190
	/**
191
	 * Retrieves statistics about the cache.
192
	 *
193
	 * @return array An associative array containing the cache statistics.
194
	 *    The array has the following keys:
195
	 *      - curr_items: The number of items currently stored in the cache.
196
	 *      - get_hits: The number of successful cache hits.
197
	 *      - get_misses: The number of cache misses.
198
	 *      - curr_connections: The number of current open connections to the cache server.
199
	 *      - version: The version of the cache server.
200
	 *      - hit_rate: The cache hit rate as a decimal value with two decimal places.
201
	 *      - miss_rate: The cache miss rate as a decimal value with two decimal places.
202
	 *
203
	 * If the statistics cannot be obtained, an empty array is returned.
204
	 */
205
	public function getStats()
206
	{
207
		$results = [];
208
209
		try
210
		{
211
			$cache = $this->obj->info();
212
		}
213
		catch (\RedisException $e)
214
		{
215
			$cache = false;
216
		}
217
218
		if ($cache === false)
219
		{
220
			return $results;
221
		}
222
223
		$elapsed = max($cache['uptime_in_seconds'], 1) / 60;
224
		$cache['tracking_total_keys'] = count($this->obj->keys('*'));
225
226
		$results['curr_items'] = comma_format($cache['tracking_total_keys'] ?? 0, 0);
227
		$results['get_hits'] = comma_format($cache['keyspace_hits'] ?? 0, 0);
228
		$results['get_misses'] = comma_format($cache['keyspace_misses'] ?? 0, 0);
229
		$results['curr_connections'] = $cache['connected_clients'] ?? 0;
230
		$results['version'] = $cache['redis_version'] ?? '0.0.0';
231
		$results['hit_rate'] = sprintf("%.2f", $cache['keyspace_hits'] / $elapsed);
232
		$results['miss_rate'] = sprintf("%.2f", $cache['keyspace_misses'] / $elapsed);
233
234
		return $results;
235
	}
236
237
	/**
238
	 * {@inheritDoc}
239
	 */
240
	public function exists($key)
241
	{
242
		return $this->obj->exists($key);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->obj->exists($key) also could return the type integer which is incompatible with the return type mandated by ElkArte\Cache\CacheMetho...thodInterface::exists() of boolean.
Loading history...
243
	}
244
245
	/**
246
	 * {@inheritDoc}
247
	 */
248
	public function get($key, $ttl = 120)
249
	{
250
		if (!$this->isConnected)
251
		{
252
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the return type mandated by ElkArte\Cache\CacheMetho...eMethodInterface::get() of array|null.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
253
		}
254
255
		try
256
		{
257
			$result = $this->obj->get($key);
258
		}
259
		catch (\RedisException $e)
260
		{
261
			$result = null;
262
		}
263
264
		$this->is_miss = $result === null || $result === false;
265
266
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type string which is incompatible with the return type mandated by ElkArte\Cache\CacheMetho...eMethodInterface::get() of array|null.
Loading history...
267
	}
268
269
	/**
270
	 * {@inheritDoc}
271
	 */
272
	public function put($key, $value, $ttl = 120)
273
	{
274
		if (!$this->isConnected)
275
		{
276
			return false;
277
		}
278
279
		try
280
		{
281
			if ($value === null)
282
			{
283
				$this->obj->del($key);
284
			}
285
286
			if ($ttl > 0)
287
			{
288
				return $this->obj->setex($key, $ttl, $value);
289
			}
290
291
			return $this->obj->set($key, $value);
292
		}
293
		catch (\RedisException $e)
294
		{
295
			return false;
296
		}
297
	}
298
299
	/**
300
	 * {@inheritDoc}
301
	 */
302
	public function clean($type = '')
303
	{
304
		// Clear it out
305
		$this->obj->flushDB();
306
	}
307
308
	/**
309
	 * {@inheritDoc}
310
	 */
311
	public function details()
312
	{
313
		return [
314
			'title' => $this->title(),
315
			'version' => phpversion('redis')
316
		];
317
	}
318
319
	/**
320
	 * Adds the settings to the settings page.
321
	 *
322
	 * Used by integrate_modify_cache_settings added in the title method
323
	 *
324
	 * @param array $config_vars
325
	 */
326
	public function settings(&$config_vars)
327
	{
328
		global $txt, $cache_servers, $cache_servers_redis;
329
330
		$var = [
331
			'cache_servers_redis', $txt['cache_redis'], 'file', 'text', 30, 'cache_redis', 'force_div_id' => 'redis_cache_redis',
332
		];
333
334
		// Use generic global cache_servers value to load the initial form value
335
		if (HttpReq::instance()->getQuery('save') === null)
336
		{
337
			$cache_servers_redis = $cache_servers;
338
		}
339
340
		$serversList = $this->getServers();
341
		$serversList = empty($serversList) ? $txt['admin_search_results_none'] : $serversList;
342
		$var['postinput'] = $txt['cache_redis_servers'] . $serversList . '</li></ul>';
343
344
		$config_vars[] = $var;
345
		$config_vars[] = ['cache_uid', $txt['cache_uid'], 'file', 'text', $txt['cache_uid'], 'cache_uid', 'force_div_id' => 'redis_cache_uid'];
346
		$config_vars[] = ['cache_password', $txt['cache_password'], 'file', 'password', $txt['cache_password'], 'cache_password', 'force_div_id' => 'redis_cache_password', 'skip_verify_pass' => true];
347
	}
348
}
349