Cache.php 11.7 KB
Newer Older
zYne's avatar
zYne committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
<?php
/*
 *  $Id$
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.phpdoctrine.com>.
 */
21
Doctrine::autoload('Doctrine_EventListener');
zYne's avatar
zYne committed
22 23 24 25
/**
 * Doctrine_Cache
 *
 * @package     Doctrine
26
 * @subpackage  Cache
zYne's avatar
zYne committed
27 28 29 30 31 32
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link        www.phpdoctrine.com
 * @since       1.0
 * @version     $Revision$
 */
33
class Doctrine_Cache extends Doctrine_EventListener implements Countable, IteratorAggregate
zYne's avatar
zYne committed
34
{
zYne's avatar
zYne committed
35 36 37
    /**
     * @var array $_options                         an array of general caching options
     */
zYne's avatar
zYne committed
38 39 40 41 42 43
    protected $_options = array('size'                  => 1000,
                                'lifeTime'              => 3600,
                                'addStatsPropability'   => 0.25,
                                'savePropability'       => 0.10,
                                'cleanPropability'      => 0.01,
                                'statsFile'             => '../data/stats.cache',
zYne's avatar
zYne committed
44
                                );
zYne's avatar
zYne committed
45 46 47
    /**
     * @var array $_queries                         query stack
     */
zYne's avatar
zYne committed
48
    protected $_queries = array();
zYne's avatar
zYne committed
49 50 51
    /**
     * @var Doctrine_Cache_Interface $_driver       the cache driver object
     */
zYne's avatar
zYne committed
52
    protected $_driver;
zYne's avatar
zYne committed
53 54 55
    /**
     * @var array $data                             current cache data array
     */
56
    protected $_data = array();
zYne's avatar
zYne committed
57 58 59 60
    /**
     * @var boolean $success                        the success of last operation
     */
    protected $_success = false;
zYne's avatar
zYne committed
61 62 63 64 65 66 67 68
    /**
     * constructor
     *
     * @param Doctrine_Cache_Interface|string $driver       cache driver name or a driver object
     * @param array $options                                cache driver options
     */
    public function __construct($driver, $options = array())
    {
69 70 71 72 73 74 75
        if (is_object($driver)) {
           if ( ! ($driver instanceof Doctrine_Cache_Interface)) {
               throw new Doctrine_Cache_Exception('Driver should implement Doctrine_Cache_Interface.');
           }
           
           $this->_driver = $driver;
           $this->_driver->setOptions($options);
zYne's avatar
zYne committed
76 77
        } else {
            $class = 'Doctrine_Cache_' . ucwords(strtolower($driver));
zYne's avatar
zYne committed
78
    
zYne's avatar
zYne committed
79 80 81 82 83 84 85 86 87 88 89 90 91 92
            if ( ! class_exists($class)) {
                throw new Doctrine_Cache_Exception('Cache driver ' . $driver . ' could not be found.');
            }
    
            $this->_driver = new $class($options);
        }
    }
    /**
     * getDriver
     * returns the current cache driver
     *
     * @return Doctrine_Cache_Driver
     */
    public function getDriver()
zYne's avatar
zYne committed
93
    {
zYne's avatar
zYne committed
94 95 96 97 98 99 100 101 102 103 104
        return $this->_driver;
    }
    /**
     * setOption
     *
     * @param mixed $option     the option name
     * @param mixed $value      option value
     * @return boolean          TRUE on success, FALSE on failure
     */
    public function setOption($option, $value)
    {
105 106
        // sanity check (we need this since we are using isset() instead of array_key_exists())
        if ($value === null) {
zYne's avatar
zYne committed
107
            throw new Doctrine_Cache_Exception('Null values not accepted for options.');
108
        }
zYne's avatar
zYne committed
109

110
        if (isset($this->_options[$option])) {
zYne's avatar
zYne committed
111 112
            $this->_options[$option] = $value;
            return true;
zYne's avatar
zYne committed
113
        }
zYne's avatar
zYne committed
114
        return false;
zYne's avatar
zYne committed
115
    }
zYne's avatar
zYne committed
116 117 118 119 120 121 122
    /**
     * getOption
     * 
     * @param mixed $option     the option name
     * @return mixed            option value
     */
    public function getOption($option)
zYne's avatar
zYne committed
123
    {
zYne's avatar
zYne committed
124 125 126 127 128
        if ( ! isset($this->_options[$option])) {
            throw new Doctrine_Cache_Exception('Unknown option ' . $option);
        }

        return $this->_options[$option];
zYne's avatar
zYne committed
129
    }
zYne's avatar
zYne committed
130
    /**
zYne's avatar
zYne committed
131 132
     * add
     * adds a query to internal query stack
zYne's avatar
zYne committed
133
     *
zYne's avatar
zYne committed
134 135
     * @param string|array $query           sql query string
     * @param string $namespace             connection namespace
zYne's avatar
zYne committed
136 137
     * @return void
     */
zYne's avatar
zYne committed
138 139
    public function add($query, $namespace = null)
    {
140
        if (isset($namespace)) {
zYne's avatar
zYne committed
141 142 143 144 145 146 147 148 149 150 151 152 153 154
            $this->_queries[$namespace][] = $query;
        } else {
            $this->_queries[] = $query;
        }
    }
    /**
     * getQueries
     *
     * @param string $namespace     optional query namespace
     * @return array                an array of sql query strings
     */
    public function getAll($namespace = null)
    {
        if (isset($namespace)) {
155
            if ( ! isset($this->_queries[$namespace])) {
zYne's avatar
zYne committed
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
                return array();
            }

            return $this->_queries[$namespace];
        }
        
        return $this->_queries;
    }
    /**
     * pop
     *
     * pops a query from the stack
     * @return string
     */
    public function pop()
zYne's avatar
zYne committed
171
    {
zYne's avatar
zYne committed
172 173
        return array_pop($this->_queries);
    }
zYne's avatar
zYne committed
174 175 176 177 178 179 180 181 182 183
    /**
     * reset
     *
     * removes all queries from the query stack
     * @return void
     */
    public function reset()
    {
        $this->_queries = array();
    }
zYne's avatar
zYne committed
184 185 186
    /**
     * count
     *
zYne's avatar
zYne committed
187
     * @return integer          the number of queries in the stack
zYne's avatar
zYne committed
188 189 190
     */
    public function count() 
    {
zYne's avatar
zYne committed
191 192 193 194 195 196 197 198 199 200
        return count($this->_queries);
    }
    /**
     * getIterator
     *
     * @return ArrayIterator    an iterator that iterates through the query stack
     */
    public function getIterator()
    {
        return new ArrayIterator($this->_queries);
zYne's avatar
zYne committed
201
    }
zYne's avatar
zYne committed
202 203 204 205 206 207 208
    /**
     * @return boolean          whether or not the last cache operation was successful
     */
    public function isSuccessful() 
    {
        return $this->_success;
    }
zYne's avatar
zYne committed
209 210 211 212 213
    /**
     * save
     *
     * @return boolean
     */
zYne's avatar
zYne committed
214
    public function clean()
zYne's avatar
zYne committed
215
    {
zYne's avatar
zYne committed
216
        $rand = (mt_rand() / mt_getrandmax());
zYne's avatar
zYne committed
217

218
        if ($rand <= $this->_options['cleanPropability']) {
zYne's avatar
zYne committed
219
            $queries = $this->readStats();
zYne's avatar
zYne committed
220

zYne's avatar
zYne committed
221 222 223 224 225 226 227 228
            $stats   = array();
    
            foreach ($queries as $query) {
                if (isset($stats[$query])) {
                    $stats[$query]++;
                } else {
                    $stats[$query] = 1;
                }
zYne's avatar
zYne committed
229
            }
zYne's avatar
zYne committed
230 231 232 233 234 235 236
            sort($stats);
    
            $i = $this->_options['size'];
    
            while ($i--) {
                $element = next($stats);
                $query   = key($stats);
zYne's avatar
zYne committed
237

zYne's avatar
zYne committed
238 239
                $hash = md5($query);

zYne's avatar
zYne committed
240
                $this->_driver->delete($hash);
zYne's avatar
zYne committed
241 242 243
            }
        }
    }
zYne's avatar
zYne committed
244 245 246 247 248 249 250
    /**
     * readStats
     *
     * @return array
     */
    public function readStats() 
    {
251 252
        if ($this->_options['statsFile'] !== false) {
           $content = file_get_contents($this->_options['statsFile']);
zYne's avatar
zYne committed
253 254 255 256
           
           $e = explode("\n", $content);
           
           return array_map('unserialize', $e);
257 258
        }
        return array();
zYne's avatar
zYne committed
259
    }
zYne's avatar
zYne committed
260
    /**
zYne's avatar
zYne committed
261
     * appendStats
zYne's avatar
zYne committed
262 263 264 265
     *
     * adds all queries to stats file
     * @return void
     */
zYne's avatar
zYne committed
266
    public function appendStats()
zYne's avatar
zYne committed
267
    {
268
        if ($this->_options['statsFile'] !== false) {
zYne's avatar
zYne committed
269

zYne's avatar
zYne committed
270 271 272
            if ( ! file_exists($this->_options['statsFile'])) {
                throw new Doctrine_Cache_Exception("Couldn't save cache statistics. Cache statistics file doesn't exists!");
            }
zYne's avatar
zYne committed
273 274
            
            $rand = (mt_rand() / mt_getrandmax());
zYne's avatar
zYne committed
275

zYne's avatar
zYne committed
276
            if ($rand <= $this->_options['addStatsPropability']) {
zYne's avatar
zYne committed
277
                file_put_contents($this->_options['statsFile'], implode("\n", array_map('serialize', $this->_queries)));
zYne's avatar
zYne committed
278
            }
zYne's avatar
zYne committed
279 280 281
        }
    }
    /**
282 283
     * preQuery
     * listens on the Doctrine_Event preQuery event
zYne's avatar
zYne committed
284 285 286 287 288 289
     *
     * adds the issued query to internal query stack
     * and checks if cached element exists
     *
     * @return boolean
     */
290
    public function preQuery(Doctrine_Event $event)
291
    {
zYne's avatar
zYne committed
292
        $query = $event->getQuery();
zYne's avatar
zYne committed
293 294

        $data  = false;
zYne's avatar
zYne committed
295
        // only process SELECT statements
296
        if (strtoupper(substr(ltrim($query), 0, 6)) == 'SELECT') {
zYne's avatar
zYne committed
297

zYne's avatar
zYne committed
298
            $this->add($query, $event->getInvoker()->getName());
zYne's avatar
zYne committed
299

zYne's avatar
zYne committed
300
            $data = $this->_driver->fetch(md5(serialize($query)));
zYne's avatar
zYne committed
301

zYne's avatar
zYne committed
302 303
            $this->success = ($data) ? true : false;

zYne's avatar
zYne committed
304
            if ( ! $data) {
zYne's avatar
zYne committed
305
                $rand = (mt_rand() / mt_getrandmax());
zYne's avatar
zYne committed
306 307

                if ($rand < $this->_options['savePropability']) {
zYne's avatar
zYne committed
308
                    $stmt = $event->getInvoker()->getAdapter()->query($query);
zYne's avatar
zYne committed
309 310 311 312 313

                    $data = $stmt->fetchAll(Doctrine::FETCH_ASSOC);

                    $this->success = true;

zYne's avatar
zYne committed
314
                    $this->_driver->save(md5(serialize($query)), $data);
zYne's avatar
zYne committed
315 316
                }
            }
317 318
            if ($this->success)
            {
319
                $this->_data = $data;
320 321
                return true;
            }
zYne's avatar
zYne committed
322
        }
323
        return false;
zYne's avatar
zYne committed
324
    }
zYne's avatar
zYne committed
325
    /**
326 327
     * preFetch
     * listens the preFetch event of Doctrine_Connection_Statement
zYne's avatar
zYne committed
328 329 330 331 332 333
     *
     * advances the internal pointer of cached data and returns 
     * the current element
     *
     * @return array
     */
334
    public function preFetch(Doctrine_Event $event)
zYne's avatar
zYne committed
335
    {
zYne's avatar
zYne committed
336
        $ret = current($this->_data);
337
        next($this->_data);
zYne's avatar
zYne committed
338
        return $ret;
zYne's avatar
zYne committed
339
    }
zYne's avatar
zYne committed
340
    /**
341 342
     * preFetch
     * listens the preFetchAll event of Doctrine_Connection_Statement
zYne's avatar
zYne committed
343 344 345 346 347
     *
     * returns the current cache data array
     *
     * @return array
     */
348
    public function preFetchAll(Doctrine_Event $event)
zYne's avatar
zYne committed
349 350 351
    {
        return $this->_data;
    }
zYne's avatar
zYne committed
352
    /**
353 354
     * preExecute
     * listens the preExecute event of Doctrine_Connection_Statement
zYne's avatar
zYne committed
355 356 357 358 359 360
     *
     * adds the issued query to internal query stack
     * and checks if cached element exists
     *
     * @return boolean
     */
361
    public function preExecute(Doctrine_Event $event)
zYne's avatar
zYne committed
362
    {
zYne's avatar
zYne committed
363
        $query = $event->getQuery();
zYne's avatar
zYne committed
364 365 366

        $data  = false;

zYne's avatar
zYne committed
367
        // only process SELECT statements
368
        if (strtoupper(substr(ltrim($query), 0, 6)) == 'SELECT') {
zYne's avatar
zYne committed
369

zYne's avatar
zYne committed
370
            $this->add($query, $event->getInvoker()->getDbh()->getName());
zYne's avatar
zYne committed
371

zYne's avatar
zYne committed
372 373
            $data = $this->_driver->fetch(md5(serialize(array($query, $event->getParams()))));

zYne's avatar
zYne committed
374 375
            $this->success = ($data) ? true : false;

zYne's avatar
zYne committed
376
            if ( ! $data) {
zYne's avatar
zYne committed
377
                $rand = (mt_rand() / mt_getrandmax());
zYne's avatar
zYne committed
378

zYne's avatar
zYne committed
379 380 381 382 383
                if ($rand <= $this->_options['savePropability']) {

                    $stmt = $event->getInvoker()->getStatement();

                    $stmt->execute($event->getParams());
zYne's avatar
zYne committed
384 385 386 387 388 389 390 391

                    $data = $stmt->fetchAll(Doctrine::FETCH_ASSOC);

                    $this->success = true;

                    $this->_driver->save(md5(serialize(array($query, $event->getParams()))), $data);
                }
            }
392 393
            if ($this->success)
            {
394
                $this->_data = $data;
395 396
                return true;
            }
zYne's avatar
zYne committed
397
        }
398
        return false;
zYne's avatar
zYne committed
399
    }
zYne's avatar
zYne committed
400
}