Cache.php 11.4 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
<?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
19
 * <http://www.phpdoctrine.org>.
zYne's avatar
zYne committed
20
 */
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
    protected $_options = array('size'                  => 1000,
39 40 41 42 43 44
        'lifeTime'              => 3600,
        'addStatsPropability'   => 0.25,
        'savePropability'       => 0.10,
        'cleanPropability'      => 0.01,
        'statsFile'             => '../data/stats.cache',
    );
45

zYne's avatar
zYne committed
46 47 48
    /**
     * @var array $_queries                         query stack
     */
zYne's avatar
zYne committed
49
    protected $_queries = array();
50

zYne's avatar
zYne committed
51 52 53
    /**
     * @var Doctrine_Cache_Interface $_driver       the cache driver object
     */
zYne's avatar
zYne committed
54
    protected $_driver;
55

zYne's avatar
zYne committed
56 57 58
    /**
     * @var array $data                             current cache data array
     */
59
    protected $_data = array();
60

zYne's avatar
zYne committed
61 62 63 64
    /**
     * @var boolean $success                        the success of last operation
     */
    protected $_success = false;
65

zYne's avatar
zYne committed
66 67 68 69 70 71 72 73
    /**
     * 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())
    {
74
        if (is_object($driver)) {
75 76 77 78 79 80
            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
81 82
        } else {
            $class = 'Doctrine_Cache_' . ucwords(strtolower($driver));
83

zYne's avatar
zYne committed
84 85 86
            if ( ! class_exists($class)) {
                throw new Doctrine_Cache_Exception('Cache driver ' . $driver . ' could not be found.');
            }
87

zYne's avatar
zYne committed
88 89 90
            $this->_driver = new $class($options);
        }
    }
91

zYne's avatar
zYne committed
92 93 94 95 96 97 98
    /**
     * getDriver
     * returns the current cache driver
     *
     * @return Doctrine_Cache_Driver
     */
    public function getDriver()
zYne's avatar
zYne committed
99
    {
zYne's avatar
zYne committed
100 101
        return $this->_driver;
    }
102

zYne's avatar
zYne committed
103 104 105 106 107 108 109 110 111
    /**
     * 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)
    {
112 113
        // sanity check (we need this since we are using isset() instead of array_key_exists())
        if ($value === null) {
zYne's avatar
zYne committed
114
            throw new Doctrine_Cache_Exception('Null values not accepted for options.');
115
        }
zYne's avatar
zYne committed
116

117
        if (isset($this->_options[$option])) {
zYne's avatar
zYne committed
118 119
            $this->_options[$option] = $value;
            return true;
zYne's avatar
zYne committed
120
        }
zYne's avatar
zYne committed
121
        return false;
zYne's avatar
zYne committed
122
    }
123

zYne's avatar
zYne committed
124 125 126 127 128 129 130
    /**
     * getOption
     * 
     * @param mixed $option     the option name
     * @return mixed            option value
     */
    public function getOption($option)
zYne's avatar
zYne committed
131
    {
zYne's avatar
zYne committed
132 133 134 135 136
        if ( ! isset($this->_options[$option])) {
            throw new Doctrine_Cache_Exception('Unknown option ' . $option);
        }

        return $this->_options[$option];
zYne's avatar
zYne committed
137
    }
138

zYne's avatar
zYne committed
139
    /**
zYne's avatar
zYne committed
140 141
     * add
     * adds a query to internal query stack
zYne's avatar
zYne committed
142
     *
zYne's avatar
zYne committed
143 144
     * @param string|array $query           sql query string
     * @param string $namespace             connection namespace
zYne's avatar
zYne committed
145 146
     * @return void
     */
zYne's avatar
zYne committed
147 148
    public function add($query, $namespace = null)
    {
149
        if (isset($namespace)) {
zYne's avatar
zYne committed
150 151 152 153 154
            $this->_queries[$namespace][] = $query;
        } else {
            $this->_queries[] = $query;
        }
    }
155

zYne's avatar
zYne committed
156 157 158 159 160 161 162 163 164
    /**
     * getQueries
     *
     * @param string $namespace     optional query namespace
     * @return array                an array of sql query strings
     */
    public function getAll($namespace = null)
    {
        if (isset($namespace)) {
165
            if ( ! isset($this->_queries[$namespace])) {
zYne's avatar
zYne committed
166 167 168 169 170
                return array();
            }

            return $this->_queries[$namespace];
        }
171

zYne's avatar
zYne committed
172 173
        return $this->_queries;
    }
174

zYne's avatar
zYne committed
175 176 177 178 179 180 181
    /**
     * pop
     *
     * pops a query from the stack
     * @return string
     */
    public function pop()
zYne's avatar
zYne committed
182
    {
zYne's avatar
zYne committed
183 184
        return array_pop($this->_queries);
    }
185

zYne's avatar
zYne committed
186 187 188 189 190 191 192 193 194 195
    /**
     * reset
     *
     * removes all queries from the query stack
     * @return void
     */
    public function reset()
    {
        $this->_queries = array();
    }
196

zYne's avatar
zYne committed
197 198 199
    /**
     * count
     *
zYne's avatar
zYne committed
200
     * @return integer          the number of queries in the stack
zYne's avatar
zYne committed
201 202 203
     */
    public function count() 
    {
zYne's avatar
zYne committed
204 205
        return count($this->_queries);
    }
206

zYne's avatar
zYne committed
207 208 209 210 211 212 213 214
    /**
     * getIterator
     *
     * @return ArrayIterator    an iterator that iterates through the query stack
     */
    public function getIterator()
    {
        return new ArrayIterator($this->_queries);
zYne's avatar
zYne committed
215
    }
216

zYne's avatar
zYne committed
217 218 219 220 221 222 223
    /**
     * @return boolean          whether or not the last cache operation was successful
     */
    public function isSuccessful() 
    {
        return $this->_success;
    }
224

zYne's avatar
zYne committed
225 226 227 228 229
    /**
     * save
     *
     * @return boolean
     */
zYne's avatar
zYne committed
230
    public function clean()
zYne's avatar
zYne committed
231
    {
zYne's avatar
zYne committed
232
        $rand = (mt_rand() / mt_getrandmax());
zYne's avatar
zYne committed
233

234
        if ($rand <= $this->_options['cleanPropability']) {
zYne's avatar
zYne committed
235
            $queries = $this->readStats();
zYne's avatar
zYne committed
236

zYne's avatar
zYne committed
237
            $stats   = array();
238

zYne's avatar
zYne committed
239 240 241 242 243 244
            foreach ($queries as $query) {
                if (isset($stats[$query])) {
                    $stats[$query]++;
                } else {
                    $stats[$query] = 1;
                }
zYne's avatar
zYne committed
245
            }
zYne's avatar
zYne committed
246
            sort($stats);
247

zYne's avatar
zYne committed
248
            $i = $this->_options['size'];
249

zYne's avatar
zYne committed
250 251 252
            while ($i--) {
                $element = next($stats);
                $query   = key($stats);
zYne's avatar
zYne committed
253

zYne's avatar
zYne committed
254 255
                $hash = md5($query);

zYne's avatar
zYne committed
256
                $this->_driver->delete($hash);
zYne's avatar
zYne committed
257 258 259
            }
        }
    }
260

zYne's avatar
zYne committed
261 262 263 264 265 266 267
    /**
     * readStats
     *
     * @return array
     */
    public function readStats() 
    {
268
        if ($this->_options['statsFile'] !== false) {
269 270 271 272 273
            $content = file_get_contents($this->_options['statsFile']);

            $e = explode("\n", $content);

            return array_map('unserialize', $e);
274 275
        }
        return array();
zYne's avatar
zYne committed
276
    }
277

zYne's avatar
zYne committed
278
    /**
zYne's avatar
zYne committed
279
     * appendStats
zYne's avatar
zYne committed
280 281 282 283
     *
     * adds all queries to stats file
     * @return void
     */
zYne's avatar
zYne committed
284
    public function appendStats()
zYne's avatar
zYne committed
285
    {
286
        if ($this->_options['statsFile'] !== false) {
zYne's avatar
zYne committed
287

zYne's avatar
zYne committed
288 289 290
            if ( ! file_exists($this->_options['statsFile'])) {
                throw new Doctrine_Cache_Exception("Couldn't save cache statistics. Cache statistics file doesn't exists!");
            }
291

zYne's avatar
zYne committed
292
            $rand = (mt_rand() / mt_getrandmax());
zYne's avatar
zYne committed
293

zYne's avatar
zYne committed
294
            if ($rand <= $this->_options['addStatsPropability']) {
zYne's avatar
zYne committed
295
                file_put_contents($this->_options['statsFile'], implode("\n", array_map('serialize', $this->_queries)));
zYne's avatar
zYne committed
296
            }
zYne's avatar
zYne committed
297 298
        }
    }
299

zYne's avatar
zYne committed
300
    /**
301 302
     * preQuery
     * listens on the Doctrine_Event preQuery event
zYne's avatar
zYne committed
303 304 305 306 307 308
     *
     * adds the issued query to internal query stack
     * and checks if cached element exists
     *
     * @return boolean
     */
309
    public function preQuery(Doctrine_Event $event)
310
    {
zYne's avatar
zYne committed
311
        $query = $event->getQuery();
zYne's avatar
zYne committed
312 313

        $data  = false;
zYne's avatar
zYne committed
314
        // only process SELECT statements
315 316 317
        if (strtoupper(substr(ltrim($query), 0, 6)) != 'SELECT') {
            return false;
        }
zYne's avatar
zYne committed
318

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

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

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

325 326
        if ( ! $data) {
            $rand = (mt_rand() / mt_getrandmax());
zYne's avatar
zYne committed
327

328 329
            if ($rand < $this->_options['savePropability']) {
                $stmt = $event->getInvoker()->getAdapter()->query($query);
zYne's avatar
zYne committed
330

331
                $data = $stmt->fetchAll(Doctrine::FETCH_ASSOC);
zYne's avatar
zYne committed
332

333
                $this->success = true;
zYne's avatar
zYne committed
334

335
                $this->_driver->save(md5(serialize($query)), $data);
336
            }
zYne's avatar
zYne committed
337
        }
338 339 340 341 342
        if ($this->success)
        {
            $this->_data = $data;
            return true;
        }
343
        return false;
zYne's avatar
zYne committed
344
    }
345

zYne's avatar
zYne committed
346
    /**
347 348
     * preFetch
     * listens the preFetch event of Doctrine_Connection_Statement
zYne's avatar
zYne committed
349 350 351 352 353 354
     *
     * advances the internal pointer of cached data and returns 
     * the current element
     *
     * @return array
     */
355
    public function preFetch(Doctrine_Event $event)
zYne's avatar
zYne committed
356
    {
zYne's avatar
zYne committed
357
        $ret = current($this->_data);
358
        next($this->_data);
zYne's avatar
zYne committed
359
        return $ret;
zYne's avatar
zYne committed
360
    }
361

zYne's avatar
zYne committed
362
    /**
363 364
     * preFetch
     * listens the preFetchAll event of Doctrine_Connection_Statement
zYne's avatar
zYne committed
365 366 367 368 369
     *
     * returns the current cache data array
     *
     * @return array
     */
370
    public function preFetchAll(Doctrine_Event $event)
zYne's avatar
zYne committed
371 372 373
    {
        return $this->_data;
    }
374

zYne's avatar
zYne committed
375
    /**
376 377
     * preExecute
     * listens the preExecute event of Doctrine_Connection_Statement
zYne's avatar
zYne committed
378 379 380 381 382 383
     *
     * adds the issued query to internal query stack
     * and checks if cached element exists
     *
     * @return boolean
     */
384
    public function preExecute(Doctrine_Event $event)
zYne's avatar
zYne committed
385
    {
zYne's avatar
zYne committed
386
        $query = $event->getQuery();
zYne's avatar
zYne committed
387 388 389

        $data  = false;

zYne's avatar
zYne committed
390
        // only process SELECT statements
391 392 393
        if (strtoupper(substr(ltrim($query), 0, 6)) != 'SELECT') {
            return false;
        }
zYne's avatar
zYne committed
394

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

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

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

401 402
        if ( ! $data) {
            $rand = (mt_rand() / mt_getrandmax());
zYne's avatar
zYne committed
403

404
            if ($rand <= $this->_options['savePropability']) {
zYne's avatar
zYne committed
405

406
                $stmt = $event->getInvoker()->getStatement();
zYne's avatar
zYne committed
407

408
                $stmt->execute($event->getParams());
zYne's avatar
zYne committed
409

410
                $data = $stmt->fetchAll(Doctrine::FETCH_ASSOC);
zYne's avatar
zYne committed
411

412
                $this->success = true;
zYne's avatar
zYne committed
413

414
                $this->_driver->save(md5(serialize(array($query, $event->getParams()))), $data);
415
            }
zYne's avatar
zYne committed
416
        }
417 418 419 420 421
        if ($this->success)
        {
            $this->_data = $data;
            return true;
        }
422
        return false;
zYne's avatar
zYne committed
423
    }
zYne's avatar
zYne committed
424
}