Subversion Repositories Applications.framework

Compare Revisions

Ignore whitespace Rev 245 → Rev 246

/trunk/framework/Cache.php
New file
0,0 → 1,326
<?php
// declare(encoding='UTF-8');
/**
* Classe Cache permettant de mettre en cache des données.
* Basée sur les principes de Zend_Cache (Copyright (c) 2005-2010, Zend Technologies USA, Inc. All rights reserved.)
*
* @category php 5.2
* @package Framework
* @author Jean-Pascal MILCENT <jpm@tela-botanica.org>
* @copyright Copyright (c) 2010, Tela Botanica (accueil@tela-botanica.org)
* @license http://framework.zend.com/license/new-bsd Licence New BSD
* @license http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt Licence CECILL
* @license http://www.gnu.org/licenses/gpl.html Licence GNU-GPL
* @version $Id$
* @link /doc/framework/
*/
class Cache {
/** Socke les enregistrements du cache dans des fichiers textes. */
const STOCKAGE_MODE_FICHIER = "Fichier";
/** Socke les enregistrements du cache dans une base de données SQLite. */
const STOCKAGE_MODE_SQLITE = "Sqlite";
/** 'tous' (par défaut) : supprime tous les enregistrements. */
const NETTOYAGE_MODE_TOUS = "tous";
/** 'expiration' : supprime tous les enregistrements dont la date d'expériration est dépassée. */
const NETTOYAGE_MODE_EXPIRATION = "expiration";
/** 'avecLesTags' : supprime tous les enregistrements contenant tous les tags indiqués. */
const NETTOYAGE_MODE_AVEC_LES_TAGS = "avecLesTags";
/** 'sansLesTags' : supprime tous les enregistrements contenant aucun des tags indiqués. */
const NETTOYAGE_MODE_SANS_LES_TAGS = "sansLesTags";
/** 'avecUnTag' : supprime tous les enregistrements contenant au moins un des tags indiqués. */
const NETTOYAGE_MODE_AVEC_UN_TAG = "avecUnTag";
/**
* Dernier identifiant de cache utilisé.
*
* @var string $dernier_id
*/
private $dernier_id = null;
/**
* Les options disponibles pour le cache :
*
* ====> (boolean) controle_ecriture : [write_control]
* - Enable / disable write control (the cache is read just after writing to detect corrupt entries)
* - Enable write control will lightly slow the cache writing but not the cache reading
* Write control can detect some corrupt cache files but maybe it's not a perfect control
*
* ====> (boolean) mise_en_cache : [caching]
* - Enable / disable caching
* (can be very useful for the debug of cached scripts)
*
* =====> (string) cache_id_prefixe : [cache_id_prefix]
* - prefix for cache ids (namespace)
*
* ====> (boolean) serialisation_auto : [automatic_serialization]
* - Enable / disable automatic serialization
* - It can be used to save directly datas which aren't strings (but it's slower)
*
* ====> (int) nettoyage_auto : [automatic_cleaning_factor]
* - 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
*
* ====> (int) duree_de_vie : [lifetime]
* - Cache lifetime (in seconds)
* - If null, the cache is valid forever.
*
* @var array $options les options disponibles pour le cache .
*/
protected $options = array(
'stockage_mode' => self::STOCKAGE_MODE_FICHIER,
'stockage_chemin' => null,
'controle_ecriture' => true,
'mise_en_cache' => true,
'cache_id_prefixe' => null,
'serialisation_auto' => false,
'nettoyage_auto' => 10,
'duree_de_vie' => 3600,
);
public function __construct($options) {
}
/**
* Fabrique et retourne l'identifiant du cache.
*
* Vérifie l'option 'cache_id_prefixe' et retourne le nouvel id avec préfixe ou simplement l'id lui même si elle vaut null.
*
* @param string $id Identifiant du cache.
* @return string Identifiant du cache avec ou sans préfixe.
*/
protected function getId($id) {
$nouvel_id = $id;
if (($id !== null) && isset($this->options['cache_id_prefixe'])) {
$nouvel_id = $this->options['cache_id_prefixe'] . $id;
}
return $nouvel_id;
}
/**
* Permet de (re-)définir l'emplacement pour le stockage du cache.
* En fonction du mode de stockage utilisé , l'emplacement indiqué correspondra au chemin du :
* - dossier où stocker les fichiers pour le mode "fichier".
* - fichier de la base de données pour le mode "sqlite".
* @param string $emplacement chemin vers dossier (Cache::STOCKAGE_MODE_FICHIER) ou fichier base Sqlite (Cache::STOCKAGE_MODE_SQLITE)
* @return void
*/
public function setEmplacement($emplacement) {
if ($emplacement != null) {
$this->executerMethodeStockage('setEmplacement', array($emplacement));
} else {
trigger_error("L'emplacement ne peut pas être null.", E_USER_WARNING);
}
}
private function setEmplacementFichier($emplacement) {
if (!is_dir($emplacement)) {
trigger_error("L'emplacement doit être un dossier.", E_USER_WARNING);
}
if (!is_writable($emplacement)) {
trigger_error("Le dossier de stockage du cache n'est pas accessible en écriture", E_USER_WARNING);
}
$emplacement = rtrim(realpath($emplacement), '\\/').DS;
$this->options['stockage_chemin'] = $emplacement;
}
private function setEmplacementSqlite($emplacement) {
if (!extension_loaded('sqlite')) {
trigger_error("Impossible d'utiliser le mode de sotckage SQLite car l'extenssion 'sqlite' n'est pas chargé dans ".
"l'environnement PHP courrant.\n Le mode de stockage par fichier sera utilisé à la place.");
$emplacement = rtrim(realpath($emplacement), '\\/').DS;
$this->options['stockage_mode'] = self::STOCKAGE_MODE_FICHIER;
}
$this->options['stockage_chemin'] = $emplacement;
}
private function executerMethodeStockage($prefixe, $params) {
$methode = 'sauver'.$this->options['mode_stockage'];
if (method_exists($this, $methode)) {
$resultat = call_user_func_array(array($this, $methode), $params);
} else {
$resultat = false;
trigger_error("La méthode '$methode' n'existe pas dans la classe '".get_class($this)."'.", E_USER_WARNING);
}
return $resultat;
}
/**
* Teste si un cache est disponible pour l'identifiant donné et (si oui) le retourne (false dans le cas contraire)
*
* @param string $id Identifiant de cache.
* @param boolean $ne_pas_tester_validiter_du_cache Si mis à true, la validité du cache n'est pas testée
* @return mixed|false Cached datas
*/
public function charger($id, $ne_pas_tester_validiter_du_cache = false) {
$donnees = false;
if ($this->options['mise_en_cache'] === true) {
$id = $this->getId($id);
$this->dernier_id = $id;
self::validerIdOuTag($id);
$donnees = $this->executerMethodeStockage('charger', array($id, $ne_pas_tester_validiter_du_cache));
$donnees = $this->deserialiserAutomatiquement($donnees);
}
return $donnees;
}
/**
* Sauvegarde en cache les données passées en paramètre.
*
* @param mixed $donnees Données à mettre en cache (peut être différent d'une chaine si serialisation_auto vaut true).
* @param string $id Identifiant du cache (s'il n'est pas définit, le dernier identifiant sera utilisé).
* @param array $tags Mots-clés du cache.
* @param int $duree_de_vie_specifique Si != false, indique une durée de vie spécifique pour cet enregistrement en cache (null => durée de vie infinie)
* @return boolean True si aucun problème n'est survenu.
*/
public function sauver($donnees, $id = null, $tags = array(), $duree_de_vie_specifique = false) {
$resultat = true;
if ($this->options['mise_en_cache'] === true) {
$id = ($id === null) ? $this->dernier_id : $this->getId($id);
self::validerIdOuTag($id);
self::validerTableauDeTags($tags);
$donnees = $this->serialiserAutomatiquement($donnees);
$this->nettoyerAutomatiquement();
$resultat = $this->executerMethodeStockage('sauver', array($donnees, $id, $tags, $duree_de_vie_specifique));
if ($resultat == false) {
// Le cache étant peut être corrompu, nous le supprimons
$this->supprimer($id);
} else {
$resultat = $this->controlerEcriture($id, $donnees);
}
}
return $resultat;
}
/**
* Nettoyage des enregistrements en cache
*
* Mode de nettoyage disponibles :
* 'tous' (défaut) => supprime tous les enregistrements ($tags n'est pas utilisé)
* 'expiration' => supprime tous les enregistrements dont la date d'expériration est dépassée ($tags n'est pas utilisé)
* 'avecLesTag' => supprime tous les enregistrements contenant tous les tags indiqués
* 'sansLesTag' => supprime tous les enregistrements contenant aucun des tags indiqués
* 'avecUnTag' => supprime tous les enregistrements contenant au moins un des tags indiqués
*
* @param string $mode mode de nettoyage
* @param array|string $tags peut être un tableau de chaîne ou une simple chaine.
* @return boolean True si ok
*/
public function nettoyer($mode = self::NETTOYAGE_MODE_TOUS, $tags = array()) {
$resultat = true;
if ($this->options['mise_en_cache'] === true) {
if (!in_array($mode, array(Cache::NETTOYAGE_MODE_TOUS,
Cache::NETTOYAGE_MODE_EXPIRATION,
Cache::NETTOYAGE_MODE_AVEC_LES_TAGS,
Cache::NETTOYAGE_MODE_SANS_LES_TAGS,
Cache::NETTOYAGE_MODE_AVEC_UN_TAG))) {
trigger_error("Le mode de nettoyage du cache indiqué n'est pas valide", E_USER_WARNING);
}
self::validerTableauDeTags($tags);
$resultat = $this->executerMethodeStockage('nettoyer', array($mode, $tags));
}
return $resultat;
}
/**
* Supprime un enregistrement en cache.
*
* @param string $id Identificant du cache à supprimer.
* @return boolean True si ok
*/
public function supprimer($id) {
$resultat = true;
if ($this->options['mise_en_cache'] === true) {
$id = $this->getId($id);
self::validerIdOuTag($id);
$resultat = $this->executerMethodeStockage('supprimer', array($id));
}
return $resultat;
}
private function controlerEcriture($id, $donnees_avant_ecriture) {
$resultat = true;
if ($this->options['controle_ecriture']) {
$donnees_apres_ecriture = $this->executerMethodeStockage('charger', array($id, true));
if ($donnees_avant_ecriture != $donnees_apres_ecriture) {
$this->executerMethodeStockage('supprimer', array($id));
$resultat = false;
}
}
return $resultat;
}
private function deserialiserAutomatiquement($donnees) {
if ($donnees !== false && $this->options['serialisation_auto']) {
// we need to unserialize before sending the result
$donnees = unserialize($donnees);
}
return $donnees;
}
private function serialiserAutomatiquement($donnees) {
if ($this->options['serialisation_auto']) {
// we need to serialize datas before storing them
$donnees = serialize($donnees);
} else {
if (!is_string($donnees)) {
trigger_error("Les données doivent être une chaîne de caractères ou vous devez activez l'option serialisation_auto = true", E_USER_WARNING);
}
}
return $donnees;
}
private function nettoyerAutomatiquement() {
if ($this->options['nettoyage_auto'] > 0) {
$rand = rand(1, $this->options['nettoyage_auto']);
if ($rand == 1) {
$this->clean(self::NETTOYAGE_MODE_EXPIRER);
}
}
}
/**
* Valide un identifiant de cache ou un tag (securité, nom de fichiers fiables, préfixes réservés...)
*
* @param string $chaine Identificant de cache ou tag
* @return void
*/
protected static function validerIdOuTag($chaine) {
if (!is_string($chaine)) {
trigger_error('Id ou tag invalide : doit être une chaîne de caractères', E_USER_WARNING);
}
if (substr($chaine, 0, 9) == 'internal-') {
trigger_error('"internal-*" identifiants ou tags sont réservés', E_USER_WARNING);
}
if (!preg_match('~^[a-zA-Z0-9_]+$~D', $chaine)) {
trigger_error("Id ou tag invalide '$chaine' : doit contenir seulement [a-zA-Z0-9_]", E_USER_WARNING);
}
}
 
/**
* Valide un tableau de tags (securité, nom de fichiers fiables, préfixes réservés...)
*
* @param array $tags tableau de tags
* @return void
*/
protected static function validerTableauDeTags($tags) {
if (!is_array($tags)) {
trigger_error("Tableau de tags invalide : doit être un tableau 'array'", E_USER_WARNING);
}
foreach ($tags as $tag) {
self::validerIdOuTag($tag);
}
reset($tags);
}
}