Subversion Repositories Applications.framework

Rev

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

Rev Author Line No. Line
269 jpm 1
<?php
2
class CacheSqlite {
3
	/**
4
	 * Available options
5
	 *
6
	 * =====> (string) cache_db_complete_path :
7
	 * - the complete path (filename included) of the SQLITE database
8
	 *
9
	 * ====> (int) automatic_vacuum_factor :
10
	 * - Disable / Tune the automatic vacuum process
11
	 * - The automatic vacuum process defragment the database file (and make it smaller)
12
	 *   when a clean() or delete() is called
13
	 *	 0			   => no automatic vacuum
14
	 *	 1			   => systematic vacuum (when delete() or clean() methods are called)
15
	 *	 x (integer) > 1 => automatic vacuum randomly 1 times on x clean() or delete()
16
	 *
17
	 * @var array Available options
18
	 */
19
	protected $options = array(
20
		'stockage_chemin' => null,
21
		'defragmentation_auto' => 10
22
	);
23
 
24
	/**
25
	 * DB ressource
26
	 *
27
	 * @var mixed $db
28
	 */
29
	private $bdd = null;
30
 
31
	/**
32
	 * Boolean to store if the structure has benn checked or not
33
	 *
34
	 * @var boolean $structure_ok
35
	 */
36
	private $structure_ok = false;
37
 
38
	private $Cache = null;
39
 
40
	/**
41
	 * Constructor
42
	 *
43
	 * @param  array $options Associative array of options
44
	 * @throws Zend_cache_Exception
45
	 * @return void
46
	 */
47
	public function __construct(array $options = array(), Cache $cache) {
48
		$this->Cache = $cache;
49
		if (extension_loaded('sqlite')) {
50
			$this->setOptions($options);
51
		} else {
52
			$e = "Impossible d'utiliser le cache SQLITE car l'extenssion 'sqlite' n'est pas chargée dans l'environnement PHP courrant.";
53
			trigger_error($e, E_USER_ERROR);
54
		}
55
	}
56
 
57
	/**
58
	 * Destructor
59
	 *
60
	 * @return void
61
	 */
62
	public function __destruct() {
63
		@sqlite_close($this->getConnexion());
64
	}
65
 
66
	private function setOptions($options) {
67
		while (list($nom, $valeur) = each($options)) {
68
			if (!is_string($nom)) {
69
				trigger_error("Nom d'option incorecte : $nom", E_USER_WARNING);
70
			}
71
			$nom = strtolower($nom);
72
			if (array_key_exists($nom, $this->options)) {
73
				$this->options[$nom] = $valeur;
74
			}
75
		}
76
	}
77
 
78
	public function setEmplacement($emplacement) {
79
	 	if (extension_loaded('sqlite')) {
80
			$this->options['stockage_chemin'] = $emplacement;
81
		} else {
82
			trigger_error("Impossible d'utiliser le mode de sotckage SQLite car l'extenssion 'sqlite' n'est pas chargé dans ".
83
				"l'environnement PHP courrant.", E_USER_ERROR);
84
		}
85
	}
86
 
87
	/**
88
	 * Test if a cache is available for the given id and (if yes) return it (false else)
89
	 *
90
	 * @param  string  $id					 Cache id
91
	 * @param  boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
92
	 * @return string|false Cached datas
93
	 */
94
	public function charger($id, $ne_pas_tester_validiter_du_cache = false) {
95
		$this->verifierEtCreerStructureBdd();
96
		$requete = "SELECT content FROM cache WHERE id = '$id'".
97
			(($ne_pas_tester_validiter_du_cache) ? '' : ' AND (expire = 0 OR expire > '.time().')');
98
		$resultat = $this->requeter($requete);
99
		$ligne = @sqlite_fetch_array($resultat);
100
		return ($ligne) ? $ligne['content'] : false;
101
	}
102
 
103
	/**
104
	 * Test if a cache is available or not (for the given id)
105
	 *
106
	 * @param string $id Cache id
107
	 * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record
108
	 */
109
	public function tester($id) {
110
		$this->verifierEtCreerStructureBdd();
111
		$requete = "SELECT lastModified FROM cache WHERE id = '$id' AND (expire = 0 OR expire > ".time().')';
112
		$resultat = $this->requeter($requete);
113
		$ligne = @sqlite_fetch_array($resultat);
114
		return ($ligne) ? ((int) $ligne['lastModified']) : false;
115
	}
116
 
117
	/**
118
	 * Save some string datas into a cache record
119
	 *
120
	 * Note : $data is always "string" (serialization is done by the
121
	 * core not by the backend)
122
	 *
123
	 * @param  string $data			 Datas to cache
124
	 * @param  string $id			   Cache id
125
	 * @param  array  $tags			 Array of strings, the cache record will be tagged by each string entry
126
	 * @param  int	$specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
127
	 * @throws Zend_Cache_Exception
128
	 * @return boolean True if no problem
129
	 */
130
	public function sauver($donnees, $id, $tags = array(), $duree_vie_specifique = false) {
131
		$this->verifierEtCreerStructureBdd();
132
		$donnees = @sqlite_escape_string($donnees);
133
		$timestamp_courrant = time();
134
		$expiration = $this->Cache->getTimestampExpiration($duree_vie_specifique);
135
 
136
		$this->requeter("DELETE FROM cache WHERE id = '$id'");
137
		$sql = "INSERT INTO cache (id, content, lastModified, expire) VALUES ('$id', '$donnees', $timestamp_courrant, $expiration)";
138
		$resultat = $this->requeter($sql);
139
		if (!$resultat) {
140
			// TODO : ajouter un log sauver() : impossible de stocker le cache d'id '$id'
141
			Debug::printr("sauver() : impossible de stocker le cache d'id '$id'");
142
			$resultat =  false;
143
		} else {
144
			$resultat = true;
145
			foreach ($tags as $tag) {
146
				$resultat = $this->enregisterTag($id, $tag) && $resultat;
147
			}
148
		}
149
		return $resultat;
150
	}
151
 
152
	/**
153
	 * Remove a cache record
154
	 *
155
	 * @param  string $id Cache id
156
	 * @return boolean True if no problem
157
	 */
158
	public function supprimer($id) {
159
		$this->verifierEtCreerStructureBdd();
160
		$resultat = $this->requeter("SELECT COUNT(*) AS nbr FROM cache WHERE id = '$id'");
161
		$resultat_nbre = @sqlite_fetch_single($resultat);
162
		$suppression_cache = $this->requeter("DELETE FROM cache WHERE id = '$id'");
163
		$suppression_tags = $this->requeter("DELETE FROM tag WHERE id = '$id'");
164
		$this->defragmenterAutomatiquement();
165
		return ($resultat_nbre && $suppression_cache && $suppression_tags);
166
	}
167
 
168
	/**
169
	 * Clean some cache records
170
	 *
171
	 * Available modes are :
172
	 * Zend_Cache::CLEANING_MODE_ALL (default)	=> remove all cache entries ($tags is not used)
173
	 * Zend_Cache::CLEANING_MODE_OLD			  => remove too old cache entries ($tags is not used)
174
	 * Zend_Cache::CLEANING_MODE_MATCHING_TAG	 => remove cache entries matching all given tags
175
	 *											   ($tags can be an array of strings or a single string)
176
	 * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
177
	 *											   ($tags can be an array of strings or a single string)
178
	 * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
179
	 *											   ($tags can be an array of strings or a single string)
180
	 *
181
	 * @param  string $mode Clean mode
182
	 * @param  array  $tags Array of tags
183
	 * @return boolean True if no problem
184
	 */
185
	public function nettoyer($mode = Cache::NETTOYAGE_MODE_TOUS, $tags = array()) {
186
		$this->verifierEtCreerStructureBdd();
187
		$retour = $this->nettoyerSqlite($mode, $tags);
188
		$this->defragmenterAutomatiquement();
189
		return $retour;
190
	}
191
 
192
	/**
193
	 * Return an array of stored cache ids
194
	 *
195
	 * @return array array of stored cache ids (string)
196
	 */
197
	public function getIds() {
198
		$this->verifierEtCreerStructureBdd();
199
		$resultat = $this->requeter('SELECT id FROM cache WHERE (expire = 0 OR expire > '.time().')');
200
		$retour = array();
201
		while ($id = @sqlite_fetch_single($resultat)) {
202
			$retour[] = $id;
203
		}
204
		return $retour;
205
	}
206
 
207
	/**
208
	 * Return an array of stored tags
209
	 *
210
	 * @return array array of stored tags (string)
211
	 */
212
	public function getTags() {
213
		$this->verifierEtCreerStructureBdd();
214
		$resultat = $this->requeter('SELECT DISTINCT(name) AS name FROM tag');
215
		$retour = array();
216
		while ($id = @sqlite_fetch_single($resultat)) {
217
			$retour[] = $id;
218
		}
219
		return $retour;
220
	}
221
 
222
	/**
223
	 * Return an array of stored cache ids which match given tags
224
	 *
225
	 * In case of multiple tags, a logical AND is made between tags
226
	 *
227
	 * @param array $tags array of tags
228
	 * @return array array of matching cache ids (string)
229
	 */
230
	public function getIdsAvecLesTags($tags = array()) {
231
		$this->verifierEtCreerStructureBdd();
232
		$premier = true;
233
		$ids = array();
234
		foreach ($tags as $tag) {
235
			$resultat = $this->requeter("SELECT DISTINCT(id) AS id FROM tag WHERE name='$tag'");
236
			if ($resultat) {
237
				$lignes = @sqlite_fetch_all($resultat, SQLITE_ASSOC);
238
				$ids_tmp = array();
239
				foreach ($lignes as $ligne) {
240
					$ids_tmp[] = $ligne['id'];
241
				}
242
				if ($premier) {
243
					$ids = $ids_tmp;
244
					$premier = false;
245
				} else {
246
					$ids = array_intersect($ids, $ids_tmp);
247
				}
248
			}
249
		}
250
 
251
		$retour = array();
252
		if (count($ids) > 0) {
253
			foreach ($ids as $id) {
254
				$retour[] = $id;
255
			}
256
		}
257
		return $retour;
258
	}
259
 
260
	/**
261
	 * Return an array of stored cache ids which don't match given tags
262
	 *
263
	 * In case of multiple tags, a logical OR is made between tags
264
	 *
265
	 * @param array $tags array of tags
266
	 * @return array array of not matching cache ids (string)
267
	 */
268
	public function getIdsSansLesTags($tags = array()) {
269
		$this->verifierEtCreerStructureBdd();
270
		$resultat = $this->requeter('SELECT id FROM cache');
271
		$lignes = @sqlite_fetch_all($resultat, SQLITE_ASSOC);
272
		$retour = array();
273
		foreach ($lignes as $ligne) {
274
			$id = $ligne['id'];
275
			$correspondance = false;
276
			foreach ($tags as $tag) {
277
				$resultat = $this->requeter("SELECT COUNT(*) AS nbr FROM tag WHERE name = '$tag' AND id = '$id'");
278
				if ($resultat) {
279
					$nbre = (int) @sqlite_fetch_single($resultat);
280
					if ($nbre > 0) {
281
						$correspondance = true;
282
					}
283
				}
284
			}
285
			if (!$correspondance) {
286
				$retour[] = $id;
287
			}
288
		}
289
		return $retour;
290
	}
291
 
292
	/**
293
	 * Return an array of stored cache ids which match any given tags
294
	 *
295
	 * In case of multiple tags, a logical AND is made between tags
296
	 *
297
	 * @param array $tags array of tags
298
	 * @return array array of any matching cache ids (string)
299
	 */
300
	public function getIdsAvecUnTag($tags = array()) {
301
		$this->verifierEtCreerStructureBdd();
302
		$premier = true;
303
		$ids = array();
304
		foreach ($tags as $tag) {
305
			$resultat = $this->requeter("SELECT DISTINCT(id) AS id FROM tag WHERE name = '$tag'");
306
			if ($resultat) {
307
				$lignes = @sqlite_fetch_all($resultat, SQLITE_ASSOC);
308
				$ids_tmp = array();
309
				foreach ($lignes as $ligne) {
310
					$ids_tmp[] = $ligne['id'];
311
				}
312
				if ($premier) {
313
					$ids = $ids_tmp;
314
					$premier = false;
315
				} else {
316
					$ids = array_merge($ids, $ids_tmp);
317
				}
318
			}
319
		}
320
 
321
		$retour = array();
322
		if (count($ids) > 0) {
323
			foreach ($ids as $id) {
324
				$retour[] = $id;
325
			}
326
		}
327
		return $retour;
328
	}
329
 
330
	/**
331
	 * Return the filling percentage of the backend storage
332
	 *
333
	 * @throws Zend_Cache_Exception
334
	 * @return int integer between 0 and 100
335
	 */
336
	public function getPourcentageRemplissage() {
337
		$dossier = dirname($this->options['stockage_chemin']);
338
		$libre = disk_free_space($dossier);
339
		$total = disk_total_space($dossier);
340
 
341
		$pourcentage = 0;
342
		if ($total == 0) {
343
			trigger_error("Impossible d'utiliser la fonction disk_total_space", E_USER_WARNING);
344
		} else {
345
			$pourcentage = ($libre >= $total) ? 100 : ((int) (100. * ($total - $libre) / $total));
346
		}
347
		return $pourcentage;
348
	}
349
 
350
	/**
351
	 * Return an array of metadatas for the given cache id
352
	 *
353
	 * The array must include these keys :
354
	 * - expire : the expire timestamp
355
	 * - tags : a string array of tags
356
	 * - mtime : timestamp of last modification time
357
	 *
358
	 * @param string $id cache id
359
	 * @return array array of metadatas (false if the cache id is not found)
360
	 */
361
	public function getMetadonnees($id) {
362
		$this->verifierEtCreerStructureBdd();
363
		$tags = array();
364
		$resultat = $this->requeter("SELECT name FROM tag WHERE id = '$id'");
365
		if ($resultat) {
366
			$lignes = @sqlite_fetch_all($resultat, SQLITE_ASSOC);
367
			foreach ($lignes as $ligne) {
368
				$tags[] = $ligne['name'];
369
			}
370
		}
371
		$resultat = $this->requeter("SELECT lastModified, expire FROM cache WHERE id = '$id'");
372
		if ($resultat) {
373
			$ligne = @sqlite_fetch_array($resultat, SQLITE_ASSOC);
374
			$resultat = array(
375
				'tags' => $tags,
376
				'mtime' => $ligne['lastModified'],
377
				'expiration' => $ligne['expire']);
378
		} else {
379
			$resultat = false;
380
		}
381
		return $resultat;
382
	}
383
 
384
	/**
385
	 * Give (if possible) an extra lifetime to the given cache id
386
	 *
387
	 * @param string $id cache id
388
	 * @param int $extraLifetime
389
	 * @return boolean true if ok
390
	 */
391
	public function ajouterSupplementDureeDeVie($id, $supplement_duree_de_vie) {
392
		$this->verifierEtCreerStructureBdd();
393
		$augmentation = false;
394
		$requete = "SELECT expire FROM cache WHERE id = '$id' AND (expire = 0 OR expire > ".time().')';
395
		$resultat = $this->requeter($requete);
396
		if ($resultat) {
397
			$expiration = @sqlite_fetch_single($resultat);
398
			$nouvelle_expiration = $expiration + $supplement_duree_de_vie;
399
			$resultat = $this->requeter('UPDATE cache SET lastModified = '.time().", expire = $nouvelle_expiration WHERE id = '$id'");
400
			$augmentation = ($resultat) ? true : false;
401
		}
402
		return $augmentation;
403
	}
404
 
405
	/**
406
	 * Return the connection resource
407
	 *
408
	 * If we are not connected, the connection is made
409
	 *
410
	 * @throws Zend_Cache_Exception
411
	 * @return resource Connection resource
412
	 */
413
	private function getConnexion() {
414
		if (!is_resource($this->bdd)) {
415
			if ($this->options['stockage_chemin'] === null) {
416
				$e = "L'emplacement du chemin vers le fichier de la base de données SQLite n'a pas été défini";
417
				trigger_error($e, E_USER_ERROR);
418
			} else {
419
				$this->bdd = sqlite_open($this->options['stockage_chemin']);
420
				if (!(is_resource($this->bdd))) {
421
					$e = "Impossible d'ouvrir le fichier '".$this->options['stockage_chemin']."' de la base de données SQLite.";
422
					trigger_error($e, E_USER_ERROR);
423
					$this->bdd = null;
424
				}
425
			}
426
		}
427
		return $this->bdd;
428
	}
429
 
430
	/**
431
	 * Execute une requête SQL sans afficher de messages d'erreur.
432
	 *
433
	 * @param string $requete requête SQL
434
	 * @return mixed|false resultats de la requête
435
	 */
436
	private function requeter($requete) {
437
		$bdd = $this->getConnexion();
438
		//Debug::printr($requete);
439
		$resultat = (is_resource($bdd)) ? @sqlite_query($bdd, $requete, SQLITE_ASSOC, $e_sqlite) : false;
440
		if (is_resource($bdd) && ! $resultat) {
441
			Debug::printr("Erreur SQLITE :\n$e_sqlite\nPour la requête :\n$requete\nRessource : $bdd");
442
		}
443
		return $resultat;
444
	}
445
 
446
	/**
447
	 * Deal with the automatic vacuum process
448
	 *
449
	 * @return void
450
	 */
451
	private function defragmenterAutomatiquement() {
452
		if ($this->options['defragmentation_auto'] > 0) {
453
			$rand = rand(1, $this->options['defragmentation_auto']);
454
			if ($rand == 1) {
455
				$this->requeter('VACUUM');
456
				@sqlite_close($this->getConnexion());
457
			}
458
		}
459
	}
460
 
461
	/**
462
	 * Register a cache id with the given tag
463
	 *
464
	 * @param  string $id  Cache id
465
	 * @param  string $tag Tag
466
	 * @return boolean True if no problem
467
	 */
468
	private function enregisterTag($id, $tag) {
469
		$requete_suppression = "DELETE FROM tag WHERE name = '$tag' AND id = '$id'";
470
		$resultat = $this->requeter($requete_suppression);
471
		$requete_insertion = "INSERT INTO tag(name,id) VALUES ('$tag','$id')";
472
		$resultat = $this->requeter($requete_insertion);
473
		if (!$resultat) {
474
			// TODO : ajouter un log -> impossible d'enregistrer le tag=$tag pour le cache id=$id");
475
			Debug::printr("Impossible d'enregistrer le tag=$tag pour le cache id=$id");
476
		}
477
		return ($resultat) ? true : false;
478
	}
479
 
480
	/**
481
	 * Build the database structure
482
	 *
483
	 * @return false
484
	 */
485
	private function creerStructure() {
486
		Debug::printr("création de la str");
487
		$this->requeter('DROP INDEX IF EXISTS tag_id_index');
488
		$this->requeter('DROP INDEX IF EXISTS tag_name_index');
489
		$this->requeter('DROP INDEX IF EXISTS cache_id_expire_index');
490
		$this->requeter('DROP TABLE IF EXISTS version');
491
		$this->requeter('DROP TABLE IF EXISTS cache');
492
		$this->requeter('DROP TABLE IF EXISTS tag');
493
		$this->requeter('CREATE TABLE version (num INTEGER PRIMARY KEY)');
494
		$this->requeter('CREATE TABLE cache(id TEXT PRIMARY KEY, content BLOB, lastModified INTEGER, expire INTEGER)');
495
		$this->requeter('CREATE TABLE tag (name TEXT, id TEXT)');
496
		$this->requeter('CREATE INDEX tag_id_index ON tag(id)');
497
		$this->requeter('CREATE INDEX tag_name_index ON tag(name)');
498
		$this->requeter('CREATE INDEX cache_id_expire_index ON cache(id, expire)');
499
		$this->requeter('INSERT INTO version (num) VALUES (1)');
500
	}
501
 
502
	/**
503
	 * Check if the database structure is ok (with the good version)
504
	 *
505
	 * @return boolean True if ok
506
	 */
507
	private function verifierBddStructureVersion() {
508
		$version_ok = false;
509
		$resultat = $this->requeter('SELECT num FROM version');
510
		if ($resultat) {
511
			$ligne = @sqlite_fetch_array($resultat);
512
			if ($ligne) {
513
				if (((int) $ligne['num']) == 1) {
514
					$version_ok = true;
515
				} else {
516
					// 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é
517
				}
518
			}
519
		}
520
		return $version_ok;
521
	}
522
 
523
	/**
524
	 * Clean some cache records
525
	 *
526
	 * Available modes are :
527
	 * Zend_Cache::CLEANING_MODE_ALL (default)	=> remove all cache entries ($tags is not used)
528
	 * Zend_Cache::CLEANING_MODE_OLD			  => remove too old cache entries ($tags is not used)
529
	 * Zend_Cache::CLEANING_MODE_MATCHING_TAG	 => remove cache entries matching all given tags
530
	 *											   ($tags can be an array of strings or a single string)
531
	 * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
532
	 *											   ($tags can be an array of strings or a single string)
533
	 * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
534
	 *											   ($tags can be an array of strings or a single string)
535
	 *
536
	 * @param  string $mode Clean mode
537
	 * @param  array  $tags Array of tags
538
	 * @return boolean True if no problem
539
	 */
540
	private function nettoyerSqlite($mode = Cache::NETTOYAGE_MODE_TOUS, $tags = array()) {
541
		$nettoyage_ok = false;
542
		switch ($mode) {
543
			case Cache::NETTOYAGE_MODE_TOUS:
544
				$suppression_cache = $this->requeter('DELETE FROM cache');
545
				$suppression_tag = $this->requeter('DELETE FROM tag');
546
				$nettoyage_ok = $suppression_cache && $suppression_tag;
547
				break;
548
			case Cache::NETTOYAGE_MODE_EXPIRATION:
549
				$mktime = time();
550
				$suppression_tag = $this->requeter("DELETE FROM tag WHERE id IN (SELECT id FROM cache WHERE expire > 0 AND expire <= $mktime)");
551
				$suppression_cache = $this->requeter("DELETE FROM cache WHERE expire > 0 AND expire <= $mktime");
552
				return $suppression_tag && $suppression_cache;
553
				break;
554
			case Cache::NETTOYAGE_MODE_AVEC_LES_TAGS:
555
				$ids = $this->getIdsAvecLesTags($tags);
556
				$resultat = true;
557
				foreach ($ids as $id) {
558
					$resultat = $this->supprimer($id) && $resultat;
559
				}
560
				return $resultat;
561
				break;
562
			case Cache::NETTOYAGE_MODE_SANS_LES_TAGS:
563
				$ids = $this->getIdsSansLesTags($tags);
564
				$resultat = true;
565
				foreach ($ids as $id) {
566
					$resultat = $this->supprimer($id) && $resultat;
567
				}
568
				return $resultat;
569
				break;
570
			case Cache::NETTOYAGE_MODE_AVEC_UN_TAG:
571
				$ids = $this->getIdsAvecUnTag($tags);
572
				$resultat = true;
573
				foreach ($ids as $id) {
574
					$resultat = $this->supprimer($id) && $resultat;
575
				}
576
				return $resultat;
577
				break;
578
			default:
579
				break;
580
		}
581
		return $nettoyage_ok;
582
	}
583
 
584
	/**
585
	 * Check if the database structure is ok (with the good version), if no : build it
586
	 *
587
	 * @throws Zend_Cache_Exception
588
	 * @return boolean True if ok
589
	 */
590
	private function verifierEtCreerStructureBdd() {
591
		if (! $this->structure_ok) {
592
			if (! $this->verifierBddStructureVersion()) {
593
				$this->creerStructure();
594
				if (! $this->verifierBddStructureVersion()) {
595
					$e = "Impossible de construire la base de données de cache dans ".$this->options['stockage_chemin'];
596
					trigger_error($e, E_USER_WARNING);
597
					$this->structure_ok = false;
598
				}
599
			}
600
			$this->structure_ok = true;
601
		}
602
		return $this->structure_ok;
603
	}
604
 
605
}
606
?>