PageRenderTime 64ms CodeModel.GetById 13ms app.highlight 42ms RepoModel.GetById 2ms app.codeStats 0ms

/framework/vendor/zend/Zend/Cache/Backend/Sqlite.php

http://zoop.googlecode.com/
PHP | 679 lines | 389 code | 32 blank | 258 comment | 54 complexity | 410920650e5d8e1021993be9ae9880f5 MD5 | raw file
  1<?php
  2/**
  3 * Zend Framework
  4 *
  5 * LICENSE
  6 *
  7 * This source file is subject to the new BSD license that is bundled
  8 * with this package in the file LICENSE.txt.
  9 * It is also available through the world-wide-web at this URL:
 10 * http://framework.zend.com/license/new-bsd
 11 * If you did not receive a copy of the license and are unable to
 12 * obtain it through the world-wide-web, please send an email
 13 * to license@zend.com so we can send you a copy immediately.
 14 *
 15 * @category   Zend
 16 * @package    Zend_Cache
 17 * @subpackage Zend_Cache_Backend
 18 * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
 19 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 20 * @version    $Id: Sqlite.php 20096 2010-01-06 02:05:09Z bkarwin $
 21 */
 22
 23
 24/**
 25 * @see Zend_Cache_Backend_Interface
 26 */
 27require_once 'Zend/Cache/Backend/ExtendedInterface.php';
 28
 29/**
 30 * @see Zend_Cache_Backend
 31 */
 32require_once 'Zend/Cache/Backend.php';
 33
 34/**
 35 * @package    Zend_Cache
 36 * @subpackage Zend_Cache_Backend
 37 * @copyright  Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
 38 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 39 */
 40class Zend_Cache_Backend_Sqlite extends Zend_Cache_Backend implements Zend_Cache_Backend_ExtendedInterface
 41{
 42    /**
 43     * Available options
 44     *
 45     * =====> (string) cache_db_complete_path :
 46     * - the complete path (filename included) of the SQLITE database
 47     *
 48     * ====> (int) automatic_vacuum_factor :
 49     * - Disable / Tune the automatic vacuum process
 50     * - The automatic vacuum process defragment the database file (and make it smaller)
 51     *   when a clean() or delete() is called
 52     *     0               => no automatic vacuum
 53     *     1               => systematic vacuum (when delete() or clean() methods are called)
 54     *     x (integer) > 1 => automatic vacuum randomly 1 times on x clean() or delete()
 55     *
 56     * @var array Available options
 57     */
 58    protected $_options = array(
 59        'cache_db_complete_path' => null,
 60        'automatic_vacuum_factor' => 10
 61    );
 62
 63    /**
 64     * DB ressource
 65     *
 66     * @var mixed $_db
 67     */
 68    private $_db = null;
 69
 70    /**
 71     * Boolean to store if the structure has benn checked or not
 72     *
 73     * @var boolean $_structureChecked
 74     */
 75    private $_structureChecked = false;
 76
 77    /**
 78     * Constructor
 79     *
 80     * @param  array $options Associative array of options
 81     * @throws Zend_cache_Exception
 82     * @return void
 83     */
 84    public function __construct(array $options = array())
 85    {
 86        parent::__construct($options);
 87        if ($this->_options['cache_db_complete_path'] === null) {
 88            Zend_Cache::throwException('cache_db_complete_path option has to set');
 89        }
 90        if (!extension_loaded('sqlite')) {
 91            Zend_Cache::throwException("Cannot use SQLite storage because the 'sqlite' extension is not loaded in the current PHP environment");
 92        }
 93        $this->_getConnection();
 94    }
 95
 96    /**
 97     * Destructor
 98     *
 99     * @return void
100     */
101    public function __destruct()
102    {
103        @sqlite_close($this->_getConnection());
104    }
105
106    /**
107     * Test if a cache is available for the given id and (if yes) return it (false else)
108     *
109     * @param  string  $id                     Cache id
110     * @param  boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
111     * @return string|false Cached datas
112     */
113    public function load($id, $doNotTestCacheValidity = false)
114    {
115        $this->_checkAndBuildStructure();
116        $sql = "SELECT content FROM cache WHERE id='$id'";
117        if (!$doNotTestCacheValidity) {
118            $sql = $sql . " AND (expire=0 OR expire>" . time() . ')';
119        }
120        $result = $this->_query($sql);
121        $row = @sqlite_fetch_array($result);
122        if ($row) {
123            return $row['content'];
124        }
125        return false;
126    }
127
128    /**
129     * Test if a cache is available or not (for the given id)
130     *
131     * @param string $id Cache id
132     * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record
133     */
134    public function test($id)
135    {
136        $this->_checkAndBuildStructure();
137        $sql = "SELECT lastModified FROM cache WHERE id='$id' AND (expire=0 OR expire>" . time() . ')';
138        $result = $this->_query($sql);
139        $row = @sqlite_fetch_array($result);
140        if ($row) {
141            return ((int) $row['lastModified']);
142        }
143        return false;
144    }
145
146    /**
147     * Save some string datas into a cache record
148     *
149     * Note : $data is always "string" (serialization is done by the
150     * core not by the backend)
151     *
152     * @param  string $data             Datas to cache
153     * @param  string $id               Cache id
154     * @param  array  $tags             Array of strings, the cache record will be tagged by each string entry
155     * @param  int    $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime)
156     * @throws Zend_Cache_Exception
157     * @return boolean True if no problem
158     */
159    public function save($data, $id, $tags = array(), $specificLifetime = false)
160    {
161        $this->_checkAndBuildStructure();
162        $lifetime = $this->getLifetime($specificLifetime);
163        $data = @sqlite_escape_string($data);
164        $mktime = time();
165        if ($lifetime === null) {
166            $expire = 0;
167        } else {
168            $expire = $mktime + $lifetime;
169        }
170        $this->_query("DELETE FROM cache WHERE id='$id'");
171        $sql = "INSERT INTO cache (id, content, lastModified, expire) VALUES ('$id', '$data', $mktime, $expire)";
172        $res = $this->_query($sql);
173        if (!$res) {
174            $this->_log("Zend_Cache_Backend_Sqlite::save() : impossible to store the cache id=$id");
175            return false;
176        }
177        $res = true;
178        foreach ($tags as $tag) {
179            $res = $this->_registerTag($id, $tag) && $res;
180        }
181        return $res;
182    }
183
184    /**
185     * Remove a cache record
186     *
187     * @param  string $id Cache id
188     * @return boolean True if no problem
189     */
190    public function remove($id)
191    {
192        $this->_checkAndBuildStructure();
193        $res = $this->_query("SELECT COUNT(*) AS nbr FROM cache WHERE id='$id'");
194        $result1 = @sqlite_fetch_single($res);
195        $result2 = $this->_query("DELETE FROM cache WHERE id='$id'");
196        $result3 = $this->_query("DELETE FROM tag WHERE id='$id'");
197        $this->_automaticVacuum();
198        return ($result1 && $result2 && $result3);
199    }
200
201    /**
202     * Clean some cache records
203     *
204     * Available modes are :
205     * Zend_Cache::CLEANING_MODE_ALL (default)    => remove all cache entries ($tags is not used)
206     * Zend_Cache::CLEANING_MODE_OLD              => remove too old cache entries ($tags is not used)
207     * Zend_Cache::CLEANING_MODE_MATCHING_TAG     => remove cache entries matching all given tags
208     *                                               ($tags can be an array of strings or a single string)
209     * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
210     *                                               ($tags can be an array of strings or a single string)
211     * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
212     *                                               ($tags can be an array of strings or a single string)
213     *
214     * @param  string $mode Clean mode
215     * @param  array  $tags Array of tags
216     * @return boolean True if no problem
217     */
218    public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
219    {
220        $this->_checkAndBuildStructure();
221        $return = $this->_clean($mode, $tags);
222        $this->_automaticVacuum();
223        return $return;
224    }
225
226    /**
227     * Return an array of stored cache ids
228     *
229     * @return array array of stored cache ids (string)
230     */
231    public function getIds()
232    {
233        $this->_checkAndBuildStructure();
234        $res = $this->_query("SELECT id FROM cache WHERE (expire=0 OR expire>" . time() . ")");
235        $result = array();
236        while ($id = @sqlite_fetch_single($res)) {
237            $result[] = $id;
238        }
239        return $result;
240    }
241
242    /**
243     * Return an array of stored tags
244     *
245     * @return array array of stored tags (string)
246     */
247    public function getTags()
248    {
249        $this->_checkAndBuildStructure();
250        $res = $this->_query("SELECT DISTINCT(name) AS name FROM tag");
251        $result = array();
252        while ($id = @sqlite_fetch_single($res)) {
253            $result[] = $id;
254        }
255        return $result;
256    }
257
258    /**
259     * Return an array of stored cache ids which match given tags
260     *
261     * In case of multiple tags, a logical AND is made between tags
262     *
263     * @param array $tags array of tags
264     * @return array array of matching cache ids (string)
265     */
266    public function getIdsMatchingTags($tags = array())
267    {
268        $first = true;
269        $ids = array();
270        foreach ($tags as $tag) {
271            $res = $this->_query("SELECT DISTINCT(id) AS id FROM tag WHERE name='$tag'");
272            if (!$res) {
273                return array();
274            }
275            $rows = @sqlite_fetch_all($res, SQLITE_ASSOC);
276            $ids2 = array();
277            foreach ($rows as $row) {
278                $ids2[] = $row['id'];
279            }
280            if ($first) {
281                $ids = $ids2;
282                $first = false;
283            } else {
284                $ids = array_intersect($ids, $ids2);
285            }
286        }
287        $result = array();
288        foreach ($ids as $id) {
289            $result[] = $id;
290        }
291        return $result;
292    }
293
294    /**
295     * Return an array of stored cache ids which don't match given tags
296     *
297     * In case of multiple tags, a logical OR is made between tags
298     *
299     * @param array $tags array of tags
300     * @return array array of not matching cache ids (string)
301     */
302    public function getIdsNotMatchingTags($tags = array())
303    {
304        $res = $this->_query("SELECT id FROM cache");
305        $rows = @sqlite_fetch_all($res, SQLITE_ASSOC);
306        $result = array();
307        foreach ($rows as $row) {
308            $id = $row['id'];
309            $matching = false;
310            foreach ($tags as $tag) {
311                $res = $this->_query("SELECT COUNT(*) AS nbr FROM tag WHERE name='$tag' AND id='$id'");
312                if (!$res) {
313                    return array();
314                }
315                $nbr = (int) @sqlite_fetch_single($res);
316                if ($nbr > 0) {
317                    $matching = true;
318                }
319            }
320            if (!$matching) {
321                $result[] = $id;
322            }
323        }
324        return $result;
325    }
326
327    /**
328     * Return an array of stored cache ids which match any given tags
329     *
330     * In case of multiple tags, a logical AND is made between tags
331     *
332     * @param array $tags array of tags
333     * @return array array of any matching cache ids (string)
334     */
335    public function getIdsMatchingAnyTags($tags = array())
336    {
337        $first = true;
338        $ids = array();
339        foreach ($tags as $tag) {
340            $res = $this->_query("SELECT DISTINCT(id) AS id FROM tag WHERE name='$tag'");
341            if (!$res) {
342                return array();
343            }
344            $rows = @sqlite_fetch_all($res, SQLITE_ASSOC);
345            $ids2 = array();
346            foreach ($rows as $row) {
347                $ids2[] = $row['id'];
348            }
349            if ($first) {
350                $ids = $ids2;
351                $first = false;
352            } else {
353                $ids = array_merge($ids, $ids2);
354            }
355        }
356        $result = array();
357        foreach ($ids as $id) {
358            $result[] = $id;
359        }
360        return $result;
361    }
362
363    /**
364     * Return the filling percentage of the backend storage
365     *
366     * @throws Zend_Cache_Exception
367     * @return int integer between 0 and 100
368     */
369    public function getFillingPercentage()
370    {
371        $dir = dirname($this->_options['cache_db_complete_path']);
372        $free = disk_free_space($dir);
373        $total = disk_total_space($dir);
374        if ($total == 0) {
375            Zend_Cache::throwException('can\'t get disk_total_space');
376        } else {
377            if ($free >= $total) {
378                return 100;
379            }
380            return ((int) (100. * ($total - $free) / $total));
381        }
382    }
383
384    /**
385     * Return an array of metadatas for the given cache id
386     *
387     * The array must include these keys :
388     * - expire : the expire timestamp
389     * - tags : a string array of tags
390     * - mtime : timestamp of last modification time
391     *
392     * @param string $id cache id
393     * @return array array of metadatas (false if the cache id is not found)
394     */
395    public function getMetadatas($id)
396    {
397        $tags = array();
398        $res = $this->_query("SELECT name FROM tag WHERE id='$id'");
399        if ($res) {
400            $rows = @sqlite_fetch_all($res, SQLITE_ASSOC);
401            foreach ($rows as $row) {
402                $tags[] = $row['name'];
403            }
404        }
405        $this->_query('CREATE TABLE cache (id TEXT PRIMARY KEY, content BLOB, lastModified INTEGER, expire INTEGER)');
406        $res = $this->_query("SELECT lastModified,expire FROM cache WHERE id='$id'");
407        if (!$res) {
408            return false;
409        }
410        $row = @sqlite_fetch_array($res, SQLITE_ASSOC);
411        return array(
412            'tags' => $tags,
413            'mtime' => $row['lastModified'],
414            'expire' => $row['expire']
415        );
416    }
417
418    /**
419     * Give (if possible) an extra lifetime to the given cache id
420     *
421     * @param string $id cache id
422     * @param int $extraLifetime
423     * @return boolean true if ok
424     */
425    public function touch($id, $extraLifetime)
426    {
427        $sql = "SELECT expire FROM cache WHERE id='$id' AND (expire=0 OR expire>" . time() . ')';
428        $res = $this->_query($sql);
429        if (!$res) {
430            return false;
431        }
432        $expire = @sqlite_fetch_single($res);
433        $newExpire = $expire + $extraLifetime;
434        $res = $this->_query("UPDATE cache SET lastModified=" . time() . ", expire=$newExpire WHERE id='$id'");
435        if ($res) {
436            return true;
437        } else {
438            return false;
439        }
440    }
441
442    /**
443     * Return an associative array of capabilities (booleans) of the backend
444     *
445     * The array must include these keys :
446     * - automatic_cleaning (is automating cleaning necessary)
447     * - tags (are tags supported)
448     * - expired_read (is it possible to read expired cache records
449     *                 (for doNotTestCacheValidity option for example))
450     * - priority does the backend deal with priority when saving
451     * - infinite_lifetime (is infinite lifetime can work with this backend)
452     * - get_list (is it possible to get the list of cache ids and the complete list of tags)
453     *
454     * @return array associative of with capabilities
455     */
456    public function getCapabilities()
457    {
458        return array(
459            'automatic_cleaning' => true,
460            'tags' => true,
461            'expired_read' => true,
462            'priority' => false,
463            'infinite_lifetime' => true,
464            'get_list' => true
465        );
466    }
467
468    /**
469     * PUBLIC METHOD FOR UNIT TESTING ONLY !
470     *
471     * Force a cache record to expire
472     *
473     * @param string $id Cache id
474     */
475    public function ___expire($id)
476    {
477        $time = time() - 1;
478        $this->_query("UPDATE cache SET lastModified=$time, expire=$time WHERE id='$id'");
479    }
480
481    /**
482     * Return the connection resource
483     *
484     * If we are not connected, the connection is made
485     *
486     * @throws Zend_Cache_Exception
487     * @return resource Connection resource
488     */
489    private function _getConnection()
490    {
491        if (is_resource($this->_db)) {
492            return $this->_db;
493        } else {
494            $this->_db = @sqlite_open($this->_options['cache_db_complete_path']);
495            if (!(is_resource($this->_db))) {
496                Zend_Cache::throwException("Impossible to open " . $this->_options['cache_db_complete_path'] . " cache DB file");
497            }
498            return $this->_db;
499        }
500    }
501
502    /**
503     * Execute an SQL query silently
504     *
505     * @param string $query SQL query
506     * @return mixed|false query results
507     */
508    private function _query($query)
509    {
510        $db = $this->_getConnection();
511        if (is_resource($db)) {
512            $res = @sqlite_query($db, $query);
513            if ($res === false) {
514                return false;
515            } else {
516                return $res;
517            }
518        }
519        return false;
520    }
521
522    /**
523     * Deal with the automatic vacuum process
524     *
525     * @return void
526     */
527    private function _automaticVacuum()
528    {
529        if ($this->_options['automatic_vacuum_factor'] > 0) {
530            $rand = rand(1, $this->_options['automatic_vacuum_factor']);
531            if ($rand == 1) {
532                $this->_query('VACUUM');
533                @sqlite_close($this->_getConnection());
534            }
535        }
536    }
537
538    /**
539     * Register a cache id with the given tag
540     *
541     * @param  string $id  Cache id
542     * @param  string $tag Tag
543     * @return boolean True if no problem
544     */
545    private function _registerTag($id, $tag) {
546        $res = $this->_query("DELETE FROM TAG WHERE name='$tag' AND id='$id'");
547        $res = $this->_query("INSERT INTO tag (name, id) VALUES ('$tag', '$id')");
548        if (!$res) {
549            $this->_log("Zend_Cache_Backend_Sqlite::_registerTag() : impossible to register tag=$tag on id=$id");
550            return false;
551        }
552        return true;
553    }
554
555    /**
556     * Build the database structure
557     *
558     * @return false
559     */
560    private function _buildStructure()
561    {
562        $this->_query('DROP INDEX tag_id_index');
563        $this->_query('DROP INDEX tag_name_index');
564        $this->_query('DROP INDEX cache_id_expire_index');
565        $this->_query('DROP TABLE version');
566        $this->_query('DROP TABLE cache');
567        $this->_query('DROP TABLE tag');
568        $this->_query('CREATE TABLE version (num INTEGER PRIMARY KEY)');
569        $this->_query('CREATE TABLE cache (id TEXT PRIMARY KEY, content BLOB, lastModified INTEGER, expire INTEGER)');
570        $this->_query('CREATE TABLE tag (name TEXT, id TEXT)');
571        $this->_query('CREATE INDEX tag_id_index ON tag(id)');
572        $this->_query('CREATE INDEX tag_name_index ON tag(name)');
573        $this->_query('CREATE INDEX cache_id_expire_index ON cache(id, expire)');
574        $this->_query('INSERT INTO version (num) VALUES (1)');
575    }
576
577    /**
578     * Check if the database structure is ok (with the good version)
579     *
580     * @return boolean True if ok
581     */
582    private function _checkStructureVersion()
583    {
584        $result = $this->_query("SELECT num FROM version");
585        if (!$result) return false;
586        $row = @sqlite_fetch_array($result);
587        if (!$row) {
588            return false;
589        }
590        if (((int) $row['num']) != 1) {
591            // old cache structure
592            $this->_log('Zend_Cache_Backend_Sqlite::_checkStructureVersion() : old cache structure version detected => the cache is going to be dropped');
593            return false;
594        }
595        return true;
596    }
597
598    /**
599     * Clean some cache records
600     *
601     * Available modes are :
602     * Zend_Cache::CLEANING_MODE_ALL (default)    => remove all cache entries ($tags is not used)
603     * Zend_Cache::CLEANING_MODE_OLD              => remove too old cache entries ($tags is not used)
604     * Zend_Cache::CLEANING_MODE_MATCHING_TAG     => remove cache entries matching all given tags
605     *                                               ($tags can be an array of strings or a single string)
606     * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
607     *                                               ($tags can be an array of strings or a single string)
608     * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
609     *                                               ($tags can be an array of strings or a single string)
610     *
611     * @param  string $mode Clean mode
612     * @param  array  $tags Array of tags
613     * @return boolean True if no problem
614     */
615    private function _clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array())
616    {
617        switch ($mode) {
618            case Zend_Cache::CLEANING_MODE_ALL:
619                $res1 = $this->_query('DELETE FROM cache');
620                $res2 = $this->_query('DELETE FROM tag');
621                return $res1 && $res2;
622                break;
623            case Zend_Cache::CLEANING_MODE_OLD:
624                $mktime = time();
625                $res1 = $this->_query("DELETE FROM tag WHERE id IN (SELECT id FROM cache WHERE expire>0 AND expire<=$mktime)");
626                $res2 = $this->_query("DELETE FROM cache WHERE expire>0 AND expire<=$mktime");
627                return $res1 && $res2;
628                break;
629            case Zend_Cache::CLEANING_MODE_MATCHING_TAG:
630                $ids = $this->getIdsMatchingTags($tags);
631                $result = true;
632                foreach ($ids as $id) {
633                    $result = $this->remove($id) && $result;
634                }
635                return $result;
636                break;
637            case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
638                $ids = $this->getIdsNotMatchingTags($tags);
639                $result = true;
640                foreach ($ids as $id) {
641                    $result = $this->remove($id) && $result;
642                }
643                return $result;
644                break;
645            case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
646                $ids = $this->getIdsMatchingAnyTags($tags);
647                $result = true;
648                foreach ($ids as $id) {
649                    $result = $this->remove($id) && $result;
650                }
651                return $result;
652                break;
653            default:
654                break;
655        }
656        return false;
657    }
658
659    /**
660     * Check if the database structure is ok (with the good version), if no : build it
661     *
662     * @throws Zend_Cache_Exception
663     * @return boolean True if ok
664     */
665    private function _checkAndBuildStructure()
666    {
667        if (!($this->_structureChecked)) {
668            if (!$this->_checkStructureVersion()) {
669                $this->_buildStructure();
670                if (!$this->_checkStructureVersion()) {
671                    Zend_Cache::throwException("Impossible to build cache structure in " . $this->_options['cache_db_complete_path']);
672                }
673            }
674            $this->_structureChecked = true;
675        }
676        return true;
677    }
678
679}