* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ /** * Cache class that stores content in a sqlite database. * * @package symfony * @subpackage cache * @author Fabien Potencier * @version SVN: $Id: sfSQLiteCache.class.php 3935 2007-05-06 10:07:08Z fabien $ */ class sfSQLiteCache extends sfCache { const DEFAULT_NAMESPACE = ''; protected $conn = null; /** * File where to put the cache database (or :memory: to store cache in memory) */ protected $database = ''; /** * Disable / Tune the automatic cleaning process * * The automatic cleaning process destroy too old (for the given life time) * cache files when a new cache file is written. * 0 => no automatic cache cleaning * 1 => systematic cache cleaning * x (integer) > 1 => automatic cleaning randomly 1 times on x cache write */ protected $automaticCleaningFactor = 500; /** * Constructor. * * @param string The database name */ public function __construct($database = null) { if (!extension_loaded('sqlite')) { throw new sfConfigurationException('sfSQLiteCache class needs "sqlite" extension'); } $this->setDatabase($database); } /** * Initializes the cache. * * @param array An array of options * Available options: * - database: database name * - automaticCleaningFactor: disable / tune automatic cleaning process (int) * */ public function initialize($options = array()) { if (isset($options['database'])) { $this->setDatabase($options['database']); unset($options['database']); } $availableOptions = array('automaticCleaningFactor'); foreach ($options as $key => $value) { if (!in_array($key, $availableOptions) && sfConfig::get('sf_logging_enabled')) { sfLogger::getInstance()->err(sprintf('sfSQLiteCache cannot take "%s" as an option', $key)); } $this->$key = $value; } } /** * Sets the database name. * * @param string The database name where to store the cache */ public function setDatabase($database) { if (!$database) { return; } $this->database = $database; $new = false; if (':memory:' == $database) { $new = true; } elseif (!is_file($database)) { $new = true; // create cache dir if needed $dir = dirname($database); $current_umask = umask(0000); if (!is_dir($dir)) { @mkdir($dir, 0777, true); } touch($database); umask($current_umask); } if (!($this->conn = @sqlite_open($this->database, 0644, $errmsg))) { throw new sfException(sprintf("Unable to connect to SQLite database: %s", $errmsg)); } if ($new) { $this->createSchema(); } } /** * Creates the database schema. * * @throws sfException */ protected function createSchema() { $statements = array( "CREATE TABLE [cache] ( [id] VARCHAR(255), [namespace] VARCHAR(255), [data] LONGVARCHAR, [created_at] TIMESTAMP )", "CREATE UNIQUE INDEX [cache_unique] ON [cache] ([namespace], [id])", ); foreach ($statements as $statement) { if (!sqlite_query($statement, $this->conn)) { throw new sfException(sqlite_error_string(sqlite_last_error($this->database))); } } } /** * Destructor. */ public function __destruct() { sqlite_close($this->conn); } /** * Gets the database name. * * @return string The database name */ public function getDatabase() { return $this->database; } /** * Tests if a cache is available and (if yes) returns it. * * @param string The cache id * @param string The name of the cache namespace * @param boolean If set to true, the cache validity won't be tested * * @return string The data in the cache (or null if no cache available) * * @see sfCache */ public function get($id, $namespace = self::DEFAULT_NAMESPACE, $doNotTestCacheValidity = false) { $statement = sprintf("SELECT data FROM cache WHERE id = '%s' AND namespace = '%s'", sqlite_escape_string($id), sqlite_escape_string($namespace)); if (!$doNotTestCacheValidity) { $statement .= sprintf(" AND created_at > '%s'", sqlite_escape_string($this->refreshTime)); } $rs = sqlite_query($statement, $this->conn); return sqlite_num_rows($rs) ? sqlite_fetch_single($rs) : null; } /** * Returns true if there is a cache for the given id and namespace. * * @param string The cache id * @param string The name of the cache namespace * @param boolean If set to true, the cache validity won't be tested * * @return boolean true if the cache exists, false otherwise * * @see sfCache */ public function has($id, $namespace = self::DEFAULT_NAMESPACE, $doNotTestCacheValidity = false) { $statement = sprintf("SELECT id FROM cache WHERE id = '%s' AND namespace = '%s'", sqlite_escape_string($id), sqlite_escape_string($namespace)); if (!$doNotTestCacheValidity) { $statement .= sprintf(" AND created_at > '%s'", sqlite_escape_string($this->refreshTime)); } return sqlite_num_rows(sqlite_query($statement, $this->conn)) ? true : false; } /** * Saves some data in the cache. * * @param string The cache id * @param string The name of the cache namespace * @param string The data to put in cache * * @return boolean true if no problem * * @see sfCache */ public function set($id, $namespace = self::DEFAULT_NAMESPACE, $data) { if ($this->automaticCleaningFactor > 0) { $rand = rand(1, $this->automaticCleaningFactor); if ($rand == 1) { $this->clean(false, 'old'); } } if (!$this->has($id, $namespace, true)) { $statement = sprintf("INSERT INTO cache (id, namespace, data, created_at) VALUES ('%s', '%s', '%s', %d)", sqlite_escape_string($id), sqlite_escape_string($namespace), sqlite_escape_string($data), time()); } else { $statement = sprintf("UPDATE cache SET data = '%s', created_at = %s WHERE id = '%s' AND namespace = '%s'", sqlite_escape_string($data), time(), sqlite_escape_string($id), sqlite_escape_string($namespace)); } if (sqlite_query($statement, $this->conn)) { return true; } return false; } /** * Removes an element from the cache. * * @param string The cache id * @param string The name of the cache namespace * * @return boolean true if no problem * * @see sfCache */ public function remove($id, $namespace = self::DEFAULT_NAMESPACE) { $statement = sprintf("DELETE FROM cache WHERE id = '%s' AND namespace = '%s'", sqlite_escape_string($id), sqlite_escape_string($namespace)); if (sqlite_query($statement, $this->conn)) { return true; } return false; } /** * Cleans the cache. * * If no namespace is specified all cache files will be destroyed * else only cache files of the specified namespace will be destroyed. * * @param string The name of the cache namespace * * @return boolean true if no problem */ public function clean($namespace = null, $mode = 'all') { if (!$namespace) { $statement = "DELETE FROM cache"; } else { $statement = sprintf("DELETE FROM cache WHERE namespace LIKE '%s%%'", $namespace); } if ('old' == $mode) { $statement .= sprintf(" %s created_at < '%s'", $namespace ? 'AND' : 'WHERE', sqlite_escape_string($this->refreshTime)); } return sqlite_num_rows(sqlite_query($statement, $this->conn)) ? true : false; } public function lastModified($id, $namespace = self::DEFAULT_NAMESPACE) { $statement = sprintf("SELECT created_at FROM cache WHERE id = '%s' AND namespace = '%s'", sqlite_escape_string($id), sqlite_escape_string($namespace)); $rs = sqlite_query($statement, $this->conn); return sqlite_num_rows($rs) ? intval(sqlite_fetch_single($rs)) : 0; } }