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