|
1 <?php |
|
2 /** |
|
3 * Smarty Internal Plugin |
|
4 * |
|
5 * @package Smarty |
|
6 * @subpackage Cacher |
|
7 */ |
|
8 |
|
9 /** |
|
10 * Smarty Cache Handler Base for Key/Value Storage Implementations |
|
11 * |
|
12 * This class implements the functionality required to use simple key/value stores |
|
13 * for hierarchical cache groups. key/value stores like memcache or APC do not support |
|
14 * wildcards in keys, therefore a cache group cannot be cleared like "a|*" - which |
|
15 * is no problem to filesystem and RDBMS implementations. |
|
16 * |
|
17 * This implementation is based on the concept of invalidation. While one specific cache |
|
18 * can be identified and cleared, any range of caches cannot be identified. For this reason |
|
19 * each level of the cache group hierarchy can have its own value in the store. These values |
|
20 * are nothing but microtimes, telling us when a particular cache group was cleared for the |
|
21 * last time. These keys are evaluated for every cache read to determine if the cache has |
|
22 * been invalidated since it was created and should hence be treated as inexistent. |
|
23 * |
|
24 * Although deep hierarchies are possible, they are not recommended. Try to keep your |
|
25 * cache groups as shallow as possible. Anything up 3-5 parents should be ok. So |
|
26 * »a|b|c« is a good depth where »a|b|c|d|e|f|g|h|i|j|k« isn't. Try to join correlating |
|
27 * cache groups: if your cache groups look somewhat like »a|b|$page|$items|$whatever« |
|
28 * consider using »a|b|c|$page-$items-$whatever« instead. |
|
29 * |
|
30 * @package Smarty |
|
31 * @subpackage Cacher |
|
32 * @author Rodney Rehm |
|
33 */ |
|
34 abstract class Smarty_CacheResource_KeyValueStore extends Smarty_CacheResource { |
|
35 |
|
36 /** |
|
37 * cache for contents |
|
38 * @var array |
|
39 */ |
|
40 protected $contents = array(); |
|
41 /** |
|
42 * cache for timestamps |
|
43 * @var array |
|
44 */ |
|
45 protected $timestamps = array(); |
|
46 |
|
47 /** |
|
48 * populate Cached Object with meta data from Resource |
|
49 * |
|
50 * @param Smarty_Template_Cached $cached cached object |
|
51 * @param Smarty_Internal_Template $_template template object |
|
52 * @return void |
|
53 */ |
|
54 public function populate(Smarty_Template_Cached $cached, Smarty_Internal_Template $_template) |
|
55 { |
|
56 $cached->filepath = $_template->source->uid |
|
57 . '#' . $this->sanitize($cached->source->name) |
|
58 . '#' . $this->sanitize($cached->cache_id) |
|
59 . '#' . $this->sanitize($cached->compile_id); |
|
60 |
|
61 $this->populateTimestamp($cached); |
|
62 } |
|
63 |
|
64 /** |
|
65 * populate Cached Object with timestamp and exists from Resource |
|
66 * |
|
67 * @param Smarty_Template_Cached $cached cached object |
|
68 * @return void |
|
69 */ |
|
70 public function populateTimestamp(Smarty_Template_Cached $cached) |
|
71 { |
|
72 if (!$this->fetch($cached->filepath, $cached->source->name, $cached->cache_id, $cached->compile_id, $content, $timestamp, $cached->source->uid)) { |
|
73 return; |
|
74 } |
|
75 $cached->content = $content; |
|
76 $cached->timestamp = (int) $timestamp; |
|
77 $cached->exists = $cached->timestamp; |
|
78 } |
|
79 |
|
80 /** |
|
81 * Read the cached template and process the header |
|
82 * |
|
83 * @param Smarty_Internal_Template $_template template object |
|
84 * @param Smarty_Template_Cached $cached cached object |
|
85 * @return booelan true or false if the cached content does not exist |
|
86 */ |
|
87 public function process(Smarty_Internal_Template $_template, Smarty_Template_Cached $cached=null) |
|
88 { |
|
89 if (!$cached) { |
|
90 $cached = $_template->cached; |
|
91 } |
|
92 $content = $cached->content ? $cached->content : null; |
|
93 $timestamp = $cached->timestamp ? $cached->timestamp : null; |
|
94 if ($content === null || !$timestamp) { |
|
95 if (!$this->fetch($_template->cached->filepath, $_template->source->name, $_template->cache_id, $_template->compile_id, $content, $timestamp, $_template->source->uid)) { |
|
96 return false; |
|
97 } |
|
98 } |
|
99 if (isset($content)) { |
|
100 $_smarty_tpl = $_template; |
|
101 eval("?>" . $content); |
|
102 return true; |
|
103 } |
|
104 return false; |
|
105 } |
|
106 |
|
107 /** |
|
108 * Write the rendered template output to cache |
|
109 * |
|
110 * @param Smarty_Internal_Template $_template template object |
|
111 * @param string $content content to cache |
|
112 * @return boolean success |
|
113 */ |
|
114 public function writeCachedContent(Smarty_Internal_Template $_template, $content) |
|
115 { |
|
116 $this->addMetaTimestamp($content); |
|
117 return $this->write(array($_template->cached->filepath => $content), $_template->properties['cache_lifetime']); |
|
118 } |
|
119 |
|
120 /** |
|
121 * Empty cache |
|
122 * |
|
123 * {@internal the $exp_time argument is ignored altogether }} |
|
124 * |
|
125 * @param Smarty $smarty Smarty object |
|
126 * @param integer $exp_time expiration time [being ignored] |
|
127 * @return integer number of cache files deleted [always -1] |
|
128 * @uses purge() to clear the whole store |
|
129 * @uses invalidate() to mark everything outdated if purge() is inapplicable |
|
130 */ |
|
131 public function clearAll(Smarty $smarty, $exp_time=null) |
|
132 { |
|
133 if (!$this->purge()) { |
|
134 $this->invalidate(null); |
|
135 } |
|
136 return -1; |
|
137 } |
|
138 |
|
139 /** |
|
140 * Empty cache for a specific template |
|
141 * |
|
142 * {@internal the $exp_time argument is ignored altogether}} |
|
143 * |
|
144 * @param Smarty $smarty Smarty object |
|
145 * @param string $resource_name template name |
|
146 * @param string $cache_id cache id |
|
147 * @param string $compile_id compile id |
|
148 * @param integer $exp_time expiration time [being ignored] |
|
149 * @return integer number of cache files deleted [always -1] |
|
150 * @uses buildCachedFilepath() to generate the CacheID |
|
151 * @uses invalidate() to mark CacheIDs parent chain as outdated |
|
152 * @uses delete() to remove CacheID from cache |
|
153 */ |
|
154 public function clear(Smarty $smarty, $resource_name, $cache_id, $compile_id, $exp_time) |
|
155 { |
|
156 $uid = $this->getTemplateUid($smarty, $resource_name, $cache_id, $compile_id); |
|
157 $cid = $uid . '#' . $this->sanitize($resource_name) . '#' . $this->sanitize($cache_id) . '#' . $this->sanitize($compile_id); |
|
158 $this->delete(array($cid)); |
|
159 $this->invalidate($cid, $resource_name, $cache_id, $compile_id, $uid); |
|
160 return -1; |
|
161 } |
|
162 /** |
|
163 * Get template's unique ID |
|
164 * |
|
165 * @param Smarty $smarty Smarty object |
|
166 * @param string $resource_name template name |
|
167 * @param string $cache_id cache id |
|
168 * @param string $compile_id compile id |
|
169 * @return string filepath of cache file |
|
170 */ |
|
171 protected function getTemplateUid(Smarty $smarty, $resource_name, $cache_id, $compile_id) |
|
172 { |
|
173 $uid = ''; |
|
174 if (isset($resource_name)) { |
|
175 $tpl = new $smarty->template_class($resource_name, $smarty); |
|
176 if ($tpl->source->exists) { |
|
177 $uid = $tpl->source->uid; |
|
178 } |
|
179 |
|
180 // remove from template cache |
|
181 if ($smarty->allow_ambiguous_resources) { |
|
182 $_templateId = $tpl->source->unique_resource . $tpl->cache_id . $tpl->compile_id; |
|
183 } else { |
|
184 $_templateId = $smarty->joined_template_dir . '#' . $resource_name . $tpl->cache_id . $tpl->compile_id; |
|
185 } |
|
186 if (isset($_templateId[150])) { |
|
187 $_templateId = sha1($_templateId); |
|
188 } |
|
189 unset($smarty->template_objects[$_templateId]); |
|
190 } |
|
191 return $uid; |
|
192 } |
|
193 |
|
194 /** |
|
195 * Sanitize CacheID components |
|
196 * |
|
197 * @param string $string CacheID component to sanitize |
|
198 * @return string sanitized CacheID component |
|
199 */ |
|
200 protected function sanitize($string) |
|
201 { |
|
202 // some poeple smoke bad weed |
|
203 $string = trim($string, '|'); |
|
204 if (!$string) { |
|
205 return null; |
|
206 } |
|
207 return preg_replace('#[^\w\|]+#S', '_', $string); |
|
208 } |
|
209 |
|
210 /** |
|
211 * Fetch and prepare a cache object. |
|
212 * |
|
213 * @param string $cid CacheID to fetch |
|
214 * @param string $resource_name template name |
|
215 * @param string $cache_id cache id |
|
216 * @param string $compile_id compile id |
|
217 * @param string $content cached content |
|
218 * @param integer &$timestamp cached timestamp (epoch) |
|
219 * @param string $resource_uid resource's uid |
|
220 * @return boolean success |
|
221 */ |
|
222 protected function fetch($cid, $resource_name = null, $cache_id = null, $compile_id = null, &$content = null, &$timestamp = null, $resource_uid = null) |
|
223 { |
|
224 $t = $this->read(array($cid)); |
|
225 $content = !empty($t[$cid]) ? $t[$cid] : null; |
|
226 $timestamp = null; |
|
227 |
|
228 if ($content && ($timestamp = $this->getMetaTimestamp($content))) { |
|
229 $invalidated = $this->getLatestInvalidationTimestamp($cid, $resource_name, $cache_id, $compile_id, $resource_uid); |
|
230 if ($invalidated > $timestamp) { |
|
231 $timestamp = null; |
|
232 $content = null; |
|
233 } |
|
234 } |
|
235 |
|
236 return !!$content; |
|
237 } |
|
238 |
|
239 /** |
|
240 * Add current microtime to the beginning of $cache_content |
|
241 * |
|
242 * {@internal the header uses 8 Bytes, the first 4 Bytes are the seconds, the second 4 Bytes are the microseconds}} |
|
243 * |
|
244 * @param string &$content the content to be cached |
|
245 */ |
|
246 protected function addMetaTimestamp(&$content) |
|
247 { |
|
248 $mt = explode(" ", microtime()); |
|
249 $ts = pack("NN", $mt[1], (int) ($mt[0] * 100000000)); |
|
250 $content = $ts . $content; |
|
251 } |
|
252 |
|
253 /** |
|
254 * Extract the timestamp the $content was cached |
|
255 * |
|
256 * @param string &$content the cached content |
|
257 * @return float the microtime the content was cached |
|
258 */ |
|
259 protected function getMetaTimestamp(&$content) |
|
260 { |
|
261 $s = unpack("N", substr($content, 0, 4)); |
|
262 $m = unpack("N", substr($content, 4, 4)); |
|
263 $content = substr($content, 8); |
|
264 return $s[1] + ($m[1] / 100000000); |
|
265 } |
|
266 |
|
267 /** |
|
268 * Invalidate CacheID |
|
269 * |
|
270 * @param string $cid CacheID |
|
271 * @param string $resource_name template name |
|
272 * @param string $cache_id cache id |
|
273 * @param string $compile_id compile id |
|
274 * @param string $resource_uid source's uid |
|
275 * @return void |
|
276 */ |
|
277 protected function invalidate($cid = null, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null) |
|
278 { |
|
279 $now = microtime(true); |
|
280 $key = null; |
|
281 // invalidate everything |
|
282 if (!$resource_name && !$cache_id && !$compile_id) { |
|
283 $key = 'IVK#ALL'; |
|
284 } |
|
285 // invalidate all caches by template |
|
286 else if ($resource_name && !$cache_id && !$compile_id) { |
|
287 $key = 'IVK#TEMPLATE#' . $resource_uid . '#' . $this->sanitize($resource_name); |
|
288 } |
|
289 // invalidate all caches by cache group |
|
290 else if (!$resource_name && $cache_id && !$compile_id) { |
|
291 $key = 'IVK#CACHE#' . $this->sanitize($cache_id); |
|
292 } |
|
293 // invalidate all caches by compile id |
|
294 else if (!$resource_name && !$cache_id && $compile_id) { |
|
295 $key = 'IVK#COMPILE#' . $this->sanitize($compile_id); |
|
296 } |
|
297 // invalidate by combination |
|
298 else { |
|
299 $key = 'IVK#CID#' . $cid; |
|
300 } |
|
301 $this->write(array($key => $now)); |
|
302 } |
|
303 |
|
304 /** |
|
305 * Determine the latest timestamp known to the invalidation chain |
|
306 * |
|
307 * @param string $cid CacheID to determine latest invalidation timestamp of |
|
308 * @param string $resource_name template name |
|
309 * @param string $cache_id cache id |
|
310 * @param string $compile_id compile id |
|
311 * @param string $resource_uid source's filepath |
|
312 * @return float the microtime the CacheID was invalidated |
|
313 */ |
|
314 protected function getLatestInvalidationTimestamp($cid, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null) |
|
315 { |
|
316 // abort if there is no CacheID |
|
317 if (false && !$cid) { |
|
318 return 0; |
|
319 } |
|
320 // abort if there are no InvalidationKeys to check |
|
321 if (!($_cid = $this->listInvalidationKeys($cid, $resource_name, $cache_id, $compile_id, $resource_uid))) { |
|
322 return 0; |
|
323 } |
|
324 |
|
325 // there are no InValidationKeys |
|
326 if (!($values = $this->read($_cid))) { |
|
327 return 0; |
|
328 } |
|
329 // make sure we're dealing with floats |
|
330 $values = array_map('floatval', $values); |
|
331 return max($values); |
|
332 } |
|
333 |
|
334 /** |
|
335 * Translate a CacheID into the list of applicable InvalidationKeys. |
|
336 * |
|
337 * Splits "some|chain|into|an|array" into array( '#clearAll#', 'some', 'some|chain', 'some|chain|into', ... ) |
|
338 * |
|
339 * @param string $cid CacheID to translate |
|
340 * @param string $resource_name template name |
|
341 * @param string $cache_id cache id |
|
342 * @param string $compile_id compile id |
|
343 * @param string $resource_uid source's filepath |
|
344 * @return array list of InvalidationKeys |
|
345 * @uses $invalidationKeyPrefix to prepend to each InvalidationKey |
|
346 */ |
|
347 protected function listInvalidationKeys($cid, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null) |
|
348 { |
|
349 $t = array('IVK#ALL'); |
|
350 $_name = $_compile = '#'; |
|
351 if ($resource_name) { |
|
352 $_name .= $resource_uid . '#' . $this->sanitize($resource_name); |
|
353 $t[] = 'IVK#TEMPLATE' . $_name; |
|
354 } |
|
355 if ($compile_id) { |
|
356 $_compile .= $this->sanitize($compile_id); |
|
357 $t[] = 'IVK#COMPILE' . $_compile; |
|
358 } |
|
359 $_name .= '#'; |
|
360 // some poeple smoke bad weed |
|
361 $cid = trim($cache_id, '|'); |
|
362 if (!$cid) { |
|
363 return $t; |
|
364 } |
|
365 $i = 0; |
|
366 while (true) { |
|
367 // determine next delimiter position |
|
368 $i = strpos($cid, '|', $i); |
|
369 // add complete CacheID if there are no more delimiters |
|
370 if ($i === false) { |
|
371 $t[] = 'IVK#CACHE#' . $cid; |
|
372 $t[] = 'IVK#CID' . $_name . $cid . $_compile; |
|
373 $t[] = 'IVK#CID' . $_name . $_compile; |
|
374 break; |
|
375 } |
|
376 $part = substr($cid, 0, $i); |
|
377 // add slice to list |
|
378 $t[] = 'IVK#CACHE#' . $part; |
|
379 $t[] = 'IVK#CID' . $_name . $part . $_compile; |
|
380 // skip past delimiter position |
|
381 $i++; |
|
382 } |
|
383 return $t; |
|
384 } |
|
385 |
|
386 /** |
|
387 * Check is cache is locked for this template |
|
388 * |
|
389 * @param Smarty $smarty Smarty object |
|
390 * @param Smarty_Template_Cached $cached cached object |
|
391 * @return booelan true or false if cache is locked |
|
392 */ |
|
393 public function hasLock(Smarty $smarty, Smarty_Template_Cached $cached) |
|
394 { |
|
395 $key = 'LOCK#' . $cached->filepath; |
|
396 $data = $this->read(array($key)); |
|
397 return $data && time() - $data[$key] < $smarty->locking_timeout; |
|
398 } |
|
399 |
|
400 /** |
|
401 * Lock cache for this template |
|
402 * |
|
403 * @param Smarty $smarty Smarty object |
|
404 * @param Smarty_Template_Cached $cached cached object |
|
405 */ |
|
406 public function acquireLock(Smarty $smarty, Smarty_Template_Cached $cached) |
|
407 { |
|
408 $cached->is_locked = true; |
|
409 $key = 'LOCK#' . $cached->filepath; |
|
410 $this->write(array($key => time()), $smarty->locking_timeout); |
|
411 } |
|
412 |
|
413 /** |
|
414 * Unlock cache for this template |
|
415 * |
|
416 * @param Smarty $smarty Smarty object |
|
417 * @param Smarty_Template_Cached $cached cached object |
|
418 */ |
|
419 public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached) |
|
420 { |
|
421 $cached->is_locked = false; |
|
422 $key = 'LOCK#' . $cached->filepath; |
|
423 $this->delete(array($key)); |
|
424 } |
|
425 |
|
426 /** |
|
427 * Read values for a set of keys from cache |
|
428 * |
|
429 * @param array $keys list of keys to fetch |
|
430 * @return array list of values with the given keys used as indexes |
|
431 */ |
|
432 protected abstract function read(array $keys); |
|
433 |
|
434 /** |
|
435 * Save values for a set of keys to cache |
|
436 * |
|
437 * @param array $keys list of values to save |
|
438 * @param int $expire expiration time |
|
439 * @return boolean true on success, false on failure |
|
440 */ |
|
441 protected abstract function write(array $keys, $expire=null); |
|
442 |
|
443 /** |
|
444 * Remove values from cache |
|
445 * |
|
446 * @param array $keys list of keys to delete |
|
447 * @return boolean true on success, false on failure |
|
448 */ |
|
449 protected abstract function delete(array $keys); |
|
450 |
|
451 /** |
|
452 * Remove *all* values from cache |
|
453 * |
|
454 * @return boolean true on success, false on failure |
|
455 */ |
|
456 protected function purge() |
|
457 { |
|
458 return false; |
|
459 } |
|
460 |
|
461 } |
|
462 |
|
463 ?> |