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: Cache.php 299 2011-01-18 14:03:46Z jpm $
15: * @link /doc/framework/
16: */
17: class Cache {
18: /** Socke les enregistrements du cache dans des fichiers textes de façon extremement simple. */
19: const STOCKAGE_MODE_SIMPLE = "FichierSimple";
20: /** Socke les enregistrements du cache dans des fichiers textes. */
21: const STOCKAGE_MODE_FICHIER = "Fichier";
22: /** Socke les enregistrements du cache dans une base de données SQLite. */
23: const STOCKAGE_MODE_SQLITE = "Sqlite";
24:
25: /** 'tous' (par défaut) : supprime tous les enregistrements. */
26: const NETTOYAGE_MODE_TOUS = "tous";
27: /** 'expiration' : supprime tous les enregistrements dont la date d'expériration est dépassée. */
28: const NETTOYAGE_MODE_EXPIRATION = "expiration";
29: /** 'avecLesTags' : supprime tous les enregistrements contenant tous les tags indiqués. */
30: const NETTOYAGE_MODE_AVEC_LES_TAGS = "avecLesTags";
31: /** 'sansLesTags' : supprime tous les enregistrements contenant aucun des tags indiqués. */
32: const NETTOYAGE_MODE_SANS_LES_TAGS = "sansLesTags";
33: /** 'avecUnTag' : supprime tous les enregistrements contenant au moins un des tags indiqués. */
34: const NETTOYAGE_MODE_AVEC_UN_TAG = "avecUnTag";
35:
36: /**
37: * Dernier identifiant de cache utilisé.
38: *
39: * @var string $dernier_id
40: */
41: private $dernier_id = null;
42:
43: /**
44: * Les options disponibles pour le cache :
45: * ====> (string) stockage_mode :
46: * Indique le mode de stockage du cache à utiliser parmis :
47: * - Cache::STOCKAGE_MODE_FICHIER : sous forme d'une arborescence de fichiers et dossier
48: * - Cache::STOCKAGE_MODE_SQLITE : sous forme d'une base de données SQLite
49: *
50: * ====> (string) stockage_chemin :
51: * Chemin vers :
52: * - Cache::STOCKAGE_MODE_FICHIER : le dossier devant contenir l'arborescence.
53: * - Cache::STOCKAGE_MODE_SQLITE : le fichier contenant la base SQLite.
54: *
55: * ====> (boolean) controle_ecriture :
56: * - Active / Désactive le controle d'écriture (le cache est lue jute après l'écriture du fichier pour détecter sa corruption)
57: * - Activer le controle d'écriture ralentira légèrement l'écriture du fichier de cache mais pas sa lecture
58: * Le controle d'écriture peut détecter la corruption de fichier mais ce n'est pas un système de controle parfait.
59: *
60: * ====> (boolean) mise_en_cache :
61: * - Active / Désactive la mise en cache
62: * (peut être très utile pour le débogage des scripts utilisant le cache
63: *
64: * =====> (string) cache_id_prefixe :
65: * - préfixe pour les identifiant de cache ( = espace de nom)
66: *
67: * ====> (boolean) serialisation_auto :
68: * - Active / Désactive la sérialisation automatique
69: * - Peut être utilisé pour sauver directement des données qui ne sont pas des chaines (mais c'est plus lent)
70: *
71: * ====> (int) nettoyage_auto :
72: * - Désactive / Régler le processus de nettoyage automatique
73: * - Le processus de nettoyage automatiques détruit les fichier trop vieux (pour la durée de vie donnée)
74: * quand un nouveau fichier de cache est écrit :
75: * 0 => pas de nettoyage automatique
76: * 1 => nettoyage automatique systématique
77: * x (integer) > 1 => nettoyage automatique toutes les 1 fois (au hasard) sur x écriture de fichier de cache
78: *
79: * ====> (int) duree_de_vie :
80: * - Durée de vie du cache (en secondes)
81: * - Si null, le cache est valide indéfiniment.
82: *
83: * @var array $options les options disponibles pour le cache .
84: */
85: protected $options = array(
86: 'stockage_mode' => self::STOCKAGE_MODE_FICHIER,
87: 'stockage_chemin' => null,
88: 'controle_ecriture' => true,
89: 'mise_en_cache' => true,
90: 'cache_id_prefixe' => null,
91: 'serialisation_auto' => false,
92: 'nettoyage_auto' => 10,
93: 'duree_de_vie' => 3600,
94: );
95:
96: protected $stockage = null;
97:
98: public function __construct($options = array(), $options_stockage = array()) {
99: $this->initialiserOptionsParConfig();
100: $this->setOptions($options);
101: if ($this->options['stockage_mode'] == self::STOCKAGE_MODE_FICHIER) {
102: $this->stockage = new CacheFichier($options_stockage, $this);
103: $this->stockage->setEmplacement($this->options['stockage_chemin']);
104: } else if ($this->options['stockage_mode'] == self::STOCKAGE_MODE_SQLITE) {
105: $this->stockage = new CacheSqlite($options_stockage, $this);
106: $this->stockage->setEmplacement($this->options['stockage_chemin']);
107: } else {
108: trigger_error("Ce mode de stockage n'existe pas ou ne supporte pas la création par le constructeur", E_USER_WARNING);
109: }
110: }
111:
112: private function initialiserOptionsParConfig() {
113: while (list($nom, $valeur) = each($this->options)) {
114: if (Config::existe($nom)) {
115: $this->options[$nom] = Config::get($nom);
116: }
117: }
118: }
119:
120: private function setOptions($options) {
121: while (list($nom, $valeur) = each($options)) {
122: if (!is_string($nom)) {
123: trigger_error("Nom d'option incorecte : $nom", E_USER_WARNING);
124: }
125: $nom = strtolower($nom);
126: if (array_key_exists($nom, $this->options)) {
127: $this->options[$nom] = $valeur;
128: }
129: }
130: }
131:
132: /**
133: * Permet de (re-)définir l'emplacement pour le stockage du cache.
134: * En fonction du mode de stockage utilisé , l'emplacement indiqué correspondra au chemin du :
135: * - dossier où stocker les fichiers pour le mode "fichier".
136: * - fichier de la base de données pour le mode "sqlite".
137: * @param string $emplacement chemin vers dossier (Cache::STOCKAGE_MODE_FICHIER) ou fichier base Sqlite (Cache::STOCKAGE_MODE_SQLITE)
138: * @return void
139: */
140: public function setEmplacement($emplacement) {
141: if ($emplacement != null) {
142: $this->executerMethodeStockage('setEmplacement', array($emplacement));
143: } else {
144: trigger_error("L'emplacement ne peut pas être null.", E_USER_WARNING);
145: }
146: }
147:
148: public static function fabriquer($mode, $options = array()) {
149: if ($mode == self::STOCKAGE_MODE_SIMPLE) {
150: return new CacheSimple($options);
151: } else {
152: trigger_error("Le mode '$mode' de stockage n'existe pas ou ne supporte pas la création par fabrique", E_USER_WARNING);
153: }
154: return false;
155: }
156:
157: /**
158: * Teste si un cache est disponible pour l'identifiant donné et (si oui) le retourne (false dans le cas contraire)
159: *
160: * @param string $id Identifiant de cache.
161: * @param boolean $ne_pas_tester_validiter_du_cache Si mis à true, la validité du cache n'est pas testée
162: * @return mixed|false Cached datas
163: */
164: public function charger($id, $ne_pas_tester_validiter_du_cache = false) {
165: $donnees = false;
166: if ($this->options['mise_en_cache'] === true) {
167: $id = $this->prefixerId($id);
168: $this->dernier_id = $id;
169: self::validerIdOuTag($id);
170: $donnees = $this->executerMethodeStockage('charger', array($id, $ne_pas_tester_validiter_du_cache));
171: $donnees = $this->deserialiserAutomatiquement($donnees);
172: }
173: return $donnees;
174: }
175:
176: /**
177: * Test if a cache is available for the given id
178: *
179: * @param string $id Cache id
180: * @return int|false Last modified time of cache entry if it is available, false otherwise
181: */
182: public function tester($id) {
183: $resultat = false;
184: if ($this->options['mise_en_cache'] === true) {
185: $id = $this->prefixerId($id);
186: self::validerIdOuTag($id);
187: $this->dernier_id = $id;
188: $resultat = $this->executerMethodeStockage('tester', array($id));
189: }
190: return $resultat;
191: }
192:
193: /**
194: * Sauvegarde en cache les données passées en paramètre.
195: *
196: * @param mixed $donnees Données à mettre en cache (peut être différent d'une chaine si serialisation_auto vaut true).
197: * @param string $id Identifiant du cache (s'il n'est pas définit, le dernier identifiant sera utilisé).
198: * @param array $tags Mots-clés du cache.
199: * @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)
200: * @return boolean True si aucun problème n'est survenu.
201: */
202: public function sauver($donnees, $id = null, $tags = array(), $duree_de_vie_specifique = false) {
203: $resultat = true;
204: if ($this->options['mise_en_cache'] === true) {
205: $id = ($id === null) ? $this->dernier_id : $this->prefixerId($id);
206:
207: self::validerIdOuTag($id);
208: self::validerTableauDeTags($tags);
209: $donnees = $this->serialiserAutomatiquement($donnees);
210: $this->nettoyerAutomatiquement();
211:
212: $resultat = $this->executerMethodeStockage('sauver', array($donnees, $id, $tags, $duree_de_vie_specifique));
213:
214: if ($resultat == false) {
215: // Le cache étant peut être corrompu, nous le supprimons
216: $this->supprimer($id);
217: } else {
218: $resultat = $this->controlerEcriture($id, $donnees);
219: }
220: }
221: return $resultat;
222: }
223:
224: /**
225: * Supprime un enregistrement en cache.
226: *
227: * @param string $id Identificant du cache à supprimer.
228: * @return boolean True si ok
229: */
230: public function supprimer($id) {
231: $resultat = true;
232: if ($this->options['mise_en_cache'] === true) {
233: $id = $this->prefixerId($id);
234: self::validerIdOuTag($id);
235: $resultat = $this->executerMethodeStockage('supprimer', array($id));
236: }
237: return $resultat;
238: }
239:
240: /**
241: * Nettoyage des enregistrements en cache
242: *
243: * Mode de nettoyage disponibles :
244: * 'tous' (défaut) => supprime tous les enregistrements ($tags n'est pas utilisé)
245: * 'expiration' => supprime tous les enregistrements dont la date d'expériration est dépassée ($tags n'est pas utilisé)
246: * 'avecLesTag' => supprime tous les enregistrements contenant tous les tags indiqués
247: * 'sansLesTag' => supprime tous les enregistrements contenant aucun des tags indiqués
248: * 'avecUnTag' => supprime tous les enregistrements contenant au moins un des tags indiqués
249: *
250: * @param string $mode mode de nettoyage
251: * @param array|string $tags peut être un tableau de chaîne ou une simple chaine.
252: * @return boolean True si ok
253: */
254: public function nettoyer($mode = self::NETTOYAGE_MODE_TOUS, $tags = array()) {
255: $resultat = true;
256: if ($this->options['mise_en_cache'] === true) {
257: if (!in_array($mode, array(Cache::NETTOYAGE_MODE_TOUS,
258: Cache::NETTOYAGE_MODE_EXPIRATION,
259: Cache::NETTOYAGE_MODE_AVEC_LES_TAGS,
260: Cache::NETTOYAGE_MODE_SANS_LES_TAGS,
261: Cache::NETTOYAGE_MODE_AVEC_UN_TAG))) {
262: trigger_error("Le mode de nettoyage du cache indiqué n'est pas valide", E_USER_WARNING);
263: }
264: self::validerTableauDeTags($tags);
265:
266: $resultat = $this->executerMethodeStockage('nettoyer', array($mode, $tags));
267: }
268: return $resultat;
269: }
270:
271: /**
272: * Return an array of stored cache ids
273: *
274: * @return array array of stored cache ids (string)
275: */
276: public function getIds() {
277: $ids = $this->executerMethodeStockage('getIds');
278: $ids = $this->supprimerPrefixe($ids);
279: return $ids;
280: }
281:
282: /**
283: * Return an array of stored tags
284: *
285: * @return array array of stored tags (string)
286: */
287: public function getTags() {
288: return $this->executerMethodeStockage('getTags');
289: }
290:
291: /**
292: * Return an array of stored cache ids which match given tags
293: *
294: * In case of multiple tags, a logical AND is made between tags
295: *
296: * @param array $tags array of tags
297: * @return array array of matching cache ids (string)
298: */
299: public function getIdsAvecLesTags($tags = array()) {
300: $ids = $this->executerMethodeStockage('getIdsAvecLesTags', array($tags));
301: $ids = $this->supprimerPrefixe($ids);
302: return $ids;
303: }
304:
305: /**
306: * Return an array of stored cache ids which don't match given tags
307: *
308: * In case of multiple tags, a logical OR is made between tags
309: *
310: * @param array $tags array of tags
311: * @return array array of not matching cache ids (string)
312: */
313: public function getIdsSansLesTags($tags = array()) {
314: $ids = $this->executerMethodeStockage('getIdsSansLesTags', array($tags));
315: $ids = $this->supprimerPrefixe($ids);
316: return $ids;
317: }
318:
319: /**
320: * Return an array of stored cache ids which match any given tags
321: *
322: * In case of multiple tags, a logical OR is made between tags
323: *
324: * @param array $tags array of tags
325: * @return array array of matching any cache ids (string)
326: */
327: public function getIdsAvecUnTag($tags = array()) {
328: $ids = $this->executerMethodeStockage('getIdsAvecUnTag', array($tags));
329: $ids = $this->supprimerPrefixe($ids);
330: return $ids;
331: }
332:
333: /**
334: * Return the filling percentage of the backend storage
335: *
336: * @return int integer between 0 and 100
337: */
338: public function getPourcentageRemplissage() {
339: return $this->executerMethodeStockage('getPourcentageRemplissage');
340: }
341:
342: /**
343: * Return an array of metadatas for the given cache id
344: *
345: * The array will include these keys :
346: * - expire : the expire timestamp
347: * - tags : a string array of tags
348: * - mtime : timestamp of last modification time
349: *
350: * @param string $id cache id
351: * @return array array of metadatas (false if the cache id is not found)
352: */
353: public function getMetadonnees($id) {
354: $id = $this->prefixerId($id);
355: return $this->executerMethodeStockage('getMetadonnees', array($id));
356: }
357:
358: /**
359: * Give (if possible) an extra lifetime to the given cache id
360: *
361: * @param string $id cache id
362: * @param int $extraLifetime
363: * @return boolean true if ok
364: */
365: public function ajouterSupplementDureeDeVie($id, $supplement_duree_de_vie) {
366: $id = $this->prefixerId($id);
367: return $this->executerMethodeStockage('ajouterSupplementDureeDeVie', array($id, $supplement_duree_de_vie));
368: }
369:
370:
371: /**
372: * Fabrique et retourne l'identifiant du cache avec son préfixe.
373: *
374: * 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.
375: *
376: * @param string $id Identifiant du cache.
377: * @return string Identifiant du cache avec ou sans préfixe.
378: */
379: private function prefixerId($id) {
380: $nouvel_id = $id;
381: if (($id !== null) && isset($this->options['cache_id_prefixe'])) {
382: $nouvel_id = $this->options['cache_id_prefixe'].$id;
383: }
384: return $nouvel_id;
385: }
386:
387: private function executerMethodeStockage($methode, $params = null) {
388: if (method_exists($this->stockage, $methode)) {
389: if ($params == null) {
390: $resultat = call_user_func(array($this->stockage, $methode));
391: } else {
392: $resultat = call_user_func_array(array($this->stockage, $methode), $params);
393: }
394: } else {
395: $resultat = false;
396: trigger_error("La méthode '$methode' n'existe pas dans la classe '".get_class($this)."'.", E_USER_WARNING);
397: }
398: return $resultat;
399: }
400:
401: private function supprimerPrefixe($ids) {
402: // Il est nécessaire de retirer les cache_id_prefixe des ids (voir #ZF-6178, #ZF-7600)
403: if (isset($this->options['cache_id_prefixe']) && $this->options['cache_id_prefixe'] !== '') {
404: $prefixe =& $this->options['cache_id_prefixe'];
405: $prefixe_longueur = strlen($prefixe);
406: foreach ($ids as &$id) {
407: if (strpos($id, $prefixe) === 0) {
408: $id = substr($id, $prefixe_longueur);
409: }
410: }
411: }
412: return $ids;
413: }
414:
415: private function controlerEcriture($id, $donnees_avant_ecriture) {
416: $resultat = true;
417: if ($this->options['controle_ecriture']) {
418: $donnees_apres_ecriture = $this->executerMethodeStockage('charger', array($id, true));
419: if ($donnees_avant_ecriture != $donnees_apres_ecriture) {
420: $this->executerMethodeStockage('supprimer', array($id));
421: $resultat = false;
422: }
423: }
424: return $resultat;
425: }
426:
427: private function deserialiserAutomatiquement($donnees) {
428: if ($donnees !== false && $this->options['serialisation_auto']) {
429: // we need to unserialize before sending the result
430: $donnees = unserialize($donnees);
431: }
432: return $donnees;
433: }
434:
435: private function serialiserAutomatiquement($donnees) {
436: if ($this->options['serialisation_auto']) {
437: // we need to serialize datas before storing them
438: $donnees = serialize($donnees);
439: } else {
440: if (!is_string($donnees)) {
441: 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);
442: }
443: }
444: return $donnees;
445: }
446:
447: private function nettoyerAutomatiquement() {
448: if ($this->options['nettoyage_auto'] > 0) {
449: $rand = rand(1, $this->options['nettoyage_auto']);
450: if ($rand == 1) {
451: $this->nettoyer(self::NETTOYAGE_MODE_EXPIRATION);
452: }
453: }
454: }
455:
456: /**
457: * Valide un identifiant de cache ou un tag (securité, nom de fichiers fiables, préfixes réservés...)
458: *
459: * @param string $chaine Identificant de cache ou tag
460: * @return void
461: */
462: protected static function validerIdOuTag($chaine) {
463: if (!is_string($chaine)) {
464: trigger_error('Id ou tag invalide : doit être une chaîne de caractères', E_USER_ERROR);
465: }
466: if (substr($chaine, 0, 9) == 'internal-') {
467: trigger_error('"internal-*" identifiants ou tags sont réservés', E_USER_WARNING);
468: }
469: if (!preg_match('~^[a-zA-Z0-9_]+$~D', $chaine)) {
470: trigger_error("Id ou tag invalide '$chaine' : doit contenir seulement [a-zA-Z0-9_]", E_USER_WARNING);
471: }
472: }
473:
474: /**
475: * Valide un tableau de tags (securité, nom de fichiers fiables, préfixes réservés...)
476: *
477: * @param array $tags tableau de tags
478: * @return void
479: */
480: protected static function validerTableauDeTags($tags) {
481: if (!is_array($tags)) {
482: trigger_error("Tableau de tags invalide : doit être un tableau 'array'", E_USER_WARNING);
483: }
484: foreach ($tags as $tag) {
485: self::validerIdOuTag($tag);
486: }
487: reset($tags);
488: }
489:
490: /**
491: * Calcule et retourne le timestamp d'expiration
492: *
493: * @return int timestamp d'expiration (unix timestamp)
494: */
495: public function getTimestampExpiration($duree_de_vie) {
496: if ($duree_de_vie === false) {
497: if (isset($this->options['duree_de_vie']) && is_int($this->options['duree_de_vie'])) {
498: $duree_de_vie = (int) $this->options['duree_de_vie'];
499: } else {
500: $duree_de_vie = 3600;
501: }
502: }
503: $timestamp = ($duree_de_vie === null) ? 9999999999 : (time() + $duree_de_vie);
504: return $timestamp;
505: }
506:
507: }