Cache   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 496
Duplicated Lines 0 %

Test Coverage

Coverage 60.61%

Importance

Changes 0
Metric Value
eloc 130
dl 0
loc 496
ccs 80
cts 132
cp 0.6061
rs 3.44
c 0
b 0
f 0
wmc 62

22 Methods

Rating   Name   Duplication   Size   Complexity  
A _build_prefix() 0 10 2
A isEnabled() 0 3 1
A getCacheEngine() 0 3 1
A __construct() 0 16 3
A enable() 0 11 3
A _init() 0 16 2
A getAccelerator() 0 3 1
A instance() 0 16 5
A __destruct() 0 10 2
A setLevel() 0 10 2
A removeKeys() 0 15 4
A isMiss() 0 3 2
A clean() 0 17 2
A getLevel() 0 3 1
B get() 0 38 7
A getVar() 0 5 1
A put() 0 33 6
A levelLowerThan() 0 8 2
B quick_get() 0 36 10
A _key() 0 3 1
A remove() 0 9 2
A levelHigherThan() 0 3 2

How to fix   Complexity   

Complex Class

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

1
<?php
2
3
/**
4
 * This file contains functions that deal with getting and setting 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
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\Cache;
18
19
use ElkArte\Cache\CacheMethod\AbstractCacheMethod;
20
use ElkArte\Debug;
21
use ElkArte\Helper\FileFunctions;
22
use ElkArte\Helper\Util;
23
24
/**
25
 * Class Cache - Methods that deal with getting and setting cache values.
26
 */
27
class Cache
28
{
29
	/** @var object Holds our static instance of the class */
30
	protected static $_instance;
31
32
	/** @var array Array of options for the methods (if needed) */
33
	protected $_options = [];
34
35
	/** @var bool If the cache is enabled or not. */
36
	protected $enabled = false;
37
38
	/** @var int The caching level */
39
	protected $level = 0;
40
41
	/** @var string The prefix to append to the cache key */
42
	protected $_key_prefix;
43
44
	/** @var string The accelerator in use */
45
	protected $_accelerator;
46
47
	/** @var string[] Cached keys */
48
	protected $_cached_keys = [];
49
50
	/** @var AbstractCacheMethod|null The caching engine object */
51
	protected $_cache_obj;
52
53
	/**
54
	 * Initialize the class, defines the options and the caching method to use
55
	 *
56
	 * @param int $level The level of caching
57
	 * @param string $accelerator The accelerator used
58
	 * @param array $options Any setting necessary to the caching engine
59
	 */
60
	public function __construct($level, $accelerator, $options)
61
	{
62
		$this->_options = $options;
63
64
		// Default to file based, so we can slow everything down :P
65
		if (empty($accelerator))
66
		{
67
			$accelerator = 'filebased';
68
		}
69
		$this->_accelerator = ucfirst($accelerator);
70
71
		$this->setLevel($level);
72
		if ($level > 0)
73
		{
74
			$this->enable(true);
75
			$this->level = $level;
76
		}
77
	}
78
79
	/**
80
	 * Enable or disable caching
81
	 *
82
	 * @param bool $enable
83
	 *
84
	 * @return $this
85
	 */
86
	public function enable($enable): self
87
	{
88
		// Enable it if we can
89
		if (!$this->enabled && $this->_cache_obj === null)
90 1
		{
91
			$this->_init();
92 1
		}
93
94
		$this->enabled = (bool) $enable;
95 1
96
		return $this;
97 1
	}
98
99
	/**
100 1
	 * Initialize a cache class and call its initialization method
101
	 */
102 1
	protected function _init(): void
103
	{
104
		$cache_class = '\\ElkArte\\Cache\\CacheMethod\\' . $this->_accelerator;
105
106
		if (class_exists($cache_class))
107
		{
108
			$this->_cache_obj = new $cache_class($this->_options);
109 1
			$this->enabled = $this->_cache_obj->isAvailable();
110
		}
111 1
		else
112
		{
113
			$this->_cache_obj = null;
114
			$this->enabled = false;
115
		}
116
117
		$this->_build_prefix();
118
	}
119
120
	/**
121
	 * Set $_key_prefix to a "unique" value based on timestamp of a file
122
	 */
123
	protected function _build_prefix(): void
124
	{
125
		global $boardurl;
126
127 3
		if (!FileFunctions::instance()->fileExists(CACHEDIR . '/index.php'))
128
		{
129
			touch(CACHEDIR . '/index.php');
130 3
		}
131
132 1
		$this->_key_prefix = md5($boardurl . filemtime(CACHEDIR . '/index.php')) . '-ELK-';
133
	}
134
135 3
	/**
136
	 * Check if caching is enabled
137 3
	 *
138
	 * @return bool
139
	 */
140
	public function isEnabled(): bool
141
	{
142
		return $this->enabled;
143 1
	}
144
145 1
	/**
146
	 * Return the current cache_obj
147 1
	 *
148
	 * @return AbstractCacheMethod|null
149
	 */
150
	public function getCacheEngine(): ?AbstractCacheMethod
151
	{
152
		return $this->_cache_obj;
153
	}
154 1
155
	/**
156 1
	 * Return the cache accelerator in use
157
	 *
158
	 * @return string
159 1
	 */
160 1
	public function getAccelerator(): string
161
	{
162
		return $this->_accelerator;
163
	}
164
165 1
	/**
166
	 * Find and return the instance of the Cache class if it exists,
167 1
	 * otherwise start a new instance
168
	 */
169 1
	public static function instance()
170
	{
171
		if (self::$_instance === null)
172
		{
173 1
			global $cache_accelerator, $cache_enable, $cache_uid, $cache_password, $cache_servers;
174 1
175
			$options = [
176
				'servers' => empty($cache_servers) ? [] : explode(',', $cache_servers),
177
				'cache_uid' => empty($cache_uid) ? '' : $cache_uid,
178
				'cache_password' => empty($cache_password) ? '' : $cache_password,
179
			];
180
181 277
			self::$_instance = new Cache($cache_enable, $cache_accelerator, $options);
182
		}
183 277
184
		return self::$_instance;
185
	}
186
187
	/**
188
	 * Just before forgetting about the cache, let's save the existing keys.
189
	 */
190 277
	public function __destruct()
191
	{
192 277
		$cached = $this->get('_cached_keys');
193
		if (!is_array($cached))
194 1
		{
195
			$cached = [];
196 1
		}
197 1
198
		$_cached_keys = array_unique(array_merge($this->_cached_keys, $cached));
199
		$this->put('_cached_keys', $_cached_keys);
200
	}
201
202
	/**
203
	 * Gets the value from the cache specified by key, so long as it is not older than ttl seconds.
204 1
	 *
205
	 * - It may often "miss", so shouldn't be depended on.
206
	 * - It supports the same as \ElkArte\Cache\Cache::put().
207 277
	 *
208
	 * @param string $key
209
	 * @param int $ttl = 120
210
	 *
211
	 * @return null|mixed if it was a hit
212
	 */
213
	public function get($key, $ttl = 120)
214
	{
215
		global $db_show_debug;
216
217
		if (!$this->isEnabled())
218
		{
219
			return null;
220
		}
221
222
		if ($db_show_debug === true)
223
		{
224
			$cache_hit = [
225
				'k' => $key,
226
				'd' => 'get'
227
			];
228
			$st = microtime(true);
229
		}
230
231
		$key = $this->_key($key);
232
		$value = $this->_cache_obj->get($key, $ttl);
0 ignored issues
show
Bug introduced by
The method get() does not exist on null. ( Ignorable by Annotation )

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

232
		/** @scrutinizer ignore-call */ 
233
  $value = $this->_cache_obj->get($key, $ttl);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
233
234
		if ($db_show_debug === true)
235
		{
236 259
			$cache_hit['t'] = microtime(true) - $st;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $st does not seem to be defined for all execution paths leading up to this point.
Loading history...
237
			$cache_hit['s'] = isset($value) ? strlen($value) : 0;
238 259
			Debug::instance()->cache($cache_hit);
239
		}
240 259
241
		call_integration_hook('cache_get_data', [$key, $ttl, $value]);
242 257
243
		if (!empty($value))
244
		{
245 2
			$cacheHit = Util::unserialize($value);
246
			return ($cacheHit === false) ? null : $cacheHit;
247
248
		}
249
250
		return null;
251
	}
252
253
	/**
254 2
	 * Get the key for the cache.
255 2
	 *
256
	 * @param string $key
257 2
	 *
258
	 * @return string
259
	 */
260
	protected function _key($key): string
261
	{
262
		return $this->_key_prefix . $this->_cache_obj->fixkey($key);
263
	}
264 2
265
	/**
266 2
	 * Puts value in the cache under key for ttl seconds.
267
	 *
268
	 * - It may "miss" so shouldn't be depended on
269
	 * - Uses the cache engine chosen in the ACP and saved in settings.php
270
	 * - It supports:
271
	 *   - Memcache: https://www.php.net/memcache
272
	 *   - MemcacheD: https://www.php.net/memcached
273
	 *   - APCu: https://us3.php.net/manual/en/book.apcu.php
274
	 *   - Zend: https://help.zend.com/zend/current/content/data_cache_component.htm
275
	 *   - Redis: https://redis.io/learn/develop/php
276 2
	 *
277
	 * @param string $key
278 2
	 * @param string|int|array|null $value
279
	 * @param int $ttl = 120
280
	 */
281
	public function put($key, $value, $ttl = 120): void
282
	{
283
		global $db_show_debug;
284
285
		if (!$this->isEnabled())
286
		{
287
			return;
288
		}
289
290
		// If we are showing debug information we have some data to collect
291
		if ($db_show_debug === true)
292
		{
293
			$cache_hit = [
294
				'k' => $key,
295
				'd' => 'put',
296
				's' => $value === null ? 0 : strlen(serialize($value))
297
			];
298 261
			$st = microtime(true);
299
		}
300 261
301
		$this->_cached_keys[] = $key;
302 261
		$key = $this->_key($key);
303
		$value = $value === null ? null : serialize($value);
304 259
305
		$this->_cache_obj->put($key, $value, $ttl);
306
307
		call_integration_hook('cache_put_data', [$key, $value, $ttl]);
308 2
309
		// Show the debug cache hit information
310
		if ($db_show_debug === true)
311
		{
312
			$cache_hit['t'] = microtime(true) - $st;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $st does not seem to be defined for all execution paths leading up to this point.
Loading history...
313
			Debug::instance()->cache($cache_hit);
314
		}
315
	}
316
317
	/**
318 2
	 * Try to retrieve a cache entry. On failure, call the appropriate function.
319 2
	 * This callback is sent as $file to include, and $function to call, with
320 2
	 * $params parameters.
321
	 *
322 2
	 * @param string $key cache entry key
323
	 * @param string $file file to include
324 2
	 * @param string $function function to call
325
	 * @param array $params parameters sent to the function
326
	 * @param int $level = 1
327 2
	 *
328
	 * @return array
329
	 */
330
	public function quick_get($key, $file, $function, $params, $level = 1): array
331
	{
332 2
		call_integration_hook('pre_cache_quick_get', [&$key, &$file, &$function, &$params, &$level]);
333
334
		/* Refresh the cache if either:
335
			1. Caching is disabled.
336
			2. The cache level isn't high enough.
337
			3. The item has not been cached or the cached item expired.
338
			4. The cached item has a custom expiration condition evaluating to true.
339
			5. The expire time set in the cache item has passed (needed for Zend).
340
		*/
341
		$cache_block = $this->get($key, 3600);
342
		if ($this->level < $level
343
			|| !is_array($cache_block)
344
			|| !$this->isEnabled()
345
			|| (!empty($cache_block['refresh_eval']) && eval($cache_block['refresh_eval']))
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
346
			|| (!empty($cache_block['expires']) && $cache_block['expires'] < time()))
347
		{
348
			require_once(SOURCEDIR . '/' . $file);
349
			$cache_block = call_user_func_array($function, $params);
350
351
			if ($this->level >= $level)
352
			{
353
				$this->put($key, $cache_block, $cache_block['expires'] - time());
354
			}
355
		}
356
357
		// Some cached data may need a freshening up after retrieval.
358
		if (!empty($cache_block['post_retri_eval']))
359
		{
360
			eval($cache_block['post_retri_eval']);
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
361
		}
362
363
		call_integration_hook('post_cache_quick_get', [$cache_block]);
364
365
		return $cache_block['data'];
366
	}
367
368
	/**
369
	 * Same as $this->get but sets $var to the result and return if it was a hit
370
	 *
371
	 * @param mixed $var The variable to be assigned the result
372
	 * @param string $key
373
	 * @param int $ttl
374
	 *
375
	 * @return null|bool if it was a hit
376
	 */
377
	public function getVar(&$var, $key, $ttl = 120): ?bool
378
	{
379
		$var = $this->get($key, $ttl);
380
381
		return !$this->isMiss();
382
	}
383
384
	/**
385
	 * @return bool If the result of the last get was a miss
386
	 */
387
	public function isMiss(): bool
388
	{
389
		return !$this->isEnabled() || $this->_cache_obj->isMiss();
390 259
	}
391
392 259
	/**
393
	 * Empty out the cache in use as best it can
394 259
	 *
395
	 * It may only remove the files of a certain type (if the $type parameter is given)
396
	 * Type can be user, data or left blank
397
	 *  - user clears out user data
398
	 *  - data clears out system / opcode data
399
	 *  - If no type is specified will perform a complete cache clearing
400
	 * For cache engines that do not distinguish on types, a full cache flush will be done
401 259
	 *
402
	 * @param string $type = ''
403 259
	 */
404
	public function clean($type = ''): void
405
	{
406
		if (!$this->isEnabled())
407
		{
408
			return;
409
		}
410
411
		$this->_cache_obj->clean($type);
412
413
		// Invalidate cache, to be sure!
414
		// ... as long as CACHEDIR/index.php can be modified, anyway.
415
		@touch(CACHEDIR . '/index.php');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for touch(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

415
		/** @scrutinizer ignore-unhandled */ @touch(CACHEDIR . '/index.php');

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
416
417
		// Give addons a way to trigger cache cleaning.
418 2
		call_integration_hook('integrate_clean_cache');
419
420 2
		clearstatcache();
421
	}
422 2
423
	/**
424
	 * Retrieves the current level.
425
	 *
426
	 * @return int The current level.
427
	 */
428
	public function getLevel(): int
429
	{
430
		return $this->level;
431
	}
432
433
	/**
434
	 * Set the caching level. Setting it to <= 0 disables caching
435
	 *
436
	 * @param int $level
437
	 *
438
	 * @return $this
439
	 */
440 2
	public function setLevel($level): self
441
	{
442 2
		$this->level = (int) $level;
443
444
		if ($this->level <= 0)
445
		{
446
			$this->enable(false);
447
		}
448
449
		return $this;
450
	}
451
452 3
	/**
453
	 * Checks if the system level is set to a value strictly higher than the
454 3
	 * required level of the cache request.
455
	 *
456 3
	 * @param int $level
457
	 *
458 1
	 * @return bool
459
	 */
460
	public function levelHigherThan($level): bool
461 3
	{
462
		return $this->isEnabled() && $this->level > $level;
463
	}
464
465
	/**
466
	 * Checks if the system level is set to a value strictly lower than the
467
	 * required level of the cache request.
468
	 * Returns true also if the cache is disabled (it's lower than any level).
469
	 *
470
	 * @param int $level
471
	 *
472 251
	 * @return bool
473
	 */
474 251
	public function levelLowerThan($level): bool
475
	{
476
		if (!$this->isEnabled())
477
		{
478
			return true;
479
		}
480
481
		return $this->level < $level;
482
	}
483
484
	/**
485
	 * @param $key
486 6
	 */
487
	public function remove($key): void
488 6
	{
489
		if (!$this->isEnabled())
490
		{
491
			return;
492
		}
493
494 51
		$key = $this->_key($key);
495
		$this->_cache_obj->remove($key);
496 51
	}
497
498 51
	/**
499
	 * Removes one or multiple keys from the cache.
500
	 *
501
	 * Supports the preg_match syntax.
502
	 *
503
	 * @param string|string[] $keys_match The regulat expression/s to match
504
	 *                        the key to remove from the cache.
505
	 * @param string $delimiter The delimiter used by preg_match.
506
	 * @param string $modifiers Any modifier required by the regexp.
507
	 */
508
	public function removeKeys($keys_match, $delimiter = '~', $modifiers = ''): void
509
	{
510
		if (!$this->isEnabled())
511
		{
512
			return;
513
		}
514
515
		$to_remove = (array) $keys_match;
516
		$pattern = $delimiter . implode('|', $to_remove) . $delimiter . $modifiers;
517
518
		foreach ($this->_cached_keys as $cached_key)
519
		{
520
			if (preg_match($pattern, $cached_key) === 1)
521
			{
522
				$this->_cache_obj->remove($cached_key);
523
			}
524
		}
525
	}
526
}
527