|
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: TwoLevels.php 22736 2010-07-30 16:25:54Z andyfowler $ |
|
21 */ |
|
22 |
|
23 |
|
24 /** |
|
25 * @see Zend_Cache_Backend_ExtendedInterface |
|
26 */ |
|
27 require_once 'Zend/Cache/Backend/ExtendedInterface.php'; |
|
28 |
|
29 /** |
|
30 * @see Zend_Cache_Backend |
|
31 */ |
|
32 require_once 'Zend/Cache/Backend.php'; |
|
33 |
|
34 |
|
35 /** |
|
36 * @package Zend_Cache |
|
37 * @subpackage Zend_Cache_Backend |
|
38 * @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) |
|
39 * @license http://framework.zend.com/license/new-bsd New BSD License |
|
40 */ |
|
41 |
|
42 class Zend_Cache_Backend_TwoLevels extends Zend_Cache_Backend implements Zend_Cache_Backend_ExtendedInterface |
|
43 { |
|
44 /** |
|
45 * Available options |
|
46 * |
|
47 * =====> (string) slow_backend : |
|
48 * - Slow backend name |
|
49 * - Must implement the Zend_Cache_Backend_ExtendedInterface |
|
50 * - Should provide a big storage |
|
51 * |
|
52 * =====> (string) fast_backend : |
|
53 * - Flow backend name |
|
54 * - Must implement the Zend_Cache_Backend_ExtendedInterface |
|
55 * - Must be much faster than slow_backend |
|
56 * |
|
57 * =====> (array) slow_backend_options : |
|
58 * - Slow backend options (see corresponding backend) |
|
59 * |
|
60 * =====> (array) fast_backend_options : |
|
61 * - Fast backend options (see corresponding backend) |
|
62 * |
|
63 * =====> (int) stats_update_factor : |
|
64 * - Disable / Tune the computation of the fast backend filling percentage |
|
65 * - When saving a record into cache : |
|
66 * 1 => systematic computation of the fast backend filling percentage |
|
67 * x (integer) > 1 => computation of the fast backend filling percentage randomly 1 times on x cache write |
|
68 * |
|
69 * =====> (boolean) slow_backend_custom_naming : |
|
70 * =====> (boolean) fast_backend_custom_naming : |
|
71 * =====> (boolean) slow_backend_autoload : |
|
72 * =====> (boolean) fast_backend_autoload : |
|
73 * - See Zend_Cache::factory() method |
|
74 * |
|
75 * =====> (boolean) auto_refresh_fast_cache |
|
76 * - If true, auto refresh the fast cache when a cache record is hit |
|
77 * |
|
78 * @var array available options |
|
79 */ |
|
80 protected $_options = array( |
|
81 'slow_backend' => 'File', |
|
82 'fast_backend' => 'Apc', |
|
83 'slow_backend_options' => array(), |
|
84 'fast_backend_options' => array(), |
|
85 'stats_update_factor' => 10, |
|
86 'slow_backend_custom_naming' => false, |
|
87 'fast_backend_custom_naming' => false, |
|
88 'slow_backend_autoload' => false, |
|
89 'fast_backend_autoload' => false, |
|
90 'auto_refresh_fast_cache' => true |
|
91 ); |
|
92 |
|
93 /** |
|
94 * Slow Backend |
|
95 * |
|
96 * @var Zend_Cache_Backend_ExtendedInterface |
|
97 */ |
|
98 protected $_slowBackend; |
|
99 |
|
100 /** |
|
101 * Fast Backend |
|
102 * |
|
103 * @var Zend_Cache_Backend_ExtendedInterface |
|
104 */ |
|
105 protected $_fastBackend; |
|
106 |
|
107 /** |
|
108 * Cache for the fast backend filling percentage |
|
109 * |
|
110 * @var int |
|
111 */ |
|
112 protected $_fastBackendFillingPercentage = null; |
|
113 |
|
114 /** |
|
115 * Constructor |
|
116 * |
|
117 * @param array $options Associative array of options |
|
118 * @throws Zend_Cache_Exception |
|
119 * @return void |
|
120 */ |
|
121 public function __construct(array $options = array()) |
|
122 { |
|
123 parent::__construct($options); |
|
124 |
|
125 if ($this->_options['slow_backend'] === null) { |
|
126 Zend_Cache::throwException('slow_backend option has to set'); |
|
127 } elseif ($this->_options['slow_backend'] instanceof Zend_Cache_Backend_ExtendedInterface) { |
|
128 $this->_slowBackend = $this->_options['slow_backend']; |
|
129 } else { |
|
130 $this->_slowBackend = Zend_Cache::_makeBackend( |
|
131 $this->_options['slow_backend'], |
|
132 $this->_options['slow_backend_options'], |
|
133 $this->_options['slow_backend_custom_naming'], |
|
134 $this->_options['slow_backend_autoload'] |
|
135 ); |
|
136 if (!in_array('Zend_Cache_Backend_ExtendedInterface', class_implements($this->_slowBackend))) { |
|
137 Zend_Cache::throwException('slow_backend must implement the Zend_Cache_Backend_ExtendedInterface interface'); |
|
138 } |
|
139 } |
|
140 |
|
141 if ($this->_options['fast_backend'] === null) { |
|
142 Zend_Cache::throwException('fast_backend option has to set'); |
|
143 } elseif ($this->_options['fast_backend'] instanceof Zend_Cache_Backend_ExtendedInterface) { |
|
144 $this->_fastBackend = $this->_options['fast_backend']; |
|
145 } else { |
|
146 $this->_fastBackend = Zend_Cache::_makeBackend( |
|
147 $this->_options['fast_backend'], |
|
148 $this->_options['fast_backend_options'], |
|
149 $this->_options['fast_backend_custom_naming'], |
|
150 $this->_options['fast_backend_autoload'] |
|
151 ); |
|
152 if (!in_array('Zend_Cache_Backend_ExtendedInterface', class_implements($this->_fastBackend))) { |
|
153 Zend_Cache::throwException('fast_backend must implement the Zend_Cache_Backend_ExtendedInterface interface'); |
|
154 } |
|
155 } |
|
156 |
|
157 $this->_slowBackend->setDirectives($this->_directives); |
|
158 $this->_fastBackend->setDirectives($this->_directives); |
|
159 } |
|
160 |
|
161 /** |
|
162 * Test if a cache is available or not (for the given id) |
|
163 * |
|
164 * @param string $id cache id |
|
165 * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record |
|
166 */ |
|
167 public function test($id) |
|
168 { |
|
169 $fastTest = $this->_fastBackend->test($id); |
|
170 if ($fastTest) { |
|
171 return $fastTest; |
|
172 } else { |
|
173 return $this->_slowBackend->test($id); |
|
174 } |
|
175 } |
|
176 |
|
177 /** |
|
178 * Save some string datas into a cache record |
|
179 * |
|
180 * Note : $data is always "string" (serialization is done by the |
|
181 * core not by the backend) |
|
182 * |
|
183 * @param string $data Datas to cache |
|
184 * @param string $id Cache id |
|
185 * @param array $tags Array of strings, the cache record will be tagged by each string entry |
|
186 * @param int $specificLifetime If != false, set a specific lifetime for this cache record (null => infinite lifetime) |
|
187 * @param int $priority integer between 0 (very low priority) and 10 (maximum priority) used by some particular backends |
|
188 * @return boolean true if no problem |
|
189 */ |
|
190 public function save($data, $id, $tags = array(), $specificLifetime = false, $priority = 8) |
|
191 { |
|
192 $usage = $this->_getFastFillingPercentage('saving'); |
|
193 $boolFast = true; |
|
194 $lifetime = $this->getLifetime($specificLifetime); |
|
195 $preparedData = $this->_prepareData($data, $lifetime, $priority); |
|
196 if (($priority > 0) && (10 * $priority >= $usage)) { |
|
197 $fastLifetime = $this->_getFastLifetime($lifetime, $priority); |
|
198 $boolFast = $this->_fastBackend->save($preparedData, $id, array(), $fastLifetime); |
|
199 $boolSlow = $this->_slowBackend->save($preparedData, $id, $tags, $lifetime); |
|
200 } else { |
|
201 $boolSlow = $this->_slowBackend->save($preparedData, $id, $tags, $lifetime); |
|
202 if ($boolSlow === true) { |
|
203 $boolFast = $this->_fastBackend->remove($id); |
|
204 if (!$boolFast && !$this->_fastBackend->test($id)) { |
|
205 // some backends return false on remove() even if the key never existed. (and it won't if fast is full) |
|
206 // all we care about is that the key doesn't exist now |
|
207 $boolFast = true; |
|
208 } |
|
209 } |
|
210 } |
|
211 |
|
212 return ($boolFast && $boolSlow); |
|
213 } |
|
214 |
|
215 /** |
|
216 * Test if a cache is available for the given id and (if yes) return it (false else) |
|
217 * |
|
218 * Note : return value is always "string" (unserialization is done by the core not by the backend) |
|
219 * |
|
220 * @param string $id Cache id |
|
221 * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested |
|
222 * @return string|false cached datas |
|
223 */ |
|
224 public function load($id, $doNotTestCacheValidity = false) |
|
225 { |
|
226 $res = $this->_fastBackend->load($id, $doNotTestCacheValidity); |
|
227 if ($res === false) { |
|
228 $res = $this->_slowBackend->load($id, $doNotTestCacheValidity); |
|
229 if ($res === false) { |
|
230 // there is no cache at all for this id |
|
231 return false; |
|
232 } |
|
233 } |
|
234 $array = unserialize($res); |
|
235 // maybe, we have to refresh the fast cache ? |
|
236 if ($this->_options['auto_refresh_fast_cache']) { |
|
237 if ($array['priority'] == 10) { |
|
238 // no need to refresh the fast cache with priority = 10 |
|
239 return $array['data']; |
|
240 } |
|
241 $newFastLifetime = $this->_getFastLifetime($array['lifetime'], $array['priority'], time() - $array['expire']); |
|
242 // we have the time to refresh the fast cache |
|
243 $usage = $this->_getFastFillingPercentage('loading'); |
|
244 if (($array['priority'] > 0) && (10 * $array['priority'] >= $usage)) { |
|
245 // we can refresh the fast cache |
|
246 $preparedData = $this->_prepareData($array['data'], $array['lifetime'], $array['priority']); |
|
247 $this->_fastBackend->save($preparedData, $id, array(), $newFastLifetime); |
|
248 } |
|
249 } |
|
250 return $array['data']; |
|
251 } |
|
252 |
|
253 /** |
|
254 * Remove a cache record |
|
255 * |
|
256 * @param string $id Cache id |
|
257 * @return boolean True if no problem |
|
258 */ |
|
259 public function remove($id) |
|
260 { |
|
261 $boolFast = $this->_fastBackend->remove($id); |
|
262 $boolSlow = $this->_slowBackend->remove($id); |
|
263 return $boolFast && $boolSlow; |
|
264 } |
|
265 |
|
266 /** |
|
267 * Clean some cache records |
|
268 * |
|
269 * Available modes are : |
|
270 * Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used) |
|
271 * Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used) |
|
272 * Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags |
|
273 * ($tags can be an array of strings or a single string) |
|
274 * Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags} |
|
275 * ($tags can be an array of strings or a single string) |
|
276 * Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags |
|
277 * ($tags can be an array of strings or a single string) |
|
278 * |
|
279 * @param string $mode Clean mode |
|
280 * @param array $tags Array of tags |
|
281 * @throws Zend_Cache_Exception |
|
282 * @return boolean true if no problem |
|
283 */ |
|
284 public function clean($mode = Zend_Cache::CLEANING_MODE_ALL, $tags = array()) |
|
285 { |
|
286 switch($mode) { |
|
287 case Zend_Cache::CLEANING_MODE_ALL: |
|
288 $boolFast = $this->_fastBackend->clean(Zend_Cache::CLEANING_MODE_ALL); |
|
289 $boolSlow = $this->_slowBackend->clean(Zend_Cache::CLEANING_MODE_ALL); |
|
290 return $boolFast && $boolSlow; |
|
291 break; |
|
292 case Zend_Cache::CLEANING_MODE_OLD: |
|
293 return $this->_slowBackend->clean(Zend_Cache::CLEANING_MODE_OLD); |
|
294 case Zend_Cache::CLEANING_MODE_MATCHING_TAG: |
|
295 $ids = $this->_slowBackend->getIdsMatchingTags($tags); |
|
296 $res = true; |
|
297 foreach ($ids as $id) { |
|
298 $bool = $this->remove($id); |
|
299 $res = $res && $bool; |
|
300 } |
|
301 return $res; |
|
302 break; |
|
303 case Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG: |
|
304 $ids = $this->_slowBackend->getIdsNotMatchingTags($tags); |
|
305 $res = true; |
|
306 foreach ($ids as $id) { |
|
307 $bool = $this->remove($id); |
|
308 $res = $res && $bool; |
|
309 } |
|
310 return $res; |
|
311 break; |
|
312 case Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG: |
|
313 $ids = $this->_slowBackend->getIdsMatchingAnyTags($tags); |
|
314 $res = true; |
|
315 foreach ($ids as $id) { |
|
316 $bool = $this->remove($id); |
|
317 $res = $res && $bool; |
|
318 } |
|
319 return $res; |
|
320 break; |
|
321 default: |
|
322 Zend_Cache::throwException('Invalid mode for clean() method'); |
|
323 break; |
|
324 } |
|
325 } |
|
326 |
|
327 /** |
|
328 * Return an array of stored cache ids |
|
329 * |
|
330 * @return array array of stored cache ids (string) |
|
331 */ |
|
332 public function getIds() |
|
333 { |
|
334 return $this->_slowBackend->getIds(); |
|
335 } |
|
336 |
|
337 /** |
|
338 * Return an array of stored tags |
|
339 * |
|
340 * @return array array of stored tags (string) |
|
341 */ |
|
342 public function getTags() |
|
343 { |
|
344 return $this->_slowBackend->getTags(); |
|
345 } |
|
346 |
|
347 /** |
|
348 * Return an array of stored cache ids which match given tags |
|
349 * |
|
350 * In case of multiple tags, a logical AND is made between tags |
|
351 * |
|
352 * @param array $tags array of tags |
|
353 * @return array array of matching cache ids (string) |
|
354 */ |
|
355 public function getIdsMatchingTags($tags = array()) |
|
356 { |
|
357 return $this->_slowBackend->getIdsMatchingTags($tags); |
|
358 } |
|
359 |
|
360 /** |
|
361 * Return an array of stored cache ids which don't match given tags |
|
362 * |
|
363 * In case of multiple tags, a logical OR is made between tags |
|
364 * |
|
365 * @param array $tags array of tags |
|
366 * @return array array of not matching cache ids (string) |
|
367 */ |
|
368 public function getIdsNotMatchingTags($tags = array()) |
|
369 { |
|
370 return $this->_slowBackend->getIdsNotMatchingTags($tags); |
|
371 } |
|
372 |
|
373 /** |
|
374 * Return an array of stored cache ids which match any given tags |
|
375 * |
|
376 * In case of multiple tags, a logical AND is made between tags |
|
377 * |
|
378 * @param array $tags array of tags |
|
379 * @return array array of any matching cache ids (string) |
|
380 */ |
|
381 public function getIdsMatchingAnyTags($tags = array()) |
|
382 { |
|
383 return $this->_slowBackend->getIdsMatchingAnyTags($tags); |
|
384 } |
|
385 |
|
386 |
|
387 /** |
|
388 * Return the filling percentage of the backend storage |
|
389 * |
|
390 * @return int integer between 0 and 100 |
|
391 */ |
|
392 public function getFillingPercentage() |
|
393 { |
|
394 return $this->_slowBackend->getFillingPercentage(); |
|
395 } |
|
396 |
|
397 /** |
|
398 * Return an array of metadatas for the given cache id |
|
399 * |
|
400 * The array must include these keys : |
|
401 * - expire : the expire timestamp |
|
402 * - tags : a string array of tags |
|
403 * - mtime : timestamp of last modification time |
|
404 * |
|
405 * @param string $id cache id |
|
406 * @return array array of metadatas (false if the cache id is not found) |
|
407 */ |
|
408 public function getMetadatas($id) |
|
409 { |
|
410 return $this->_slowBackend->getMetadatas($id); |
|
411 } |
|
412 |
|
413 /** |
|
414 * Give (if possible) an extra lifetime to the given cache id |
|
415 * |
|
416 * @param string $id cache id |
|
417 * @param int $extraLifetime |
|
418 * @return boolean true if ok |
|
419 */ |
|
420 public function touch($id, $extraLifetime) |
|
421 { |
|
422 return $this->_slowBackend->touch($id, $extraLifetime); |
|
423 } |
|
424 |
|
425 /** |
|
426 * Return an associative array of capabilities (booleans) of the backend |
|
427 * |
|
428 * The array must include these keys : |
|
429 * - automatic_cleaning (is automating cleaning necessary) |
|
430 * - tags (are tags supported) |
|
431 * - expired_read (is it possible to read expired cache records |
|
432 * (for doNotTestCacheValidity option for example)) |
|
433 * - priority does the backend deal with priority when saving |
|
434 * - infinite_lifetime (is infinite lifetime can work with this backend) |
|
435 * - get_list (is it possible to get the list of cache ids and the complete list of tags) |
|
436 * |
|
437 * @return array associative of with capabilities |
|
438 */ |
|
439 public function getCapabilities() |
|
440 { |
|
441 $slowBackendCapabilities = $this->_slowBackend->getCapabilities(); |
|
442 return array( |
|
443 'automatic_cleaning' => $slowBackendCapabilities['automatic_cleaning'], |
|
444 'tags' => $slowBackendCapabilities['tags'], |
|
445 'expired_read' => $slowBackendCapabilities['expired_read'], |
|
446 'priority' => $slowBackendCapabilities['priority'], |
|
447 'infinite_lifetime' => $slowBackendCapabilities['infinite_lifetime'], |
|
448 'get_list' => $slowBackendCapabilities['get_list'] |
|
449 ); |
|
450 } |
|
451 |
|
452 /** |
|
453 * Prepare a serialized array to store datas and metadatas informations |
|
454 * |
|
455 * @param string $data data to store |
|
456 * @param int $lifetime original lifetime |
|
457 * @param int $priority priority |
|
458 * @return string serialize array to store into cache |
|
459 */ |
|
460 private function _prepareData($data, $lifetime, $priority) |
|
461 { |
|
462 $lt = $lifetime; |
|
463 if ($lt === null) { |
|
464 $lt = 9999999999; |
|
465 } |
|
466 return serialize(array( |
|
467 'data' => $data, |
|
468 'lifetime' => $lifetime, |
|
469 'expire' => time() + $lt, |
|
470 'priority' => $priority |
|
471 )); |
|
472 } |
|
473 |
|
474 /** |
|
475 * Compute and return the lifetime for the fast backend |
|
476 * |
|
477 * @param int $lifetime original lifetime |
|
478 * @param int $priority priority |
|
479 * @param int $maxLifetime maximum lifetime |
|
480 * @return int lifetime for the fast backend |
|
481 */ |
|
482 private function _getFastLifetime($lifetime, $priority, $maxLifetime = null) |
|
483 { |
|
484 if ($lifetime === null) { |
|
485 // if lifetime is null, we have an infinite lifetime |
|
486 // we need to use arbitrary lifetimes |
|
487 $fastLifetime = (int) (2592000 / (11 - $priority)); |
|
488 } else { |
|
489 $fastLifetime = (int) ($lifetime / (11 - $priority)); |
|
490 } |
|
491 if (($maxLifetime !== null) && ($maxLifetime >= 0)) { |
|
492 if ($fastLifetime > $maxLifetime) { |
|
493 return $maxLifetime; |
|
494 } |
|
495 } |
|
496 return $fastLifetime; |
|
497 } |
|
498 |
|
499 /** |
|
500 * PUBLIC METHOD FOR UNIT TESTING ONLY ! |
|
501 * |
|
502 * Force a cache record to expire |
|
503 * |
|
504 * @param string $id cache id |
|
505 */ |
|
506 public function ___expire($id) |
|
507 { |
|
508 $this->_fastBackend->remove($id); |
|
509 $this->_slowBackend->___expire($id); |
|
510 } |
|
511 |
|
512 private function _getFastFillingPercentage($mode) |
|
513 { |
|
514 |
|
515 if ($mode == 'saving') { |
|
516 // mode saving |
|
517 if ($this->_fastBackendFillingPercentage === null) { |
|
518 $this->_fastBackendFillingPercentage = $this->_fastBackend->getFillingPercentage(); |
|
519 } else { |
|
520 $rand = rand(1, $this->_options['stats_update_factor']); |
|
521 if ($rand == 1) { |
|
522 // we force a refresh |
|
523 $this->_fastBackendFillingPercentage = $this->_fastBackend->getFillingPercentage(); |
|
524 } |
|
525 } |
|
526 } else { |
|
527 // mode loading |
|
528 // we compute the percentage only if it's not available in cache |
|
529 if ($this->_fastBackendFillingPercentage === null) { |
|
530 $this->_fastBackendFillingPercentage = $this->_fastBackend->getFillingPercentage(); |
|
531 } |
|
532 } |
|
533 return $this->_fastBackendFillingPercentage; |
|
534 } |
|
535 |
|
536 } |