From e3c131627aa03f7107709e6d44c7840c09847acd Mon Sep 17 00:00:00 2001 From: Florent Bruneau Date: Sat, 16 Oct 2010 22:44:06 +0200 Subject: [PATCH] Add an API to handle caching. In most cases, the API usages is the following: $var = PlCache::get{Variant}('key', 'factory', array('factoryarg1','factoryarg2', ...)); PlCache::invalidate{Variant}, set{Variant} and has{Variant} are also provided. The Variant provides hints on the expiration of the data and whether it can be shared between users/sessions or not. Available variants are: - Local = no sharing, invalidate data after the execution of the page - Session = no sharing, invalidate data when the session dies - Global = sharable, can invalidate data after a timeout Examples: function buildDataFromDb($tableName) { return do_something_very_expensive($tableName); } /* Example using all-in-one 'get' variant' */ $value = PlCache::getSession('blah_' . $tableName', 'buildDataFromDb', array($tableName), 1 /* ensure the value is not computed more than once per second */ ); /* Same example using has, set and get */ if (!PlCache::hasSession('blah_' . $tableName)) { $value = buildDataFromDb($tableName); PlCache::setSession('blah_' . $tableName, $value, 1); } else { $value = PlCache::getSession('blah_' . $tableName); } Note: this second example can in very specific conditions produce invalid results if hasSession is called before expiration of the data and getSession after. Signed-off-by: Florent Bruneau --- classes/plcache.php | 442 ++++++++++++++++++++++++++++++++++++++++++++++++++ classes/plglobals.php | 16 +- 2 files changed, 452 insertions(+), 6 deletions(-) create mode 100644 classes/plcache.php diff --git a/classes/plcache.php b/classes/plcache.php new file mode 100644 index 0000000..9177253 --- /dev/null +++ b/classes/plcache.php @@ -0,0 +1,442 @@ +debug & DEBUG_NOCACHE) != 0) { + $storage = 'none'; + } else if (($globals->debug & DEBUG_SCRIPTCACHE) != 0 + || php_sapi_name() == 'cli') { + $storage = 'static'; + } else { + $storage = 'static'; + switch ($type) { + case self::TIMER: + if ($globals->core->memcache) { + $storage = 'memcache'; + break; + } + + case self::SESSION: + $storage = 'session'; + break; + } + } + if (!isset(self::$backends[$storage])) { + switch ($storage) { + case 'none': + self::$backends['none'] = new PlDummyCache(); + break; + + case 'static': + self::$backends['static'] = new PlStaticCache(); + break; + + case 'session': + self::$backends['session'] = new PlSessionCache(); + break; + + case 'memcache': + $servers = preg_split('/[, ]+/', $globals->core->memcache); + self::$backends['memcache'] = new PlMemcacheCache($servers); + break; + } + } + self::$backends[$type] = self::$backends[$storage]; + return self::$backends[$type]; + } + + + /** Get the value associated with the key in the cache. + * + * If the value does not exists, and a callback is provided, + * the value is built by calling the callback with the given + * expiration time. + * + * @throw PlNotFoundInCacheException if the value is not in the + * cache and $callback is null. + */ + private static function get($key, $type, $callback, $cbargs, $expire) + { + $backend = self::getBackend($type); + return $backend->get($key, $type, $callback, $cbargs, $expire); + } + + /** Invalidate the entry of the cache with the given name. + */ + private static function invalidate($key, $type) + { + $backend = self::getBackend($type); + return $backend->invalidate($key, $type); + } + + /** Set the value associated with the key in the cache. + */ + private static function set($key, $type, $var, $expire) + { + $backend = self::getBackend($type); + return $backend->set($key, $type, $var, $expire); + } + + /** Check if the key exists in the cache. + */ + private static function has($key, $type) + { + $backend = self::getBackend($type); + return $backend->has($key, $type); + } + + + /** Global data storage. Global data is independent from + * the current session and can thus be shared by several + * PHP instances (for example using memcache if enabled). + * + * Global data can expire. The expire argument follow the + * semantic of the Memcache:: API: + * - 0 mean no timeout + * - <= 2592000 mean expires in $expire seconds + * - else $expire is an unix timestamp + */ + + public static function getGlobal($key, $callback = null, $cbargs = null, + $expire = 0) + { + return self::get($key, self::TIMER, $callback, $cbargs, $expire); + } + + public static function invalidateGlobal($key) + { + return self::invalidate($key, self::TIMER); + } + + public static function setGlobal($key, $var, $expire = 0) + { + return self::set($key, self::TIMER, $var, $expire); + } + + public static function hasGlobal($key) + { + return self::has($key, self::TIMER); + } + + + /** Session data storage. Session data is session-dependent + * and thus must not be shared between sessions but can + * be stored in the $_SESSION php variable. + */ + + public static function getSession($key, $callback = null, $cbargs = null) + { + return self::get($key, self::SESSION, $callback, $cbargs, 0); + } + + public static function invalidateSession($key) + { + return self::invalidate($key, self::SESSION); + } + + public static function setSession($key, $var) + { + return self::set($key, self::SESSION, $var, 0); + } + + public static function hasSession($key) + { + return self::has($key, self::SESSION); + } + + + /** Script local data storage. This stores data that + * expires at the end of the execution of the current + * script (or page). + */ + + public static function getLocal($key, $callback = null, $cbargs = null) + { + return self::get($key, self::SCRIPT, $callback, $cbargs, 0); + } + + public static function invalidateLocal($key) + { + return self::invalidate($key, self::SCRIPT); + } + + public static function setLocal($key, $var) + { + return self::set($key, self::SCRIPT, $var, 0); + } + + public static function hasLocal($key) + { + return self::has($key, self::SCRIPT); + } +} + + +/** Exception thrown when trying to get the value associated + * with a missing key. + */ +class PlNotFoundInCacheException extends PlException +{ + public function __construct($key, $type) + { + parent::__construct('Erreur lors de l\'accès aux données', + "Key '$key' not found in cache"); + } +} + + +/** Interface for the storage backend. + */ +interface PlCacheBackend +{ + /** Return true if the backend contains the given key + * for the given storage type. + */ + public function has($key, $type); + + /** Set the value for the given key and type. + */ + public function set($key, $type, $var, $expire); + + /** Get the value for the given key and type. + * + * If the value is not found and a $callback is provided, + * call the function, pass $cbargs as arguments and use + * its output as the new value of the entry. + */ + public function get($key, $type, $callback, $cbargs, $expire); + + /** Remove the entry from the cache. + */ + public function invalidate($key, $type); +} + +class PlDummyCache implements PlCacheBackend +{ + public function has($key, $type) + { + return false; + } + + public function set($key, $type, $var, $expire) + { + } + + public function get($key, $type, $callback, $cbargs, $expire) + { + if (!is_null($callback)) { + return call_user_func_array($callback, $cbargs); + } else { + throw new PlNotFoundInCacheException($key, $type); + } + } + + public function invalidate($key, $type) + { + } +} + +abstract class PlArrayCache implements PlCacheBackend +{ + protected function getData(array $data, $key, $type) + { + $key = $this->arrayKey($key, $type); + if (!isset($data[$key])) { + throw new PlNotFoundInCacheException($key, $type); + } + if ($type == PlCache::TIMER) { + $entry = $data[$key]; + $timeout = $entry['timeout']; + if (time() > $timeout) { + throw new PlNotFoundInCacheException($key, $type); + } + return $entry['data']; + } + return $data[$key]; + } + + protected function buildData($key, $type, $var, $expire) + { + if ($type == PlCache::TIMER) { + if ($expire == 0) { + $expire = 2592000; + } + if ($expire <= 2592000) { + $expire = time() + $expire; + } + return array('timeout' => $expire, + 'data' => $var); + } + return $var; + } + + protected function getAndSetData(array $data, $key, $type, + $callback, $cbargs, $expire) + { + if (is_null($callback)) { + return $this->getData($data, $key, $type); + } else { + try { + $value = $this->getData($data, $key, $type); + } catch (PlNotFoundInCacheException $e) { + $value = call_user_func_array($callback, $cbargs); + $this->set($key, $type, $value, $expire); + } + return $value; + } + } + + protected abstract function arrayKey($key, $type); + + public function has($key, $type) + { + try { + $this->get($key, $type, null, null, 0); + return true; + } catch (PlNotFoundInCacheException $e) { + return false; + } + } +} + +class PlStaticCache extends PlArrayCache +{ + private $data = array(); + + protected function arrayKey($key, $type) + { + return $key; + } + + public function get($key, $type, $callback, $cbargs, $expire) + { + return $this->getAndSetData($this->data, $key, $type, + $callback, $cbargs, $expire); + } + + public function set($key, $type, $var, $expire) + { + $this->data[$this->arrayKey($key, $type)] + = $this->buildData($key, $type, $var, $expire); + } + + public function invalidate($key, $type) + { + unset($this->data[$key]); + } +} + +class PlSessionCache extends PlArrayCache +{ + public function __construct() + { + } + + protected function arrayKey($key, $type) + { + return '__cache_' . $key; + } + + public function get($key, $type, $callback, $cbargs, $expire) + { + return $this->getAndSetData($_SESSION, $key, $type, + $callback, $cbargs, $expire); + } + + public function set($key, $type, $var, $expire) + { + S::set($this->arrayKey($key, $type), + $this->buildData($key, $type, $var, $expire)); + } + + public function invalidate($key, $type) + { + S::kill($this->arrayKey($key, $type)); + } +} + +class PlMemcacheCache implements PlCacheBackend +{ + private $context; + + public function __construct(array $servers) + { + $this->context = new Memcache(); + foreach ($servers as $address) { + /* XXX: Not IPv6 ready. + */ + if (strpos($address, ':') !== false) { + list($addr, $port) = explode(':', $address, 2); + $this->context->addServer($addr, $port); + } else { + $this->context->addServer($address); + } + } + } + + public function has($key, $type) + { + return $this->context->get($key) !== false; + } + + public function get($key, $type, $callbac, $cbargs, $expire) + { + $value = $this->context->get($key); + if ($value === false) { + if (is_null($callback)) { + throw new PlNotFoundInCacheException($key); + } + $value = call_user_func_array($callback, $cbargs); + $this->set($key, $type, $value, $expire); + } + return $value; + } + + public function set($key, $type, $var, $expire) + { + return $this->context->set($key, $var, 0, $expire); + } + + public function invalidate($key, $type) + { + return $this->context->delete($key); + } +} + +// vim:set et sw=4 sts=4 sws=4 foldmethod=marker enc=utf-8: +?> diff --git a/classes/plglobals.php b/classes/plglobals.php index c830bad..35369b4 100644 --- a/classes/plglobals.php +++ b/classes/plglobals.php @@ -19,13 +19,17 @@ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * ***************************************************************************/ /** Debug levels: - * DEBUG_BT = show the backtraces (SQL/XMLRPC/...) - * DEBUG_VALID = run html validation - * DEBUG_SMARTY = don't hide smarty errors/warnings/notices + * DEBUG_BT = show the backtraces (SQL/XMLRPC/...) + * DEBUG_VALID = run html validation + * DEBUG_SMARTY = don't hide smarty errors/warnings/notices + * DEBUG_NOCACHE = disable cache + * DEBUG_SCRIPTCACHE = cache expires after the execution of the script */ -define('DEBUG_BT', 1); -define('DEBUG_VALID', 2); -define('DEBUG_SMARTY', 4); +define('DEBUG_BT', 1); +define('DEBUG_VALID', 2); +define('DEBUG_SMARTY', 4); +define('DEBUG_NOCACHE', 8); +define('DEBUG_SCRIPTCACHE', 16); /* First allowed value for user-defined DEBUG_* flags. * Set to 256 to keep rooms for future core flags (5 flags available). -- 2.1.4