Rev 290 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
<?php
class CacheSqlite {
/**
* Options disponibles :
*
* ====> (string) stockage_chemin :
* Chemin vers le fichier contenant la base SQLite.
*
*
* ====> (int) defragmentation_auto :
* - Désactive / Régler le processus de défragmentation automatique
* - Le processus de défragmentation automatiques réduit la taille du fichier contenant la base de données
* quand un ajout ou une suppression de cache est réalisée :
* 0 => pas de défragmentation automatique
* 1 => défragmentation automatique systématique
* x (integer) > 1 => défragmentation automatique toutes les 1 fois (au hasard) sur x ajout ou suppression de cache
*
* @var array options disponibles
*/
protected $options = array(
'stockage_chemin' => null,
'defragmentation_auto' => 10
);
/**
* DB ressource
*
* @var mixed $db
*/
private $bdd = null;
/**
* Boolean to store if the structure has benn checked or not
*
* @var boolean $structure_ok
*/
private $structure_ok = false;
private $Cache = null;
/**
* Constructor
*
* @param array $options Associative array of options
* @throws Zend_cache_Exception
* @return void
*/
public function __construct(array $options = array(), Cache $cache) {
$this->Cache = $cache;
if (extension_loaded('sqlite')) {
$this->initialiserOptionsParConfig();
$this->setOptions($options);
} else {
$e = "Impossible d'utiliser le cache SQLITE car l'extenssion 'sqlite' n'est pas chargée dans l'environnement PHP courrant.";
trigger_error($e, E_USER_ERROR);
}
}
private function initialiserOptionsParConfig() {
while (list($nom, $valeur) = each($this->options)) {
if (Config::existe($nom)) {
$this->options[$nom] = Config::get($nom);
}
}
}
/**
* Destructor
*
* @return void
*/
public function __destruct() {
@sqlite_close($this->getConnexion());
}
private function setOptions($options) {
while (list($nom, $valeur) = each($options)) {
if (!is_string($nom)) {
trigger_error("Nom d'option incorecte : $nom", E_USER_WARNING);
}
$nom = strtolower($nom);
if (array_key_exists($nom, $this->options)) {
$this->options[$nom] = $valeur;
}
}
}
public function setEmplacement($emplacement) {
if (extension_loaded('sqlite')) {
$this->options['stockage_chemin'] = $emplacement;
} else {
trigger_error("Impossible d'utiliser le mode de sotckage SQLite car l'extenssion 'sqlite' n'est pas chargé dans ".
"l'environnement PHP courrant.", E_USER_ERROR);
}
}
/**
* Test if a cache is available for the given id and (if yes) return it (false else)
*
* @param string $id Cache id
* @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
* @return string|false Cached datas
*/
public function charger($id, $ne_pas_tester_validiter_du_cache = false) {
$this->verifierEtCreerStructureBdd();
$requete = "SELECT content FROM cache WHERE id = '$id'".
(($ne_pas_tester_validiter_du_cache) ? '' : ' AND (expire = 0 OR expire > '.time().')');
$resultat = $this->requeter($requete);
$ligne = @sqlite_fetch_array($resultat);
return ($ligne) ? $ligne['content'] : false;
}
/**
* Test if a cache is available or not (for the given id)
*
* @param string $id Cache id
* @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record
*/
public function tester($id) {
$this->verifierEtCreerStructureBdd();
$requete = "SELECT lastModified FROM cache WHERE id = '$id' AND (expire = 0 OR expire > ".time().')';
$resultat = $this->requeter($requete);
$ligne = @sqlite_fetch_array($resultat);
return ($ligne) ? ((int) $ligne['lastModified']) : false;
}
/**
* Save some string datas into a cache record
*
* Note : $data is always "string" (serialization is done by the
* core not by the backend)
*
* @param string $data Datas to cache
* @param string $id Cache id
* @param array $tags Array of strings, the cache record will be tagged by each string entry
* @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
* @throws Zend_Cache_Exception
* @return boolean True if no problem
*/
public function sauver($donnees, $id, $tags = array(), $duree_vie_specifique = false) {
$this->verifierEtCreerStructureBdd();
//FIXME : si l'extension n'est pas installée, le cache passe tout de même par cette fonction et s'arrête à cet endroit.
$donnees = @sqlite_escape_string($donnees);
$timestamp_courrant = time();
$expiration = $this->Cache->getTimestampExpiration($duree_vie_specifique);
$this->requeter("DELETE FROM cache WHERE id = '$id'");
$sql = "INSERT INTO cache (id, content, lastModified, expire) VALUES ('$id', '$donnees', $timestamp_courrant, $expiration)";
$resultat = $this->requeter($sql);
if (!$resultat) {
// TODO : ajouter un log sauver() : impossible de stocker le cache d'id '$id'
Debug::printr("sauver() : impossible de stocker le cache d'id '$id'");
$resultat = false;
} else {
$resultat = true;
foreach ($tags as $tag) {
$resultat = $this->enregisterTag($id, $tag) && $resultat;
}
}
return $resultat;
}
/**
* Remove a cache record
*
* @param string $id Cache id
* @return boolean True if no problem
*/
public function supprimer($id) {
$this->verifierEtCreerStructureBdd();
$resultat = $this->requeter("SELECT COUNT(*) AS nbr FROM cache WHERE id = '$id'");
$resultat_nbre = @sqlite_fetch_single($resultat);
$suppression_cache = $this->requeter("DELETE FROM cache WHERE id = '$id'");
$suppression_tags = $this->requeter("DELETE FROM tag WHERE id = '$id'");
$this->defragmenterAutomatiquement();
return ($resultat_nbre && $suppression_cache && $suppression_tags);
}
/**
* Clean some cache records
*
* Available modes are :
* Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used)
* Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used)
* Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags
* ($tags can be an array of strings or a single string)
* Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
* ($tags can be an array of strings or a single string)
* Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
* ($tags can be an array of strings or a single string)
*
* @param string $mode Clean mode
* @param array $tags Array of tags
* @return boolean True if no problem
*/
public function nettoyer($mode = Cache::NETTOYAGE_MODE_TOUS, $tags = array()) {
$this->verifierEtCreerStructureBdd();
$retour = $this->nettoyerSqlite($mode, $tags);
$this->defragmenterAutomatiquement();
return $retour;
}
/**
* Return an array of stored cache ids
*
* @return array array of stored cache ids (string)
*/
public function getIds() {
$this->verifierEtCreerStructureBdd();
$resultat = $this->requeter('SELECT id FROM cache WHERE (expire = 0 OR expire > '.time().')');
$retour = array();
while ($id = @sqlite_fetch_single($resultat)) {
$retour[] = $id;
}
return $retour;
}
/**
* Return an array of stored tags
*
* @return array array of stored tags (string)
*/
public function getTags() {
$this->verifierEtCreerStructureBdd();
$resultat = $this->requeter('SELECT DISTINCT(name) AS name FROM tag');
$retour = array();
while ($id = @sqlite_fetch_single($resultat)) {
$retour[] = $id;
}
return $retour;
}
/**
* Return an array of stored cache ids which match given tags
*
* In case of multiple tags, a logical AND is made between tags
*
* @param array $tags array of tags
* @return array array of matching cache ids (string)
*/
public function getIdsAvecLesTags($tags = array()) {
$this->verifierEtCreerStructureBdd();
$premier = true;
$ids = array();
foreach ($tags as $tag) {
$resultat = $this->requeter("SELECT DISTINCT(id) AS id FROM tag WHERE name='$tag'");
if ($resultat) {
$lignes = @sqlite_fetch_all($resultat, SQLITE_ASSOC);
$ids_tmp = array();
foreach ($lignes as $ligne) {
$ids_tmp[] = $ligne['id'];
}
if ($premier) {
$ids = $ids_tmp;
$premier = false;
} else {
$ids = array_intersect($ids, $ids_tmp);
}
}
}
$retour = array();
if (count($ids) > 0) {
foreach ($ids as $id) {
$retour[] = $id;
}
}
return $retour;
}
/**
* Return an array of stored cache ids which don't match given tags
*
* In case of multiple tags, a logical OR is made between tags
*
* @param array $tags array of tags
* @return array array of not matching cache ids (string)
*/
public function getIdsSansLesTags($tags = array()) {
$this->verifierEtCreerStructureBdd();
$resultat = $this->requeter('SELECT id FROM cache');
$lignes = @sqlite_fetch_all($resultat, SQLITE_ASSOC);
$retour = array();
foreach ($lignes as $ligne) {
$id = $ligne['id'];
$correspondance = false;
foreach ($tags as $tag) {
$resultat = $this->requeter("SELECT COUNT(*) AS nbr FROM tag WHERE name = '$tag' AND id = '$id'");
if ($resultat) {
$nbre = (int) @sqlite_fetch_single($resultat);
if ($nbre > 0) {
$correspondance = true;
}
}
}
if (!$correspondance) {
$retour[] = $id;
}
}
return $retour;
}
/**
* Return an array of stored cache ids which match any given tags
*
* In case of multiple tags, a logical AND is made between tags
*
* @param array $tags array of tags
* @return array array of any matching cache ids (string)
*/
public function getIdsAvecUnTag($tags = array()) {
$this->verifierEtCreerStructureBdd();
$premier = true;
$ids = array();
foreach ($tags as $tag) {
$resultat = $this->requeter("SELECT DISTINCT(id) AS id FROM tag WHERE name = '$tag'");
if ($resultat) {
$lignes = @sqlite_fetch_all($resultat, SQLITE_ASSOC);
$ids_tmp = array();
foreach ($lignes as $ligne) {
$ids_tmp[] = $ligne['id'];
}
if ($premier) {
$ids = $ids_tmp;
$premier = false;
} else {
$ids = array_merge($ids, $ids_tmp);
}
}
}
$retour = array();
if (count($ids) > 0) {
foreach ($ids as $id) {
$retour[] = $id;
}
}
return $retour;
}
/**
* Return the filling percentage of the backend storage
*
* @throws Zend_Cache_Exception
* @return int integer between 0 and 100
*/
public function getPourcentageRemplissage() {
$dossier = dirname($this->options['stockage_chemin']);
$libre = disk_free_space($dossier);
$total = disk_total_space($dossier);
$pourcentage = 0;
if ($total == 0) {
trigger_error("Impossible d'utiliser la fonction disk_total_space", E_USER_WARNING);
} else {
$pourcentage = ($libre >= $total) ? 100 : ((int) (100. * ($total - $libre) / $total));
}
return $pourcentage;
}
/**
* Return an array of metadatas for the given cache id
*
* The array must include these keys :
* - expire : the expire timestamp
* - tags : a string array of tags
* - mtime : timestamp of last modification time
*
* @param string $id cache id
* @return array array of metadatas (false if the cache id is not found)
*/
public function getMetadonnees($id) {
$this->verifierEtCreerStructureBdd();
$tags = array();
$resultat = $this->requeter("SELECT name FROM tag WHERE id = '$id'");
if ($resultat) {
$lignes = @sqlite_fetch_all($resultat, SQLITE_ASSOC);
foreach ($lignes as $ligne) {
$tags[] = $ligne['name'];
}
}
$resultat = $this->requeter("SELECT lastModified, expire FROM cache WHERE id = '$id'");
if ($resultat) {
$ligne = @sqlite_fetch_array($resultat, SQLITE_ASSOC);
$resultat = array(
'tags' => $tags,
'mtime' => $ligne['lastModified'],
'expiration' => $ligne['expire']);
} else {
$resultat = false;
}
return $resultat;
}
/**
* Give (if possible) an extra lifetime to the given cache id
*
* @param string $id cache id
* @param int $extraLifetime
* @return boolean true if ok
*/
public function ajouterSupplementDureeDeVie($id, $supplement_duree_de_vie) {
$this->verifierEtCreerStructureBdd();
$augmentation = false;
$requete = "SELECT expire FROM cache WHERE id = '$id' AND (expire = 0 OR expire > ".time().')';
$resultat = $this->requeter($requete);
if ($resultat) {
$expiration = @sqlite_fetch_single($resultat);
$nouvelle_expiration = $expiration + $supplement_duree_de_vie;
$resultat = $this->requeter('UPDATE cache SET lastModified = '.time().", expire = $nouvelle_expiration WHERE id = '$id'");
$augmentation = ($resultat) ? true : false;
}
return $augmentation;
}
/**
* Return the connection resource
*
* If we are not connected, the connection is made
*
* @throws Zend_Cache_Exception
* @return resource Connection resource
*/
private function getConnexion() {
if (!is_resource($this->bdd)) {
if ($this->options['stockage_chemin'] === null) {
$e = "L'emplacement du chemin vers le fichier de la base de données SQLite n'a pas été défini";
trigger_error($e, E_USER_ERROR);
} else {
$this->bdd = sqlite_open($this->options['stockage_chemin']);
if (!(is_resource($this->bdd))) {
$e = "Impossible d'ouvrir le fichier '".$this->options['stockage_chemin']."' de la base de données SQLite.";
trigger_error($e, E_USER_ERROR);
$this->bdd = null;
}
}
}
return $this->bdd;
}
/**
* Execute une requête SQL sans afficher de messages d'erreur.
*
* @param string $requete requête SQL
* @return mixed|false resultats de la requête
*/
private function requeter($requete) {
$bdd = $this->getConnexion();
//Debug::printr($requete);
$resultat = (is_resource($bdd)) ? @sqlite_query($bdd, $requete, SQLITE_ASSOC, $e_sqlite) : false;
if (is_resource($bdd) && ! $resultat) {
Debug::printr("Erreur SQLITE :\n$e_sqlite\nPour la requête :\n$requete\nRessource : $bdd");
}
return $resultat;
}
/**
* Deal with the automatic vacuum process
*
* @return void
*/
private function defragmenterAutomatiquement() {
if ($this->options['defragmentation_auto'] > 0) {
$rand = rand(1, $this->options['defragmentation_auto']);
if ($rand == 1) {
$this->requeter('VACUUM');
@sqlite_close($this->getConnexion());
}
}
}
/**
* Register a cache id with the given tag
*
* @param string $id Cache id
* @param string $tag Tag
* @return boolean True if no problem
*/
private function enregisterTag($id, $tag) {
$requete_suppression = "DELETE FROM tag WHERE name = '$tag' AND id = '$id'";
$resultat = $this->requeter($requete_suppression);
$requete_insertion = "INSERT INTO tag(name,id) VALUES ('$tag','$id')";
$resultat = $this->requeter($requete_insertion);
if (!$resultat) {
// TODO : ajouter un log -> impossible d'enregistrer le tag=$tag pour le cache id=$id");
Debug::printr("Impossible d'enregistrer le tag=$tag pour le cache id=$id");
}
return ($resultat) ? true : false;
}
/**
* Build the database structure
*
* @return false
*/
private function creerStructure() {
$this->requeter('DROP INDEX IF EXISTS tag_id_index');
$this->requeter('DROP INDEX IF EXISTS tag_name_index');
$this->requeter('DROP INDEX IF EXISTS cache_id_expire_index');
$this->requeter('DROP TABLE IF EXISTS version');
$this->requeter('DROP TABLE IF EXISTS cache');
$this->requeter('DROP TABLE IF EXISTS tag');
$this->requeter('CREATE TABLE version (num INTEGER PRIMARY KEY)');
$this->requeter('CREATE TABLE cache(id TEXT PRIMARY KEY, content BLOB, lastModified INTEGER, expire INTEGER)');
$this->requeter('CREATE TABLE tag (name TEXT, id TEXT)');
$this->requeter('CREATE INDEX tag_id_index ON tag(id)');
$this->requeter('CREATE INDEX tag_name_index ON tag(name)');
$this->requeter('CREATE INDEX cache_id_expire_index ON cache(id, expire)');
$this->requeter('INSERT INTO version (num) VALUES (1)');
}
/**
* Check if the database structure is ok (with the good version)
*
* @return boolean True if ok
*/
private function verifierBddStructureVersion() {
$version_ok = false;
$resultat = $this->requeter('SELECT num FROM version');
if ($resultat) {
$ligne = @sqlite_fetch_array($resultat);
if ($ligne) {
if (((int) $ligne['num']) == 1) {
$version_ok = true;
} else {
// TODO : ajouter un log CacheSqlite::verifierBddStructureVersion() : vielle version de la structure de la base de données de cache détectée => le cache est entrain d'être supprimé
}
}
}
return $version_ok;
}
/**
* Clean some cache records
*
* Available modes are :
* Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used)
* Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used)
* Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags
* ($tags can be an array of strings or a single string)
* Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
* ($tags can be an array of strings or a single string)
* Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
* ($tags can be an array of strings or a single string)
*
* @param string $mode Clean mode
* @param array $tags Array of tags
* @return boolean True if no problem
*/
private function nettoyerSqlite($mode = Cache::NETTOYAGE_MODE_TOUS, $tags = array()) {
$nettoyage_ok = false;
switch ($mode) {
case Cache::NETTOYAGE_MODE_TOUS:
$suppression_cache = $this->requeter('DELETE FROM cache');
$suppression_tag = $this->requeter('DELETE FROM tag');
$nettoyage_ok = $suppression_cache && $suppression_tag;
break;
case Cache::NETTOYAGE_MODE_EXPIRATION:
$mktime = time();
$suppression_tag = $this->requeter("DELETE FROM tag WHERE id IN (SELECT id FROM cache WHERE expire > 0 AND expire <= $mktime)");
$suppression_cache = $this->requeter("DELETE FROM cache WHERE expire > 0 AND expire <= $mktime");
return $suppression_tag && $suppression_cache;
break;
case Cache::NETTOYAGE_MODE_AVEC_LES_TAGS:
$ids = $this->getIdsAvecLesTags($tags);
$resultat = true;
foreach ($ids as $id) {
$resultat = $this->supprimer($id) && $resultat;
}
return $resultat;
break;
case Cache::NETTOYAGE_MODE_SANS_LES_TAGS:
$ids = $this->getIdsSansLesTags($tags);
$resultat = true;
foreach ($ids as $id) {
$resultat = $this->supprimer($id) && $resultat;
}
return $resultat;
break;
case Cache::NETTOYAGE_MODE_AVEC_UN_TAG:
$ids = $this->getIdsAvecUnTag($tags);
$resultat = true;
foreach ($ids as $id) {
$resultat = $this->supprimer($id) && $resultat;
}
return $resultat;
break;
default:
break;
}
return $nettoyage_ok;
}
/**
* Check if the database structure is ok (with the good version), if no : build it
*
* @throws Zend_Cache_Exception
* @return boolean True if ok
*/
private function verifierEtCreerStructureBdd() {
if (! $this->structure_ok) {
if (! $this->verifierBddStructureVersion()) {
$this->creerStructure();
if (! $this->verifierBddStructureVersion()) {
$e = "Impossible de construire la base de données de cache dans ".$this->options['stockage_chemin'];
trigger_error($e, E_USER_WARNING);
$this->structure_ok = false;
}
}
$this->structure_ok = true;
}
return $this->structure_ok;
}
}
?>