00001 <?php
00027 require_once 'Zend/Cache/Backend/ExtendedInterface.php';
00028
00032 require_once 'Zend/Cache/Backend.php';
00033
00040 class Zend_Cache_Backend_Sqlite extends Zend_Cache_Backend implements Zend_Cache_Backend_ExtendedInterface
00041 {
00058 protected $_options = array(
00059 'cache_db_complete_path' => null,
00060 'automatic_vacuum_factor' => 10
00061 );
00062
00068 private $_db = null;
00069
00075 private $_structureChecked = false;
00076
00084 public function __construct(array $options = array())
00085 {
00086 parent::__construct($options);
00087 if ($this->_options['cache_db_complete_path'] === null) {
00088 Zend_Cache::throwException('cache_db_complete_path option has to set');
00089 }
00090 if (!extension_loaded('sqlite')) {
00091 Zend_Cache::throwException("Cannot use SQLite storage because the 'sqlite' extension is not loaded in the current PHP environment");
00092 }
00093 $this->_getConnection();
00094 }
00095
00101 public function __destruct()
00102 {
00103 @sqlite_close($this->_getConnection());
00104 }
00105
00113 public function load($id, $doNotTestCacheValidity = false)
00114 {
00115 $this->_checkAndBuildStructure();
00116 $sql = "SELECT content FROM cache WHERE id='$id'";
00117 if (!$doNotTestCacheValidity) {
00118 $sql = $sql . " AND (expire=0 OR expire>" . time() . ')';
00119 }
00120 $result = $this->_query($sql);
00121 $row = @sqlite_fetch_array($result);
00122 if ($row) {
00123 return $row['content'];
00124 }
00125 return false;
00126 }
00127
00134 public function test($id)
00135 {
00136 $this->_checkAndBuildStructure();
00137 $sql = "SELECT lastModified FROM cache WHERE id='$id' AND (expire=0 OR expire>" . time() . ')';
00138 $result = $this->_query($sql);
00139 $row = @sqlite_fetch_array($result);
00140 if ($row) {
00141 return ((int) $row['lastModified']);
00142 }
00143 return false;
00144 }
00145
00159 public function save($data, $id, $tags = array(), $specificLifetime = false)
00160 {
00161 $this->_checkAndBuildStructure();
00162 $lifetime = $this->getLifetime($specificLifetime);
00163 $data = @sqlite_escape_string($data);
00164 $mktime = time();
00165 if ($lifetime === null) {
00166 $expire = 0;
00167 } else {
00168 $expire = $mktime + $lifetime;
00169 }
00170 $this->_query("DELETE FROM cache WHERE id='$id'");
00171 $sql = "INSERT INTO cache (id, content, lastModified, expire) VALUES ('$id', '$data', $mktime, $expire)";
00172 $res = $this->_query($sql);
00173 if (!$res) {
00174 $this->_log("Zend_Cache_Backend_Sqlite::save() : impossible to store the cache id=$id");
00175 return false;
00176 }
00177 $res = true;
00178 foreach ($tags as $tag) {
00179 $res = $this->_registerTag($id, $tag) && $res;
00180 }
00181 return $res;
00182 }
00183
00190 public function remove($id)
00191 {
00192 $this->_checkAndBuildStructure();
00193 $res = $this->_query("SELECT COUNT(*) AS nbr FROM cache WHERE id='$id'");
00194 $result1 = @sqlite_fetch_single($res);
00195 $result2 = $this->_query("DELETE FROM cache WHERE id='$id'");
00196 $result3 = $this->_query("DELETE FROM tag WHERE id='$id'");
00197 $this->_automaticVacuum();
00198 return ($result1 && $result2 && $result3);
00199 }
00200
00218 public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
00219 {
00220 $this->_checkAndBuildStructure();
00221 $return = $this->_clean($mode, $tags);
00222 $this->_automaticVacuum();
00223 return $return;
00224 }
00225
00231 public function getIds()
00232 {
00233 $this->_checkAndBuildStructure();
00234 $res = $this->_query("SELECT id FROM cache WHERE (expire=0 OR expire>" . time() . ")");
00235 $result = array();
00236 while ($id = @sqlite_fetch_single($res)) {
00237 $result[] = $id;
00238 }
00239 return $result;
00240 }
00241
00247 public function getTags()
00248 {
00249 $this->_checkAndBuildStructure();
00250 $res = $this->_query("SELECT DISTINCT(name) AS name FROM tag");
00251 $result = array();
00252 while ($id = @sqlite_fetch_single($res)) {
00253 $result[] = $id;
00254 }
00255 return $result;
00256 }
00257
00266 public function getIdsMatchingTags($tags = array())
00267 {
00268 $first = true;
00269 $ids = array();
00270 foreach ($tags as $tag) {
00271 $res = $this->_query("SELECT DISTINCT(id) AS id FROM tag WHERE name='$tag'");
00272 if (!$res) {
00273 return array();
00274 }
00275 $rows = @sqlite_fetch_all($res, SQLITE_ASSOC);
00276 $ids2 = array();
00277 foreach ($rows as $row) {
00278 $ids2[] = $row['id'];
00279 }
00280 if ($first) {
00281 $ids = $ids2;
00282 $first = false;
00283 } else {
00284 $ids = array_intersect($ids, $ids2);
00285 }
00286 }
00287 $result = array();
00288 foreach ($ids as $id) {
00289 $result[] = $id;
00290 }
00291 return $result;
00292 }
00293
00302 public function getIdsNotMatchingTags($tags = array())
00303 {
00304 $res = $this->_query("SELECT id FROM cache");
00305 $rows = @sqlite_fetch_all($res, SQLITE_ASSOC);
00306 $result = array();
00307 foreach ($rows as $row) {
00308 $id = $row['id'];
00309 $matching = false;
00310 foreach ($tags as $tag) {
00311 $res = $this->_query("SELECT COUNT(*) AS nbr FROM tag WHERE name='$tag' AND id='$id'");
00312 if (!$res) {
00313 return array();
00314 }
00315 $nbr = (int) @sqlite_fetch_single($res);
00316 if ($nbr > 0) {
00317 $matching = true;
00318 }
00319 }
00320 if (!$matching) {
00321 $result[] = $id;
00322 }
00323 }
00324 return $result;
00325 }
00326
00335 public function getIdsMatchingAnyTags($tags = array())
00336 {
00337 $first = true;
00338 $ids = array();
00339 foreach ($tags as $tag) {
00340 $res = $this->_query("SELECT DISTINCT(id) AS id FROM tag WHERE name='$tag'");
00341 if (!$res) {
00342 return array();
00343 }
00344 $rows = @sqlite_fetch_all($res, SQLITE_ASSOC);
00345 $ids2 = array();
00346 foreach ($rows as $row) {
00347 $ids2[] = $row['id'];
00348 }
00349 if ($first) {
00350 $ids = $ids2;
00351 $first = false;
00352 } else {
00353 $ids = array_merge($ids, $ids2);
00354 }
00355 }
00356 $result = array();
00357 foreach ($ids as $id) {
00358 $result[] = $id;
00359 }
00360 return $result;
00361 }
00362
00369 public function getFillingPercentage()
00370 {
00371 $dir = dirname($this->_options['cache_db_complete_path']);
00372 $free = disk_free_space($dir);
00373 $total = disk_total_space($dir);
00374 if ($total == 0) {
00375 Zend_Cache::throwException('can\'t get disk_total_space');
00376 } else {
00377 if ($free >= $total) {
00378 return 100;
00379 }
00380 return ((int) (100. * ($total - $free) / $total));
00381 }
00382 }
00383
00395 public function getMetadatas($id)
00396 {
00397 $tags = array();
00398 $res = $this->_query("SELECT name FROM tag WHERE id='$id'");
00399 if ($res) {
00400 $rows = @sqlite_fetch_all($res, SQLITE_ASSOC);
00401 foreach ($rows as $row) {
00402 $tags[] = $row['name'];
00403 }
00404 }
00405 $this->_query('CREATE TABLE cache (id TEXT PRIMARY KEY, content BLOB, lastModified INTEGER, expire INTEGER)');
00406 $res = $this->_query("SELECT lastModified,expire FROM cache WHERE id='$id'");
00407 if (!$res) {
00408 return false;
00409 }
00410 $row = @sqlite_fetch_array($res, SQLITE_ASSOC);
00411 return array(
00412 'tags' => $tags,
00413 'mtime' => $row['lastModified'],
00414 'expire' => $row['expire']
00415 );
00416 }
00417
00425 public function touch($id, $extraLifetime)
00426 {
00427 $sql = "SELECT expire FROM cache WHERE id='$id' AND (expire=0 OR expire>" . time() . ')';
00428 $res = $this->_query($sql);
00429 if (!$res) {
00430 return false;
00431 }
00432 $expire = @sqlite_fetch_single($res);
00433 $newExpire = $expire + $extraLifetime;
00434 $res = $this->_query("UPDATE cache SET lastModified=" . time() . ", expire=$newExpire WHERE id='$id'");
00435 if ($res) {
00436 return true;
00437 } else {
00438 return false;
00439 }
00440 }
00441
00456 public function getCapabilities()
00457 {
00458 return array(
00459 'automatic_cleaning' => true,
00460 'tags' => true,
00461 'expired_read' => true,
00462 'priority' => false,
00463 'infinite_lifetime' => true,
00464 'get_list' => true
00465 );
00466 }
00467
00475 public function ___expire($id)
00476 {
00477 $time = time() - 1;
00478 $this->_query("UPDATE cache SET lastModified=$time, expire=$time WHERE id='$id'");
00479 }
00480
00489 private function _getConnection()
00490 {
00491 if (is_resource($this->_db)) {
00492 return $this->_db;
00493 } else {
00494 $this->_db = @sqlite_open($this->_options['cache_db_complete_path']);
00495 if (!(is_resource($this->_db))) {
00496 Zend_Cache::throwException("Impossible to open " . $this->_options['cache_db_complete_path'] . " cache DB file");
00497 }
00498 return $this->_db;
00499 }
00500 }
00501
00508 private function _query($query)
00509 {
00510 $db = $this->_getConnection();
00511 if (is_resource($db)) {
00512 $res = @sqlite_query($db, $query);
00513 if ($res === false) {
00514 return false;
00515 } else {
00516 return $res;
00517 }
00518 }
00519 return false;
00520 }
00521
00527 private function _automaticVacuum()
00528 {
00529 if ($this->_options['automatic_vacuum_factor'] > 0) {
00530 $rand = rand(1, $this->_options['automatic_vacuum_factor']);
00531 if ($rand == 1) {
00532 $this->_query('VACUUM');
00533 @sqlite_close($this->_getConnection());
00534 }
00535 }
00536 }
00537
00545 private function _registerTag($id, $tag) {
00546 $res = $this->_query("DELETE FROM TAG WHERE name='$tag' AND id='$id'");
00547 $res = $this->_query("INSERT INTO tag (name, id) VALUES ('$tag', '$id')");
00548 if (!$res) {
00549 $this->_log("Zend_Cache_Backend_Sqlite::_registerTag() : impossible to register tag=$tag on id=$id");
00550 return false;
00551 }
00552 return true;
00553 }
00554
00560 private function _buildStructure()
00561 {
00562 $this->_query('DROP INDEX tag_id_index');
00563 $this->_query('DROP INDEX tag_name_index');
00564 $this->_query('DROP INDEX cache_id_expire_index');
00565 $this->_query('DROP TABLE version');
00566 $this->_query('DROP TABLE cache');
00567 $this->_query('DROP TABLE tag');
00568 $this->_query('CREATE TABLE version (num INTEGER PRIMARY KEY)');
00569 $this->_query('CREATE TABLE cache (id TEXT PRIMARY KEY, content BLOB, lastModified INTEGER, expire INTEGER)');
00570 $this->_query('CREATE TABLE tag (name TEXT, id TEXT)');
00571 $this->_query('CREATE INDEX tag_id_index ON tag(id)');
00572 $this->_query('CREATE INDEX tag_name_index ON tag(name)');
00573 $this->_query('CREATE INDEX cache_id_expire_index ON cache(id, expire)');
00574 $this->_query('INSERT INTO version (num) VALUES (1)');
00575 }
00576
00582 private function _checkStructureVersion()
00583 {
00584 $result = $this->_query("SELECT num FROM version");
00585 if (!$result) return false;
00586 $row = @sqlite_fetch_array($result);
00587 if (!$row) {
00588 return false;
00589 }
00590 if (((int) $row['num']) != 1) {
00591
00592 $this->_log('Zend_Cache_Backend_Sqlite::_checkStructureVersion() : old cache structure version detected => the cache is going to be dropped');
00593 return false;
00594 }
00595 return true;
00596 }
00597
00615 private function _clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
00616 {
00617 switch ($mode) {
00618 case Zend_Cache::CLEANING_MODE_ALL:
00619 $res1 = $this->_query('DELETE FROM cache');
00620 $res2 = $this->_query('DELETE FROM tag');
00621 return $res1 && $res2;
00622 break;
00623 case Zend_Cache::CLEANING_MODE_OLD:
00624 $mktime = time();
00625 $res1 = $this->_query("DELETE FROM tag WHERE id IN (SELECT id FROM cache WHERE expire>0 AND expire<=$mktime)");
00626 $res2 = $this->_query("DELETE FROM cache WHERE expire>0 AND expire<=$mktime");
00627 return $res1 && $res2;
00628 break;
00629 case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
00630 $ids = $this->getIdsMatchingTags($tags);
00631 $result = true;
00632 foreach ($ids as $id) {
00633 $result = $this->remove($id) && $result;
00634 }
00635 return $result;
00636 break;
00637 case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
00638 $ids = $this->getIdsNotMatchingTags($tags);
00639 $result = true;
00640 foreach ($ids as $id) {
00641 $result = $this->remove($id) && $result;
00642 }
00643 return $result;
00644 break;
00645 case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
00646 $ids = $this->getIdsMatchingAnyTags($tags);
00647 $result = true;
00648 foreach ($ids as $id) {
00649 $result = $this->remove($id) && $result;
00650 }
00651 return $result;
00652 break;
00653 default:
00654 break;
00655 }
00656 return false;
00657 }
00658
00665 private function _checkAndBuildStructure()
00666 {
00667 if (!($this->_structureChecked)) {
00668 if (!$this->_checkStructureVersion()) {
00669 $this->_buildStructure();
00670 if (!$this->_checkStructureVersion()) {
00671 Zend_Cache::throwException("Impossible to build cache structure in " . $this->_options['cache_db_complete_path']);
00672 }
00673 }
00674 $this->_structureChecked = true;
00675 }
00676 return true;
00677 }
00678
00679 }