plugins/cache/classes/yf_cache.class.php
<?php
/**
* Caching layer.
*
* @author YFix Team <yfix.dev@gmail.com>
* @version 1.0
*/
class yf_cache
{
/** @var int Cache entries time-to-live (in seconds) */
public $TTL = 3600;
/** @var string Cache driver to use */
public $DRIVER = 'memcache';
/** @var string Namespace for drivers other than 'file' */
public $CACHE_NS = '';
/** @var bool Allows to turn off cache at any moment. Useful for unit tests and complex situations. */
public $NO_CACHE = false;
/** @var bool Forcing to delete elements */
public $FORCE_REBUILD_CACHE = false;
/** @var bool Add random value for each entry TTL (to avoid one-time cache invalidation problems) */
public $RANDOM_TTL_ADD = true;
/** @var bool Force cache class to generate unique namespace, based on project_path. Usually needed to separate projects within same cache storage (memcached as example) */
public $AUTO_CACHE_NS = false;
/**
* Catch missing method call.
* @param mixed $name
* @param mixed $args
*/
public function __call($name, $args)
{
$self = main()->get_class_name($this);
$func = null;
if (isset($this->_extend[$name])) {
$func = $this->_extend[$name];
} elseif (isset(main()->_extend[$self][$name])) {
$func = main()->_extend[$self][$name];
}
if ($func) {
return $func($args[0], $args[1], $args[2], $args[3], $this);
}
// Support for driver-specific methods
if (is_object($this->_driver)) {
return call_user_func_array([$this->_driver, $name], $args);
}
trigger_error(__CLASS__ . ': No method ' . $name, E_USER_WARNING);
return false;
}
public function __clone()
{
foreach ((array) get_object_vars($this) as $k => $v) {
if ($k[0] == '_') {
unset($this->$k);
}
}
}
/**
* Framework constructor.
* @param mixed $params
*/
public function _init($params = [])
{
if (isset($this->_init_complete)) {
return true;
}
$this->_init_settings();
$this->_connect($params);
$this->_init_complete = true;
}
/**
* @param mixed $params
*/
public function _init_settings($params = [])
{
// backwards compatibility
if ($this->FILES_TTL) {
$this->TTL = $this->FILES_TTL;
}
$conf_cache_ns = conf('CACHE_NS');
// Cache namespace need to be unique, especially when using memcached shared between several projects
if ( ! $conf_cache_ns && ! $this->CACHE_NS && $this->AUTO_CACHE_NS) {
$this->CACHE_NS = substr(md5(PROJECT_PATH), 0, 8) . '_';
}
if ($conf_cache_ns) {
$this->CACHE_NS = $conf_cache_ns;
}
$conf_no_cache = conf('NO_CACHE');
// backwards compatibility
if (defined('USE_CACHE')) {
if ( ! USE_CACHE) {
$this->NO_CACHE = true;
$this->_NO_CACHE_WHY = 'const NO_CACHE defined and false';
}
} elseif ($conf_no_cache !== null && $conf_no_cache) {
$this->NO_CACHE = true;
$this->_NO_CACHE_WHY = 'conf(NO_CACHE) is true';
} elseif ( ! main()->USE_SYSTEM_CACHE) {
$this->NO_CACHE = true;
$this->_NO_CACHE_WHY = 'main()->USE_SYSTEM_CACHE == false';
}
if (($_GET['no_core_cache'] || $_GET['no_cache']) && $this->_url_action_allowed('no_cache')) {
$this->NO_CACHE = true;
$this->_NO_CACHE_WHY = '$_GET param no_cache';
}
if ($this->NO_CACHE && ! $this->_NO_CACHE_WHY) {
$this->_NO_CACHE_WHY = 'cache()->NO_CACHE == true';
}
if (($_GET['refresh_cache'] || $_GET['rebuild_core_cache']) && $this->_url_action_allowed('refresh_cache')) {
$this->FORCE_REBUILD_CACHE = true;
}
$this->FORCE_REBUILD_CACHE = false;
}
/**
* Callback that can be overriden to ensure security when allowing url params like no_cache, refresh_cache
* We can add DEBUG_MODE checking here to not allow refresh_cache attacks, maybe add check for: conf('cache_refresh_token', 'something_random'), main()->CACHE_CONTROL_FROM_URL.
* @param mixed $action
*/
public function _url_action_allowed($action = '')
{
$actions = ['no_cache', 'refresh_cache'];
$callback = conf('cache_url_action_allowed');
if (is_callable($callback)) {
return (bool) $callback($action);
}
return true;
}
/**
* @param mixed $params
*/
public function _connect($params = [])
{
if ( ! $this->DRIVER) {
return null;
}
if (isset($this->_tried_to_connect)) {
return $this->_driver;
}
$this->_driver = null;
$this->_driver_ok = false;
$driver = $this->_set_current_driver($params);
if ($driver) {
$this->_driver = _class('cache_driver_' . $driver, 'classes/cache/');
$this->_driver_ok = $this->_driver->is_ready();
$implemented = [];
foreach (get_class_methods($this->_driver) as $method) {
if ($method[0] != '_') {
$implemented[$method] = $method;
}
}
$this->_driver->implemented = $implemented;
$this->_driver->_parent = $this;
} else {
trigger_error('CACHE: empty driver name, will not use cache', E_USER_WARNING);
}
$this->_tried_to_connect = true;
return $this->_driver;
}
/**
* @param mixed $params
*/
public function _set_current_driver($params = [])
{
$avail_drivers = $this->_get_avail_drivers_list();
$driver = '';
$want = isset($params['driver']) ? $params['driver'] : $this->DRIVER;
if ( ! $want || $want == 'auto') {
$want = 'memcache';
}
if (isset($avail_drivers[$want])) {
$driver = $want;
}
$this->DRIVER = $driver;
return $driver;
}
public function _get_avail_drivers_list()
{
$prefix = 'cache_driver_';
$suffix = '.class.php';
$plen = strlen($prefix);
$slen = strlen($suffix);
$pattern = '{,plugins/*/}classes/cache/*' . $prefix . '*' . $suffix;
$globs = [
'app' => APP_PATH . $pattern,
'framework' => YF_PATH . $pattern,
];
$drivers = [];
foreach ($globs as $glob) {
foreach (glob($glob, GLOB_BRACE) as $f) {
$f = basename($f);
$name = substr($f, strpos($f, $prefix) + $plen, -$slen);
if ($name) {
$drivers[$name] = $name;
}
}
}
return $drivers;
}
/**
* Get data from cache, if data is empty - then execute callback and store result in cache.
* @param mixed $name
* @param mixed $ttl
*/
public function getset($name, callable $func, $ttl = 0, array $params = [])
{
if ($name === false || $name === null || ! is_string($name)) {
return null;
}
if ( ! @$params['force']) {
$result = $this->get($name, $ttl, $params);
}
if (@$result === null) {
$result = $func($name, $ttl, $params, $this);
if ($result !== null) {
$this->set($name, $result, $ttl);
}
}
return $result;
}
/**
* Get data from cache.
* @param mixed $name
* @param mixed $force_ttl
*/
public function get($name, $force_ttl = 0, array $params = [])
{
if ($name === false || $name === null || ! is_string($name)) {
return null;
}
$do_real_work = true;
if ( ! $this->_driver_ok || empty($name) || $this->NO_CACHE) {
$do_real_work = false;
}
if ($this->FORCE_REBUILD_CACHE) {
$this->del($name, true);
$do_real_work = false;
}
if (DEBUG_MODE) {
$time_start = microtime(true);
}
$key_name_ns = $this->CACHE_NS . $name;
$result = null;
if ($do_real_work) {
$result = $this->_driver->get($key_name_ns, $force_ttl, $params);
}
DEBUG_MODE && debug('cache_' . __FUNCTION__ . '[]', [
'name' => $name,
'name_real' => $key_name_ns,
'data' => $result,
'driver' => $this->DRIVER,
'params' => $params,
'force_ttl' => $force_ttl,
'time' => round(microtime(true) - $time_start, 5),
'trace' => main()->trace_string(),
'did_real_work' => (int) $do_real_work,
]);
if ($_GET['refresh_cache'] && $this->_url_action_allowed('refresh_cache')) {
return null;
}
return $result;
}
/**
* Set data into cache.
* @param mixed $name
* @param mixed $data
* @param mixed $ttl
*/
public function set($name, $data, $ttl = 0)
{
if ($name === false || $name === null) {
return null;
}
$do_real_work = true;
if ( ! $this->_driver_ok || $this->NO_CACHE || $this->_no_cache[$name]) {
$do_real_work = false;
}
if (is_array($name)) {
return $this->multi_set($name, $data);
}
if (DEBUG_MODE) {
$time_start = microtime(true);
}
$ttl = (int) ($ttl ?: $this->TTL);
if ($this->RANDOM_TTL_ADD) {
$ttl += mt_rand(1, 15);
}
$key_name_ns = $this->CACHE_NS . $name;
$result = null;
if ($do_real_work) {
$result = $this->_driver->set($key_name_ns, $data, $ttl);
}
DEBUG_MODE && debug('cache_' . __FUNCTION__ . '[]', [
'name' => $name,
'name_real' => $key_name_ns,
'data' => $data,
'driver' => $this->DRIVER,
'ttl' => $ttl,
'time' => round(microtime(true) - $time_start, 5),
'trace' => main()->trace_string(),
'did_real_work' => (int) $do_real_work,
]);
return $result;
}
/**
* Delete selected cache entry.
* @param mixed $name
*/
public function del($name)
{
if ($name === false || $name === null) {
return null;
}
$do_real_work = true;
if ( ! $this->_driver_ok) {
$do_real_work = false;
}
if (is_array($name)) {
return $this->multi_del($name);
}
if (DEBUG_MODE) {
$time_start = microtime(true);
}
$key_name_ns = $this->CACHE_NS . $name;
$result = null;
if ($do_real_work) {
$result = $this->_driver->del($key_name_ns);
}
DEBUG_MODE && debug('cache_' . __FUNCTION__ . '[]', [
'name' => $name,
'name_real' => $key_name_ns,
'driver' => $this->DRIVER,
'time' => round(microtime(true) - $time_start, 5),
'did_real_work' => (int) $do_real_work,
]);
return $result;
}
/**
* Delete selected cache entry (alias).
* @param mixed $name
*/
public function refresh($name = '')
{
return $this->del($name, true);
}
/**
* Clean selected cache entry (alias).
* @param mixed $name
*/
public function clean($name = '')
{
return $this->del($name, true);
}
/**
* Clean selected cache entry (alias).
* @param mixed $name
*/
public function clear($name = '')
{
return $this->del($name, true);
}
/**
* Put data into cache (alias for 'set').
* @param mixed $name
* @param null|mixed $data
* @param mixed $ttl
*/
public function put($name = '', $data = null, $ttl = 0)
{
return $this->set($name, $data, $ttl);
}
/**
* Clean all cache entries.
*/
public function flush()
{
$do_real_work = true;
if ( ! $this->_driver_ok) {
$do_real_work = false;
}
if (DEBUG_MODE) {
$time_start = microtime(true);
}
if ($do_real_work) {
$result = $this->_driver->flush();
}
DEBUG_MODE && debug('cache_' . __FUNCTION__ . '[]', [
'data' => $result,
'driver' => $this->DRIVER,
'time' => microtime(true) - $time_start,
'did_real_work' => (int) $do_real_work,
]);
return $result;
}
/**
* Clean all cache entries (alias).
*/
public function clean_all()
{
return $this->flush();
}
/**
* Clean all cache entries (alias).
*/
public function clear_all()
{
return $this->flush();
}
/**
* Clean all cache entries (alias).
*/
public function refresh_all()
{
return $this->flush();
}
/**
* Clears all cache entries (alias).
*/
public function _clear_cache_files()
{
return $this->flush();
}
/**
* Get several cache entries at once.
* @param mixed $names
* @param mixed $force_ttl
* @param mixed $params
*/
public function multi_get($names = [], $force_ttl = 0, $params = [])
{
$do_real_work = true;
if ( ! $this->_driver_ok || $this->NO_CACHE) {
$do_real_work = false;
}
if (DEBUG_MODE) {
$time_start = microtime(true);
}
if ( ! empty($this->_no_cache)) {
foreach ((array) $names as $k => $name) {
if (isset($this->_no_cache[$name])) {
unset($names[$k]);
}
}
}
$result = null;
if ($do_real_work) {
if ($this->_driver->implemented['multi_get']) {
// Fix names prefix
$p_len = strlen($this->CACHE_NS);
foreach ((array) $names as $k => $name) {
$names[$k] = $this->CACHE_NS . $name;
}
$result = $this->_driver->multi_get($names, $force_ttl, $params);
// Fix names prefix
foreach ((array) $result as $name => $val) {
$result[substr($name, $p_len)] = $val;
unset($result[$name]);
}
} else {
$result = [];
foreach ((array) $names as $name) {
$res = $this->get($name, $force_ttl, $params);
if (isset($res)) {
$result[$name] = $res;
}
}
}
if ( ! is_array($result) || ! count((array) $result)) {
$result = null;
}
}
DEBUG_MODE && debug('cache_' . __FUNCTION__ . '[]', [
'names' => $names,
'data' => $result,
'driver' => $this->DRIVER,
'time' => microtime(true) - $time_start,
'did_real_work' => (int) $do_real_work,
]);
return $result;
}
/**
* Set several cache entries at once.
* @param mixed $data
* @param mixed $ttl
*/
public function multi_set($data = [], $ttl = 0)
{
$do_real_work = true;
if ( ! $this->_driver_ok || $this->NO_CACHE) {
$do_real_work = false;
}
if (DEBUG_MODE) {
$time_start = microtime(true);
}
if ( ! empty($this->_no_cache)) {
foreach ((array) $this->_no_cache as $name => $tmp) {
if (isset($data[$name])) {
unset($data[$name]);
}
}
}
$result = null;
if ($do_real_work) {
if ($this->_driver->implemented['multi_set']) {
// Fix names prefix
foreach ((array) $data as $name => $_data) {
$data[$this->CACHE_NS . $name] = $_data;
unset($data[$name]);
}
$result = $this->_driver->multi_set($data, $ttl);
} else {
$failed = false;
foreach ((array) $data as $name => $_data) {
if ( ! $this->set($name, $_data, $ttl)) {
$failed = true;
}
}
$result = ! $failed;
}
if ( ! $result) {
$result = null;
}
}
DEBUG_MODE && debug('cache_' . __FUNCTION__ . '[]', [
'data' => $data,
'driver' => $this->DRIVER,
'time' => microtime(true) - $time_start,
'did_real_work' => (int) $do_real_work,
]);
return $result;
}
/**
* Del several cache entries at once.
* @param mixed $names
*/
public function multi_del($names = [])
{
$do_real_work = true;
if ( ! $this->_driver_ok) {
$do_real_work = false;
}
if (DEBUG_MODE) {
$time_start = microtime(true);
}
$result = null;
if ($do_real_work) {
$old_names = $names;
$failed = false;
$implemented = $this->_driver->implemented['multi_del'];
if ($implemented) {
// Fix names prefix
foreach ((array) $names as $k => $name) {
$names[$k] = $this->CACHE_NS . $name;
}
$result = $this->_driver->multi_del($names);
if ( ! $result) {
$failed = true;
}
}
if ( ! $implemented || $result === null) {
$names = $old_names;
foreach ((array) $names as $name) {
if ( ! $this->del($name)) {
$failed = true;
}
}
}
$result = $failed ? null : true;
}
DEBUG_MODE && debug('cache_' . __FUNCTION__ . '[]', [
'names' => $names,
'data' => $result,
'driver' => $this->DRIVER,
'time' => microtime(true) - $time_start,
'did_real_work' => (int) $do_real_work,
]);
return $result;
}
public function list_keys()
{
$do_real_work = true;
if ( ! $this->_driver_ok) {
$do_real_work = false;
}
if (DEBUG_MODE) {
$time_start = microtime(true);
}
if ( ! $this->_driver->implemented['list_keys']) {
$do_real_work = false;
}
$result = null;
if ($do_real_work) {
$result = $this->_driver->list_keys();
if ($this->CACHE_NS && is_array($result) && count((array) $result)) {
$ns_len = strlen($this->CACHE_NS);
foreach ($result as $k => $v) {
if (substr($v, 0, $ns_len) !== $this->CACHE_NS) {
unset($result[$k]);
} else {
$result[$k] = substr($v, $ns_len);
}
}
}
if (is_array($result) && count((array) $result)) {
asort($result);
$result = array_values($result);
} else {
$result = null;
}
}
DEBUG_MODE && debug('cache_' . __FUNCTION__ . '[]', [
'data' => $result,
'driver' => $this->DRIVER,
'time' => microtime(true) - $time_start,
'did_real_work' => (int) $do_real_work,
]);
return $result;
}
/**
* @param mixed $prefix
*/
public function del_by_prefix($prefix = '')
{
$do_real_work = true;
if ( ! $this->_driver_ok) {
$do_real_work = false;
}
if (DEBUG_MODE) {
$time_start = microtime(true);
}
$result = null;
if ($do_real_work) {
if ( ! strlen($prefix) || ! is_string($prefix)) {
$result = $this->flush();
} else {
$result = false;
$prefix_len = strlen($prefix);
$keys = $this->list_keys();
if ($keys === null) {
$result = $this->flush();
} elseif ($keys) {
foreach ($keys as $k => $v) {
if (substr($v, 0, $prefix_len) !== $prefix) {
unset($keys[$k]);
}
}
if ($keys) {
$result = $this->multi_del($keys);
} else {
$result = true;
}
}
}
}
DEBUG_MODE && debug('cache_' . __FUNCTION__ . '[]', [
'prefix' => $prefix,
'data' => $result,
'driver' => $this->DRIVER,
'time' => microtime(true) - $time_start,
'did_real_work' => (int) $do_real_work,
]);
return $result;
}
}