Add an API to handle caching.
authorFlorent Bruneau <florent.bruneau@polytechnique.org>
Sat, 16 Oct 2010 20:44:06 +0000 (22:44 +0200)
committerFlorent Bruneau <florent.bruneau@polytechnique.org>
Sun, 17 Oct 2010 19:31:55 +0000 (21:31 +0200)
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 <florent.bruneau@polytechnique.org>
classes/plcache.php [new file with mode: 0644]
classes/plglobals.php

diff --git a/classes/plcache.php b/classes/plcache.php
new file mode 100644 (file)
index 0000000..9177253
--- /dev/null
@@ -0,0 +1,442 @@
+<?php
+/***************************************************************************
+ *  Copyright (C) 2003-2010 Polytechnique.org                              *
+ *  http://opensource.polytechnique.org/                                   *
+ *                                                                         *
+ *  This program is free software; you can redistribute it and/or modify   *
+ *  it under the terms of the GNU General Public License as published by   *
+ *  the Free Software Foundation; either version 2 of the License, or      *
+ *  (at your option) any later version.                                    *
+ *                                                                         *
+ *  This program is distributed in the hope that it will be useful,        *
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of         *
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the          *
+ *  GNU General Public License for more details.                           *
+ *                                                                         *
+ *  You should have received a copy of the GNU General Public License      *
+ *  along with this program; if not, write to the Free Software            *
+ *  Foundation, Inc.,                                                      *
+ *  59 Temple Place, Suite 330, Boston, MA  02111-1307  USA                *
+ ***************************************************************************/
+
+/** This class provide a common API for caching data.
+ */
+class PlCache
+{
+    /* Data types
+     */
+    const SCRIPT  = 0x0001; /* The value expires after the execution of the script */
+    const SESSION = 0x0002; /* The value is session specific */
+    const TIMER   = 0x0004; /* The value expires after some timeout */
+
+    private static $backends = array();
+
+    private static function getBackend($type)
+    {
+        if (isset(self::$backends[$type])) {
+            return self::$backends[$type];
+        }
+        $globals = Platal::globals();
+        if (($globals->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:
+?>
index c830bad..35369b4 100644 (file)
  *  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).