* @copyright (c) 2009-16 by Ralf Becker * @version $Id$ */ namespace EGroupware\Api; /** * Class to manage caching in eGroupware. * * It allows to cache on 4 levels: * a) tree: for all instances/domains runining on a certain source path * b) instance: for all sessions on a given instance * c) session: for all requests of a session, same as deprecated egw_session::appsession() * d) request: just for this request (same as using a static variable) * * There's a get, a set and a unset method for each level: eg. getTree() or setInstance(), * as well as a variant allowing to specify the level as first parameter: eg. unsetCache() * * getXXX($app,$location,$callback=null,array $callback_params,$expiration=0) * has three optional parameters allowing to specify: * 3. a callback if requested data is not yes stored. In that case the callback is called * and it's value is stored in the cache AND returned * 4. parameters to pass to the callback as array, see call_user_func_array * 5. an expiration time in seconds to specify how long data should be cached, * default 0 means infinite (this time is not supported for request level!) * * Data is stored under an application name and a location, like egw_session::appsession(). * In fact data stored at cache level Api\Cache::SESSION, is stored in the same way as * egw_session::appsession() so both methods can be used with each other. * * The $app parameter should be either the app or the class name, which both are unique. * * The tree and instance wide cache uses a certain provider class, to store the data * eg. in memcached or if there's nothing else configured in the filesystem (eGW's temp_dir). * * "Admin >> clear cache and register hooks" always only clears instance level cache of * calling instance. It never clears tree level cache, which makes it important to set * reasonable expiry times or think about an other means of clearing that particular item. * (Not clearing of tree-level cache is important, as regenerating it is an expensive * operation for a huge scale EGroupware hosting operation.) * * Apps needing to talk to multiple EGroupware instances (eg. EGroupware Managementserver) * can use install_id of instance as $level parameter to (set|get|unset)Cache method. * * If a callback is used to calculate the data, in case it's not found in the cache, e.g.: * $data = Api\Cache::getCache(Api\Cache::(TREE|INSTANCE), $app, $location, function($params) * { * // code to calculate / query $data * return $data; * }, $params, $expiration); * then we lock the $app:$location by storing some specific data, so only one callback is used * to calculate the data, and other processes sleep until the data is calculate to avoid races. */ class Cache { /** * tree-wide storage */ const TREE = 'Tree'; /** * instance-wide storage */ const INSTANCE = 'Instance'; /** * session-wide storage */ const SESSION = 'Session'; /** * request-wide storage */ const REQUEST = 'Request'; /** * Default provider for tree and instance data * * Can be specified eg. in the header.inc.php by setting: * $GLOBALS['egw_info']['server']['cache_provider_instance'] and optional * $GLOBALS['egw_info']['server']['cache_provider_tree'] (defaults to instance) * * Default is set (if not set here) after class definition to Cache\Apc or Cache\Files, * depending on function 'apc_fetch' exists or not * * @var array */ static $default_provider; // = array('EGroupware\Api\Cache\Files');// array('EGroupware\Api\Cache\Memcache','localhost'); /** * Maximum expiration time, if set unlimited expiration (=0) or bigger expiration times are replaced with that time * * @var int */ static $max_expiration; /** * Used to determine keys for tree- and instance-level caches * * @var string */ static $egw_server_root = EGW_SERVER_ROOT; /** * Add some data in the cache, only if the key does not yet exist * * @param string $level use Api\Cache::(TREE|INSTANCE) * @param string $app application storing data * @param string $location location name for data * @param mixed $data * @param int $expiration =0 expiration time in seconds, default 0 = never * @return boolean true if data could be stored, false otherwise incl. key already existed */ public static function addCache($level,$app,$location,$data,$expiration=0) { //error_log(__METHOD__."('$level','$app','$location',".array2string($data).",$expiration)"); switch($level) { case self::SESSION: case self::REQUEST: throw new Exception\WrongParameter(__METHOD__."('$level', ...) unsupported level parameter!"); case self::INSTANCE: case self::TREE: default: if (!($provider = self::get_provider($level))) { return false; } // limit expiration to configured maximum time if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration)) { $expiration = self::$max_expiration; } return $provider->add(self::keys($level,$app,$location),$data,$expiration); } throw new Exception\WrongParameter(__METHOD__."() unknown level '$level'!"); } /** * Set some data in the cache * * @param string $level use Cache::(TREE|INSTANCE|SESSION|REQUEST) * @param string $app application storing data * @param string $location location name for data * @param mixed $data * @param int $expiration =0 expiration time in seconds, default 0 = never * @return boolean true if data could be stored, false otherwise */ public static function setCache($level,$app,$location,$data,$expiration=0) { //error_log(__METHOD__."('$level','$app','$location',".array2string($data).",$expiration)"); switch($level) { case self::SESSION: case self::REQUEST: return call_user_func(array(__CLASS__,'set'.$level),$app,$location,$data,$expiration); case self::INSTANCE: case self::TREE: default: if (!($provider = self::get_provider($level))) { return false; } // limit expiration to configured maximum time if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration)) { $expiration = self::$max_expiration; } return $provider->set(self::keys($level,$app,$location),$data,$expiration); } throw new Exception\WrongParameter(__METHOD__."() unknown level '$level'!"); } /** * Increment some value in the cache * * @param string $level use Cache::(TREE|INSTANCE), Cache::(SESSION|REQUEST) are NOT supported! * @param string $app application storing data * @param string $location location name for data * @param int $offset =1 how much to increment by * @param int $intial_value =0 value to set and return, if not in cache * @param int $expiration =0 * @return false|int new value on success, false on error */ public static function incrementCache(string $level, string $app, string $location, int $offset=1, int $intial_value=0, int $expiration=0) { //error_log(__METHOD__."('$level', '$app', '$location', $offset, $inital_value, $expiration)"); switch($level) { case self::SESSION: case self::REQUEST: break; case self::INSTANCE: case self::TREE: default: if (!($provider = self::get_provider($level))) { return false; } // limit expiration to configured maximum time if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration)) { $expiration = self::$max_expiration; } return $provider->increment(self::keys($level, $app, $location), $offset, $intial_value, $expiration); } throw new Exception\WrongParameter(__METHOD__."() unsupported level '$level'!"); } /** * Decrements value in cache, but never below 0 * * If new value would be below 0, 0 will be set as new value! * * @param string $level use Cache::(TREE|INSTANCE), Cache::(SESSION|REQUEST) are NOT supported! * @param string $app application storing data * @param string $location location name for data * @param int $offset =1 how much to increment by * @param int $intial_value =0 value to set and return, if not in cache * @param int $expiration =0 * @return false|int new value on success, false on error */ public static function decrementCache(string $level, string $app, string $location, int $offset=1, int $intial_value=0, int $expiration=0) { //error_log(__METHOD__."('$level', '$app', '$location', $offset, $inital_value, $expiration)"); switch($level) { case self::SESSION: case self::REQUEST: break; case self::INSTANCE: case self::TREE: default: if (!($provider = self::get_provider($level))) { return false; } // limit expiration to configured maximum time if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration)) { $expiration = self::$max_expiration; } return $provider->decrement(self::keys($level, $app, $location), $offset, $intial_value, $expiration); } throw new Exception\WrongParameter(__METHOD__."() unsupported level '$level'!"); } /** * Content stored to signal one callback is already running and calculating the data, to not run it multiple times */ const CALLBACK_LOCK = '***LOCKED***'; /** * Maximum time an item is locked, to recover from eg. a crash while the lock is set */ const CALLBACL_LOCK_TIMEOUT = 10; // 10s /** * Time other processes sleep to wait for one callback to calculate the data */ const CALLBACK_LOCK_USLEEP = 50000; // 50ms /** * Get some data from the cache * * @param string $level use Api\Cache::(TREE|INSTANCE|SESSION|REQUEST) * @param string $app application storing data * @param string|array $location location(s) name for data * @param callback $callback =null callback to get/create the value, if it's not cache * @param array $callback_params =array() array with parameters for the callback * @param int $expiration =0 expiration time in seconds, default 0 = never * @return mixed NULL if data not found in cache (and no callback specified) or * if $location is an array: location => data pairs for existing location-data, non-existing is not returned */ public static function getCache($level,$app,$location,$callback=null,array $callback_params=array(),$expiration=0) { switch($level) { case self::SESSION: case self::REQUEST: foreach((array)$location as $l) { $data[$l] = call_user_func(array(__CLASS__,'get'.$level),$app,$l,$callback,$callback_params,$expiration); } return is_array($location) ? $data : $data[$l]; case self::INSTANCE: case self::TREE: default: if (!($provider = self::get_provider($level))) { return null; } try { if (is_array($location)) { if (!is_null($callback)) { throw new Exception\WrongParameter(__METHOD__."() you can NOT use multiple locations (\$location parameter is an array) together with a callback!"); } if (is_a($provider, 'EGroupware\Api\Cache\ProviderMultiple')) { $data = $provider->mget($keys=self::keys($level,$app,$location)); } else // default implementation calls get multiple times { $data = array(); foreach($location as $l) { $data[$l] = $provider->get($keys=self::keys($level,$app,$l)); if (!isset($data[$l])) unset($data[$l]); } } } else { // try reading cached data and, if not found AND a callback to do so given, calculate it // only allow one process to do the (heavy) calculation by storing some lock-data // other processes reading the lock-data sleep for a short time, before try reading it again for($n=10+self::CALLBACL_LOCK_TIMEOUT*1000000/self::CALLBACK_LOCK_USLEEP; $n >= 0; $n--) { $data = $provider->get($keys=self::keys($level,$app,$location)); if ($data === self::CALLBACK_LOCK) { usleep(self::CALLBACK_LOCK_USLEEP); } elseif($data === null && isset($callback)) { $provider->set($keys, self::CALLBACK_LOCK, self::CALLBACL_LOCK_TIMEOUT); try { $data = call_user_func_array($callback, $callback_params); } catch(\Throwable $e) { // remove lock, before re-throwing the exception/error $provider->delete($keys); throw $e; } // limit expiration to configured maximum time if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration)) { $expiration = self::$max_expiration; } $provider->set($keys, $data, $expiration); break; } else { break; } } } } catch(Exception $e) { unset($e); $data = null; } return $data; } throw new Exception\WrongParameter(__METHOD__."() unknown level '$level'!"); } /** * Unset some data in the cache * * @param string $level use Api\Cache::(TREE|INSTANCE|SESSION|REQUEST) * @param string $app application storing data * @param string $location location name for data * @return boolean true if data was set, false if not (like isset()) */ public static function unsetCache($level,$app,$location) { switch($level) { case self::SESSION: case self::REQUEST: return call_user_func(array(__CLASS__,'unset'.$level),$app,$location); case self::INSTANCE: case self::TREE: default: if (!($provider = self::get_provider($level, false))) { return false; } return $provider->delete(self::keys($level,$app,$location)); } throw new Exception\WrongParameter(__METHOD__."() unknown level '$level'!"); } /** * Set some data in the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @param mixed $data * @param int $expiration =0 expiration time in seconds, default 0 = never * @return boolean true if data could be stored, false otherwise */ public static function setTree($app,$location,$data,$expiration=0) { //error_log(__METHOD__."('$app','$location',".array2string($data).",$expiration)"); return self::setCache(self::TREE,$app,$location,$data,$expiration); } /** * Get some data from the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @param callback $callback =null callback to get/create the value, if it's not cache * @param array $callback_params =array() array with parameters for the callback * @param int $expiration =0 expiration time in seconds, default 0 = never * @return mixed NULL if data not found in cache (and no callback specified) */ public static function getTree($app,$location,$callback=null,array $callback_params=array(),$expiration=0) { return self::getCache(self::TREE,$app,$location,$callback,$callback_params,$expiration); } /** * Unset some data in the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @return boolean true if data was set, false if not (like isset()) */ public static function unsetTree($app,$location) { return self::unsetCache(self::TREE,$app,$location); } /** * Set some data in the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @param mixed $data * @param int $expiration =0 expiration time in seconds, default 0 = never * @return boolean true if data could be stored, false otherwise */ public static function setInstance($app,$location,$data,$expiration=0) { return self::setCache(self::INSTANCE,$app,$location,$data,$expiration); } /** * Get some data from the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @param callback $callback =null callback to get/create the value, if it's not cache * @param array $callback_params =array() array with parameters for the callback * @param int $expiration =0 expiration time in seconds, default 0 = never * @return mixed NULL if data not found in cache (and no callback specified) */ public static function getInstance($app,$location,$callback=null,array $callback_params=array(),$expiration=0) { return self::getCache(self::INSTANCE,$app,$location,$callback,$callback_params,$expiration); } /** * Unset some data in the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @return boolean true if data was set, false if not (like isset()) */ public static function unsetInstance($app,$location) { return self::unsetCache(self::INSTANCE,$app,$location); } /** * Prefix for appname to store expiration time in session cache */ const SESSION_EXPIRATION_PREFIX = '*expiration*'; /** * Set some data in the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @param mixed $data * @param int $expiration =0 expiration time in seconds, default 0 = never * @return boolean true if data could be stored, false otherwise */ public static function setSession($app,$location,$data,$expiration=0) { if (isset($_SESSION[Session::EGW_SESSION_ENCRYPTED])) { if (Session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!'); return false; // can no longer store something in the session, eg. because commit_session() was called } $_SESSION[Session::EGW_APPSESSION_VAR][$app][$location] = $data; if ($expiration > 0) { $_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location] = time()+$expiration; } return true; } /** * Get some data from the cache for the whole source tree (all instances) * * Returns a reference to the var in the session! * * @param string $app application storing data * @param string $location location name for data * @param callback $callback =null callback to get/create the value, if it's not cache * @param array $callback_params =array() array with parameters for the callback * @param int $expiration =0 expiration time in seconds, default 0 = never * @return mixed NULL if data not found in cache (and no callback specified) */ public static function &getSession($app, $location, $callback=null, array $callback_params=array(), $expiration=0) { if (isset($_SESSION[Session::EGW_SESSION_ENCRYPTED])) { if (Session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!'); $ret = null; // can no longer store something in the session, eg. because commit_session() was called return $ret; } // check if entry is expired and clean it up in that case if (isset($_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location]) && $_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location] < time()) { unset($_SESSION[Session::EGW_APPSESSION_VAR][$app][$location], $_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location]); } if (!isset($_SESSION[Session::EGW_APPSESSION_VAR][$app][$location]) && !is_null($callback)) { $_SESSION[Session::EGW_APPSESSION_VAR][$app][$location] = call_user_func_array($callback,$callback_params); } return $_SESSION[Session::EGW_APPSESSION_VAR][$app][$location]; } /** * Unset some data in the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @return boolean true if data was set, false if not (like isset()) */ public static function unsetSession($app,$location) { if (isset($_SESSION[Session::EGW_SESSION_ENCRYPTED])) { if (Session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!'); return false; // can no longer store something in the session, eg. because commit_session() was called } // check if entry is expired and clean it up in that case if (isset($_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location]) && $_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location] < time()) { unset($_SESSION[Session::EGW_APPSESSION_VAR][$app][$location], $_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location]); } if (!isset($_SESSION[Session::EGW_APPSESSION_VAR][$app][$location])) { return false; } unset($_SESSION[Session::EGW_APPSESSION_VAR][$app][$location]); return true; } /** * Static varible to cache request wide * * @var array */ private static $request_cache = array(); /** * Set some data in the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @param mixed $data * @param int $expiration =0 expiration time is NOT used for REQUEST! * @return boolean true if data could be stored, false otherwise */ public static function setRequest($app,$location,$data,$expiration=0) { unset($expiration); // not used, but required by function signature self::$request_cache[$app][$location] = $data; return true; } /** * Get some data from the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @param callback $callback =null callback to get/create the value, if it's not cache * @param array $callback_params =array() array with parameters for the callback * @param int $expiration =0 expiration time is NOT used for REQUEST! * @return mixed NULL if data not found in cache (and no callback specified) */ public static function getRequest($app,$location,$callback=null,array $callback_params=array(),$expiration=0) { unset($expiration); // not used, but required by function signature if (!isset(self::$request_cache[$app][$location]) && !is_null($callback)) { self::$request_cache[$app][$location] = call_user_func_array($callback,$callback_params); } return self::$request_cache[$app][$location]; } /** * Unset some data in the cache for the whole source tree (all instances) * * @param string $app application storing data * @param string $location location name for data * @return boolean true if data was set, false if not (like isset()) */ public static function unsetRequest($app,$location) { if (!isset(self::$request_cache[$app][$location])) { return false; } unset(self::$request_cache[$app][$location]); return true; } /** * Get a caching provider for tree or instance level * * The returned provider already has an opened connection * * @param string $level Api\Cache::(TREE|INSTANCE) or install_id * @param boolean $log_not_found =true false do not log if no provider found, used eg. to supress error via unsetCache during installation * @return Cache\Provider|Cache\ProviderMultiple */ static protected function get_provider($level, $log_not_found=true) { static $providers = array(); if ($level != self::TREE) $level = self::INSTANCE; if (!isset($providers[$level])) { $params = $GLOBALS['egw_info']['server']['cache_provider_'.strtolower($level)]; if (!isset($params) && $level == self::INSTANCE && isset(self::$default_provider)) { $params = self::$default_provider; } if (!isset($params)) { if ($level == self::TREE) // if no tree level provider use the instance level one { $providers[$level] = self::get_provider(self::INSTANCE); } else { $providers[$level] = false; // no provider specified $reason = 'no provider specified'; } } elseif (!$params) { $providers[$level] = false; // cache for $level disabled $reason = "cache for $level disabled"; } else { if (!is_array($params)) $params = (array)$params; $class = array_shift($params); if (!class_exists($class)) { $providers[$level] = false; // provider class not found $reason = "provider $class not found"; } else { try { $providers[$level] = new $class($params); } catch(Exception $e) { $providers[$level] = false; // eg. could not open connection to backend $reason = "error instanciating provider $class: ".$e->getMessage(); } } } if (!$providers[$level] && $log_not_found) error_log(__METHOD__."($level) no provider found ($reason)!".function_backtrace()); } //error_log(__METHOD__."($level) = ".array2string($providers[$level]).', cache_provider='.array2string($GLOBALS['egw_info']['server']['cache_provider_'.strtolower($level)])); return $providers[$level]; } /** * Get class-name of caching provider * * @param string $level * @return string class-name of provider */ public static function getProvider($level=self::INSTANCE) { $provider = self::get_provider($level); return get_class($provider); } /** * Get a system configuration, even if in setup and it's not read * * @param string $name * @param boolean $throw =true throw an exception, if we can't retriev the value * @return string|boolean string with config or false if not found and !$throw */ public static function get_system_config($name, $throw=true) { if(!isset($GLOBALS['egw_info']['server'][$name])) { if (isset($GLOBALS['egw_setup']) && isset($GLOBALS['egw_setup']->db) || $GLOBALS['egw']->db) { $db = $GLOBALS['egw']->db ? $GLOBALS['egw']->db : $GLOBALS['egw_setup']->db; try { if (($rs = $db->select(Config::TABLE,'config_value',array( 'config_app' => 'phpgwapi', 'config_name' => $name, ),__LINE__,__FILE__))) { $GLOBALS['egw_info']['server'][$name] = $rs->fetchColumn(); } else { error_log(__METHOD__."('$name', $throw) config value NOT found!");//.function_backtrace()); } } catch(Db\Exception $e) { if ($throw) error_log(__METHOD__."('$name', $throw) cound NOT query value: ".$e->getMessage());//.function_backtrace()); } } if (!$GLOBALS['egw_info']['server'][$name] && $throw) { throw new Exception (__METHOD__."($name) \$GLOBALS['egw_info']['server']['$name'] is NOT set!"); } } return $GLOBALS['egw_info']['server'][$name]; } /** * Flush (delete) whole (instance) cache or application/class specific part of it * * @param string $level =self::INSTANCE * @param string $app =null app-name or "all" to empty complete cache */ public static function flush($level=self::INSTANCE, $app=null) { $ret = true; if (!($provider = self::get_provider($level))) { $ret = false; } else { if (!$provider->flush($app !== "all" ? self::keys($level, $app) : array())) { if ($level == self::INSTANCE) { self::generate_instance_key(); } else { $ret = false; } } } //error_log(__METHOD__."('$level', '$app') returning ".array2string($ret)); return $ret; } /** * Unset instance key, so it get read again and re-read install_id from database */ public static function unset_instance_key() { self::$instance_key = null; $GLOBALS['egw_info']['server']['install_id'] = self::get_system_config('install_id', false); } /** * Key used for instance specific data * * @var string */ private static $instance_key; /** * Generate a new instance key and by doing so effectivly flushes whole instance cache * * @param string $install_id =null default use install_id of current instance * @return string new key also stored in self::$instance_key */ public static function generate_instance_key($install_id=null) { if (!isset($install_id)) { self::$instance_key = null; $install_id = self::get_system_config('install_id'); } $instance_key = self::INSTANCE.'-'.$install_id.'-'.microtime(true); self::setTree(__CLASS__, $install_id, $instance_key); //error_log(__METHOD__."(install_id='$install_id') returning '".$instance_key."'"); return $instance_key; } /** * Get keys array from $level, $app and $location * * @param string $level Api\Cache::(TREE|INSTANCE) or instance_id * @param string $app =null * @param string $location =null * @return array */ public static function keys($level, $app=null, $location=null) { static $tree_key = null; switch($level) { case self::TREE: if (!isset($tree_key)) { $tree_key = $level.'-'.str_replace(array(':','/','\\'),'-', self::$egw_server_root); // add charset to key, if not utf-8 (as everything we store depends on charset!) if (($charset = self::get_system_config('system_charset',false)) && $charset != 'utf-8') { $tree_key .= '-'.$charset; } } $level_key = $tree_key; break; default: // arbitrary install_id given --> check for current instance if ($level !== $GLOBALS['egw_info']['server']['install_id']) { $level_key = self::getTree(__CLASS__, $level); if (!isset($level_key)) $level_key = self::generate_instance_key($level); break; } // fall-through for current instance case self::INSTANCE: if (!isset(self::$instance_key)) { self::$instance_key = self::getTree(__CLASS__, self::get_system_config('install_id')); //error_log(__METHOD__."('$level',...) instance_key read from tree-cache=".array2string(self::$instance_key)); if (!isset(self::$instance_key)) self::$instance_key = self::generate_instance_key(); } $level_key = self::$instance_key; break; } $keys = array($level_key); if (isset($app)) { $keys[] = $app; if (isset($location)) $keys[] = $location; } return $keys; } /** * Let everyone know the methods of this class should be used only statically * */ function __construct() { throw new Exception\WrongParameter("All methods of class ".__CLASS__." should be called static!"); } } // setting apc(u) as default provider, if apc(u)_fetch function exists AND further checks in Api\Cache\Apc(u) recommed it if (is_null(Cache::$default_provider)) { Cache::$default_provider = PHP_SAPI === 'cli' ? 'EGroupware\Api\Cache\Files' : (function_exists('apcu_fetch') && Cache\Apcu::available() ? 'EGroupware\Api\Cache\Apcu' : (function_exists('apc_fetch') && Cache\Apc::available() ? 'EGroupware\Api\Cache\Apc' : 'EGroupware\Api\Cache\Files')); } //error_log('Cache::$default_provider='.array2string(Cache::$default_provider));