Subversion Repositories Applications.framework

Rev

Rev 248 | Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
246 jpm 1
<?php
2
// declare(encoding='UTF-8');
3
/**
4
 * Classe Cache permettant de mettre en cache des données.
5
 * Basée sur les principes de Zend_Cache (Copyright (c) 2005-2010, Zend Technologies USA, Inc. All rights reserved.)
6
 *
7
 * @category	php 5.2
8
 * @package	Framework
9
 * @author		Jean-Pascal MILCENT <jpm@tela-botanica.org>
10
 * @copyright	Copyright (c) 2010, Tela Botanica (accueil@tela-botanica.org)
11
 * @license	http://framework.zend.com/license/new-bsd Licence New BSD
12
 * @license	http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt Licence CECILL
13
 * @license	http://www.gnu.org/licenses/gpl.html Licence GNU-GPL
14
 * @version	$Id$
15
 * @link		/doc/framework/
16
 */
17
class Cache {
18
	/** Socke les enregistrements du cache dans des fichiers textes. */
19
	const STOCKAGE_MODE_FICHIER = "Fichier";
20
	/** Socke les enregistrements du cache dans une base de données SQLite. */
21
	const STOCKAGE_MODE_SQLITE = "Sqlite";
22
 
23
	/** 'tous' (par défaut) : supprime tous les enregistrements. */
24
	const NETTOYAGE_MODE_TOUS = "tous";
25
	/** 'expiration' : supprime tous les enregistrements dont la date d'expériration est dépassée. */
26
	const NETTOYAGE_MODE_EXPIRATION = "expiration";
27
	/** 'avecLesTags' : supprime tous les enregistrements contenant tous les tags indiqués. */
28
	const NETTOYAGE_MODE_AVEC_LES_TAGS = "avecLesTags";
29
	/** 'sansLesTags' : supprime tous les enregistrements contenant aucun des tags indiqués. */
30
	const NETTOYAGE_MODE_SANS_LES_TAGS = "sansLesTags";
31
	/** 'avecUnTag' : supprime tous les enregistrements contenant au moins un des tags indiqués. */
32
	const NETTOYAGE_MODE_AVEC_UN_TAG = "avecUnTag";
33
 
34
	/**
35
	 * Dernier identifiant de cache utilisé.
36
	 *
37
	 * @var string $dernier_id
38
	 */
39
	private $dernier_id = null;
40
 
41
	/**
42
	 * Les options disponibles pour le cache :
43
	 *
44
	 * ====> (boolean) controle_ecriture : [write_control]
45
	 * - Enable / disable write control (the cache is read just after writing to detect corrupt entries)
46
	 * - Enable write control will lightly slow the cache writing but not the cache reading
47
	 * Write control can detect some corrupt cache files but maybe it's not a perfect control
48
	 *
49
	 * ====> (boolean) mise_en_cache : [caching]
50
	 * - Enable / disable caching
51
	 * (can be very useful for the debug of cached scripts)
52
	 *
53
	 * =====> (string) cache_id_prefixe : [cache_id_prefix]
54
	 * - prefix for cache ids (namespace)
55
	 *
56
	 * ====> (boolean) serialisation_auto : [automatic_serialization]
57
	 * - Enable / disable automatic serialization
58
	 * - It can be used to save directly datas which aren't strings (but it's slower)
59
	 *
60
	 * ====> (int) nettoyage_auto : [automatic_cleaning_factor]
61
	 * - Disable / Tune the automatic cleaning process
62
	 * - The automatic cleaning process destroy too old (for the given life time)
63
	 *   cache files when a new cache file is written :
64
	 *	 0			   => no automatic cache cleaning
65
	 *	 1			   => systematic cache cleaning
66
	 *	 x (integer) > 1 => automatic cleaning randomly 1 times on x cache write
67
	 *
68
	 * ====> (int) duree_de_vie : [lifetime]
69
	 * - Cache lifetime (in seconds)
70
	 * - If null, the cache is valid forever.
71
	 *
72
	 * @var array $options les options disponibles pour le cache .
73
	 */
74
	protected $options = array(
75
		'stockage_mode'				 => self::STOCKAGE_MODE_FICHIER,
76
		'stockage_chemin'				 => null,
77
		'controle_ecriture'			 => true,
78
		'mise_en_cache'		  		 => true,
79
		'cache_id_prefixe'		  		 => null,
80
		'serialisation_auto'		  	 => false,
81
		'nettoyage_auto'				 => 10,
82
		'duree_de_vie'			 		 => 3600,
83
	);
84
 
85
	public function __construct($options) {
86
 
87
	}
88
 
89
	/**
90
	 * Fabrique et retourne l'identifiant du cache.
91
	 *
92
	 * 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.
93
	 *
94
	 * @param  string $id Identifiant du cache.
95
	 * @return string Identifiant du cache avec ou sans préfixe.
96
	 */
97
	protected function getId($id) {
98
		$nouvel_id = $id;
99
		if (($id !== null) && isset($this->options['cache_id_prefixe'])) {
100
			$nouvel_id = $this->options['cache_id_prefixe'] . $id;
101
		}
102
		return $nouvel_id;
103
	}
104
 
105
	/**
106
	 * Permet de (re-)définir l'emplacement pour le stockage du cache.
107
	 * En fonction du mode de stockage utilisé , l'emplacement indiqué correspondra au chemin du :
108
	 *  - dossier où stocker les fichiers pour le mode "fichier".
109
	 *  - fichier de la base de données pour le mode "sqlite".
110
	 * @param string $emplacement chemin vers dossier (Cache::STOCKAGE_MODE_FICHIER) ou fichier base Sqlite (Cache::STOCKAGE_MODE_SQLITE)
111
	 * @return void
112
	 */
113
	public function setEmplacement($emplacement) {
114
		if ($emplacement != null) {
115
			$this->executerMethodeStockage('setEmplacement', array($emplacement));
116
		} else {
117
            trigger_error("L'emplacement ne peut pas être null.", E_USER_WARNING);
118
		}
119
	}
120
 
121
	private function setEmplacementFichier($emplacement) {
122
		if (!is_dir($emplacement)) {
123
            trigger_error("L'emplacement doit être un dossier.", E_USER_WARNING);
124
        }
125
        if (!is_writable($emplacement)) {
126
            trigger_error("Le dossier de stockage du cache n'est pas accessible en écriture", E_USER_WARNING);
127
        }
128
        $emplacement = rtrim(realpath($emplacement), '\\/').DS;
129
        $this->options['stockage_chemin'] = $emplacement;
130
	}
131
 
132
	private function setEmplacementSqlite($emplacement) {
133
	 	if (!extension_loaded('sqlite')) {
134
            trigger_error("Impossible d'utiliser le mode de sotckage SQLite car l'extenssion 'sqlite' n'est pas chargé dans ".
135
            	"l'environnement PHP courrant.\n Le mode de stockage par fichier sera utilisé à la place.");
136
            $emplacement = rtrim(realpath($emplacement), '\\/').DS;
137
            $this->options['stockage_mode'] = self::STOCKAGE_MODE_FICHIER;
138
        }
139
        $this->options['stockage_chemin'] = $emplacement;
140
	}
141
 
142
	private function executerMethodeStockage($prefixe, $params) {
143
		$methode = 'sauver'.$this->options['mode_stockage'];
144
		if (method_exists($this, $methode)) {
145
			$resultat = call_user_func_array(array($this, $methode), $params);
146
		} else {
147
			$resultat = false;
148
			trigger_error("La méthode '$methode' n'existe pas dans la classe '".get_class($this)."'.", E_USER_WARNING);
149
		}
150
		return $resultat;
151
	}
152
 
153
	/**
154
	 * Teste si un cache est disponible pour l'identifiant donné et (si oui) le retourne (false dans le cas contraire)
155
	 *
156
	 * @param  string  $id Identifiant de cache.
157
	 * @param  boolean $ne_pas_tester_validiter_du_cache Si mis à true, la validité du cache n'est pas testée
158
	 * @return mixed|false Cached datas
159
	 */
160
	public function charger($id, $ne_pas_tester_validiter_du_cache = false) {
161
		$donnees = false;
162
		if ($this->options['mise_en_cache'] === true) {
163
			$id = $this->getId($id);
164
			$this->dernier_id = $id;
165
			self::validerIdOuTag($id);
166
			$donnees = $this->executerMethodeStockage('charger', array($id, $ne_pas_tester_validiter_du_cache));
167
			$donnees = $this->deserialiserAutomatiquement($donnees);
168
		}
169
		return $donnees;
170
	}
171
 
172
	/**
173
	 * Sauvegarde en cache les données passées en paramètre.
174
	 *
175
	 * @param  mixed $donnees Données à mettre en cache (peut être différent d'une chaine si serialisation_auto vaut true).
176
	 * @param  string $id	 Identifiant du cache (s'il n'est pas définit, le dernier identifiant sera utilisé).
177
	 * @param  array $tags Mots-clés du cache.
178
	 * @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)
179
	 * @return boolean True si aucun problème n'est survenu.
180
	 */
181
	public function sauver($donnees, $id = null, $tags = array(), $duree_de_vie_specifique = false) {
182
		$resultat = true;
183
		if ($this->options['mise_en_cache'] === true) {
184
			$id = ($id === null) ? $this->dernier_id : $this->getId($id);
185
 
186
			self::validerIdOuTag($id);
187
			self::validerTableauDeTags($tags);
188
			$donnees = $this->serialiserAutomatiquement($donnees);
189
			$this->nettoyerAutomatiquement();
190
 
191
			$resultat = $this->executerMethodeStockage('sauver', array($donnees, $id, $tags, $duree_de_vie_specifique));
192
 
193
			if ($resultat == false) {
194
				// Le cache étant peut être corrompu, nous le supprimons
195
				$this->supprimer($id);
196
			} else {
197
				$resultat = $this->controlerEcriture($id, $donnees);
198
			}
199
		}
200
		return $resultat;
201
	}
202
 
203
	/**
204
	 * Nettoyage des enregistrements en cache
205
	 *
206
	 * Mode de nettoyage disponibles :
207
	 * 'tous' (défaut)	=> supprime tous les enregistrements ($tags n'est pas utilisé)
208
	 * 'expiration'		=> supprime tous les enregistrements dont la date d'expériration est dépassée ($tags n'est pas utilisé)
209
	 * 'avecLesTag'		=> supprime tous les enregistrements contenant tous les tags indiqués
210
	 * 'sansLesTag'		=> supprime tous les enregistrements contenant aucun des tags indiqués
211
	 * 'avecUnTag'			=> supprime tous les enregistrements contenant au moins un des tags indiqués
212
	 *
213
	 * @param string $mode mode de nettoyage
214
	 * @param array|string $tags peut être un tableau de chaîne ou une simple chaine.
215
	 * @return boolean True si ok
216
	 */
217
	public function nettoyer($mode = self::NETTOYAGE_MODE_TOUS, $tags = array()) {
218
		$resultat = true;
219
		if ($this->options['mise_en_cache'] === true) {
220
			if (!in_array($mode, array(Cache::NETTOYAGE_MODE_TOUS,
221
				Cache::NETTOYAGE_MODE_EXPIRATION,
222
				Cache::NETTOYAGE_MODE_AVEC_LES_TAGS,
223
				Cache::NETTOYAGE_MODE_SANS_LES_TAGS,
224
				Cache::NETTOYAGE_MODE_AVEC_UN_TAG))) {
225
				trigger_error("Le mode de nettoyage du cache indiqué n'est pas valide", E_USER_WARNING);
226
			}
227
			self::validerTableauDeTags($tags);
228
 
229
			$resultat = $this->executerMethodeStockage('nettoyer', array($mode, $tags));
230
		}
231
		return $resultat;
232
	}
233
 
234
    /**
235
     * Supprime un enregistrement en cache.
236
     *
237
     * @param  string $id Identificant du cache à supprimer.
238
     * @return boolean True si ok
239
     */
240
    public function supprimer($id) {
241
    	$resultat = true;
242
		if ($this->options['mise_en_cache'] === true) {
243
	        $id = $this->getId($id);
244
	        self::validerIdOuTag($id);
245
	       $resultat = $this->executerMethodeStockage('supprimer', array($id));
246
		}
247
		return $resultat;
248
    }
249
 
250
	private function controlerEcriture($id, $donnees_avant_ecriture) {
251
		$resultat = true;
252
		if ($this->options['controle_ecriture']) {
253
			$donnees_apres_ecriture = $this->executerMethodeStockage('charger', array($id, true));
254
			if ($donnees_avant_ecriture != $donnees_apres_ecriture) {
255
				$this->executerMethodeStockage('supprimer', array($id));
256
				$resultat = false;
257
			}
258
		}
259
		return $resultat;
260
	}
261
 
262
	private function deserialiserAutomatiquement($donnees) {
263
		if ($donnees !== false && $this->options['serialisation_auto']) {
264
				// we need to unserialize before sending the result
265
				$donnees = unserialize($donnees);
266
		}
267
		return $donnees;
268
	}
269
 
270
	private function serialiserAutomatiquement($donnees) {
271
		if ($this->options['serialisation_auto']) {
272
			// we need to serialize datas before storing them
273
			$donnees = serialize($donnees);
274
		} else {
275
			if (!is_string($donnees)) {
276
				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);
277
			}
278
		}
279
		return $donnees;
280
	}
281
 
282
	private function nettoyerAutomatiquement() {
283
		if ($this->options['nettoyage_auto'] > 0) {
284
			$rand = rand(1, $this->options['nettoyage_auto']);
285
			if ($rand == 1) {
286
				$this->clean(self::NETTOYAGE_MODE_EXPIRER);
287
			}
288
		}
289
	}
290
 
291
	/**
292
	 * Valide un identifiant de cache ou un tag (securité, nom de fichiers fiables, préfixes réservés...)
293
	 *
294
	 * @param  string $chaine Identificant de cache ou tag
295
	 * @return void
296
	 */
297
	protected static function validerIdOuTag($chaine) {
298
		if (!is_string($chaine)) {
299
			trigger_error('Id ou tag invalide : doit être une chaîne de caractères', E_USER_WARNING);
300
		}
301
		if (substr($chaine, 0, 9) == 'internal-') {
302
			trigger_error('"internal-*" identifiants ou tags sont réservés', E_USER_WARNING);
303
		}
304
		if (!preg_match('~^[a-zA-Z0-9_]+$~D', $chaine)) {
305
			trigger_error("Id ou tag invalide '$chaine' : doit contenir seulement [a-zA-Z0-9_]", E_USER_WARNING);
306
		}
307
	}
308
 
309
	/**
310
	 * Valide un tableau de tags  (securité, nom de fichiers fiables, préfixes réservés...)
311
	 *
312
	 * @param  array $tags tableau de tags
313
	 * @return void
314
	 */
315
	protected static function validerTableauDeTags($tags) {
316
		if (!is_array($tags)) {
317
			trigger_error("Tableau de tags invalide : doit être un tableau 'array'", E_USER_WARNING);
318
		}
319
		foreach ($tags as $tag) {
320
			self::validerIdOuTag($tag);
321
		}
322
		reset($tags);
323
	}
324
 
325
 
326
}