1 <?php
2 /**
3 * Smarty Internal Plugin
4 *
5 * @package Smarty
6 * @subpackage Cacher
7 */
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 {
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();
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);
61 $this->populateTimestamp($cached);
62 }
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 }
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 }
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 }
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 }
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 }
180 // remove from template cache
181 $_templateId = sha1($tpl->source->unique_resource . $tpl->cache_id . $tpl->compile_id);
182 unset($smarty->template_objects[$_templateId]);
183 }
184 return $uid;
185 }
187 /**
188 * Sanitize CacheID components
189 *
190 * @param string $string CacheID component to sanitize
191 * @return string sanitized CacheID component
192 */
193 protected function sanitize($string)
194 {
195 // some poeple smoke bad weed
196 $string = trim($string, '|');
197 if (!$string) {
198 return null;
199 }
200 return preg_replace('#[^\w\|]+#S', '_', $string);
201 }
203 /**
204 * Fetch and prepare a cache object.
205 *
206 * @param string $cid CacheID to fetch
207 * @param string $resource_name template name
208 * @param string $cache_id cache id
209 * @param string $compile_id compile id
210 * @param string $content cached content
211 * @param integer &$timestamp cached timestamp (epoch)
212 * @param string $resource_uid resource's uid
213 * @return boolean success
214 */
215 protected function fetch($cid, $resource_name = null, $cache_id = null, $compile_id = null, &$content = null, &$timestamp = null, $resource_uid = null)
216 {
217 $t = $this->read(array($cid));
218 $content = !empty($t[$cid]) ? $t[$cid] : null;
219 $timestamp = null;
221 if ($content && ($timestamp = $this->getMetaTimestamp($content))) {
222 $invalidated = $this->getLatestInvalidationTimestamp($cid, $resource_name, $cache_id, $compile_id, $resource_uid);
223 if ($invalidated > $timestamp) {
224 $timestamp = null;
225 $content = null;
226 }
227 }
229 return !!$content;
230 }
232 /**
233 * Add current microtime to the beginning of $cache_content
234 *
235 * {@internal the header uses 8 Bytes, the first 4 Bytes are the seconds, the second 4 Bytes are the microseconds}}
236 *
237 * @param string &$content the content to be cached
238 */
239 protected function addMetaTimestamp(&$content)
240 {
241 $mt = explode(" ", microtime());
242 $ts = pack("NN", $mt[1], (int) ($mt[0] * 100000000));
243 $content = $ts . $content;
244 }
246 /**
247 * Extract the timestamp the $content was cached
248 *
249 * @param string &$content the cached content
250 * @return float the microtime the content was cached
251 */
252 protected function getMetaTimestamp(&$content)
253 {
254 $s = unpack("N", substr($content, 0, 4));
255 $m = unpack("N", substr($content, 4, 4));
256 $content = substr($content, 8);
257 return $s[1] + ($m[1] / 100000000);
258 }
260 /**
261 * Invalidate CacheID
262 *
263 * @param string $cid CacheID
264 * @param string $resource_name template name
265 * @param string $cache_id cache id
266 * @param string $compile_id compile id
267 * @param string $resource_uid source's uid
268 * @return void
269 */
270 protected function invalidate($cid = null, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null)
271 {
272 $now = microtime(true);
273 $key = null;
274 // invalidate everything
275 if (!$resource_name && !$cache_id && !$compile_id) {
276 $key = 'IVK#ALL';
277 }
278 // invalidate all caches by template
279 else if ($resource_name && !$cache_id && !$compile_id) {
280 $key = 'IVK#TEMPLATE#' . $resource_uid . '#' . $this->sanitize($resource_name);
281 }
282 // invalidate all caches by cache group
283 else if (!$resource_name && $cache_id && !$compile_id) {
284 $key = 'IVK#CACHE#' . $this->sanitize($cache_id);
285 }
286 // invalidate all caches by compile id
287 else if (!$resource_name && !$cache_id && $compile_id) {
288 $key = 'IVK#COMPILE#' . $this->sanitize($compile_id);
289 }
290 // invalidate by combination
291 else {
292 $key = 'IVK#CID#' . $cid;
293 }
294 $this->write(array($key => $now));
295 }
297 /**
298 * Determine the latest timestamp known to the invalidation chain
299 *
300 * @param string $cid CacheID to determine latest invalidation timestamp of
301 * @param string $resource_name template name
302 * @param string $cache_id cache id
303 * @param string $compile_id compile id
304 * @param string $resource_uid source's filepath
305 * @return float the microtime the CacheID was invalidated
306 */
307 protected function getLatestInvalidationTimestamp($cid, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null)
308 {
309 // abort if there is no CacheID
310 if (false && !$cid) {
311 return 0;
312 }
313 // abort if there are no InvalidationKeys to check
314 if (!($_cid = $this->listInvalidationKeys($cid, $resource_name, $cache_id, $compile_id, $resource_uid))) {
315 return 0;
316 }
318 // there are no InValidationKeys
319 if (!($values = $this->read($_cid))) {
320 return 0;
321 }
322 // make sure we're dealing with floats
323 $values = array_map('floatval', $values);
324 return max($values);
325 }
327 /**
328 * Translate a CacheID into the list of applicable InvalidationKeys.
329 *
330 * Splits "some|chain|into|an|array" into array( '#clearAll#', 'some', 'some|chain', 'some|chain|into', ... )
331 *
332 * @param string $cid CacheID to translate
333 * @param string $resource_name template name
334 * @param string $cache_id cache id
335 * @param string $compile_id compile id
336 * @param string $resource_uid source's filepath
337 * @return array list of InvalidationKeys
338 * @uses $invalidationKeyPrefix to prepend to each InvalidationKey
339 */
340 protected function listInvalidationKeys($cid, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null)
341 {
342 $t = array('IVK#ALL');
343 $_name = $_compile = '#';
344 if ($resource_name) {
345 $_name .= $resource_uid . '#' . $this->sanitize($resource_name);
346 $t[] = 'IVK#TEMPLATE' . $_name;
347 }
348 if ($compile_id) {
349 $_compile .= $this->sanitize($compile_id);
350 $t[] = 'IVK#COMPILE' . $_compile;
351 }
352 $_name .= '#';
353 // some poeple smoke bad weed
354 $cid = trim($cache_id, '|');
355 if (!$cid) {
356 return $t;
357 }
358 $i = 0;
359 while (true) {
360 // determine next delimiter position
361 $i = strpos($cid, '|', $i);
362 // add complete CacheID if there are no more delimiters
363 if ($i === false) {
364 $t[] = 'IVK#CACHE#' . $cid;
365 $t[] = 'IVK#CID' . $_name . $cid . $_compile;
366 $t[] = 'IVK#CID' . $_name . $_compile;
367 break;
368 }
369 $part = substr($cid, 0, $i);
370 // add slice to list
371 $t[] = 'IVK#CACHE#' . $part;
372 $t[] = 'IVK#CID' . $_name . $part . $_compile;
373 // skip past delimiter position
374 $i++;
375 }
376 return $t;
377 }
379 /**
380 * Check is cache is locked for this template
381 *
382 * @param Smarty $smarty Smarty object
383 * @param Smarty_Template_Cached $cached cached object
384 * @return booelan true or false if cache is locked
385 */
386 public function hasLock(Smarty $smarty, Smarty_Template_Cached $cached)
387 {
388 $key = 'LOCK#' . $cached->filepath;
389 $data = $this->read(array($key));
390 return $data && time() - $data[$key] < $smarty->locking_timeout;
391 }
393 /**
394 * Lock cache for this template
395 *
396 * @param Smarty $smarty Smarty object
397 * @param Smarty_Template_Cached $cached cached object
398 */
399 public function acquireLock(Smarty $smarty, Smarty_Template_Cached $cached)
400 {
401 $cached->is_locked = true;
402 $key = 'LOCK#' . $cached->filepath;
403 $this->write(array($key => time()), $smarty->locking_timeout);
404 }
406 /**
407 * Unlock cache for this template
408 *
409 * @param Smarty $smarty Smarty object
410 * @param Smarty_Template_Cached $cached cached object
411 */
412 public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached)
413 {
414 $cached->is_locked = false;
415 $key = 'LOCK#' . $cached->filepath;
416 $this->delete(array($key));
417 }
419 /**
420 * Read values for a set of keys from cache
421 *
422 * @param array $keys list of keys to fetch
423 * @return array list of values with the given keys used as indexes
424 */
425 protected abstract function read(array $keys);
427 /**
428 * Save values for a set of keys to cache
429 *
430 * @param array $keys list of values to save
431 * @param int $expire expiration time
432 * @return boolean true on success, false on failure
433 */
434 protected abstract function write(array $keys, $expire=null);
436 /**
437 * Remove values from cache
438 *
439 * @param array $keys list of keys to delete
440 * @return boolean true on success, false on failure
441 */
442 protected abstract function delete(array $keys);
444 /**
445 * Remove *all* values from cache
446 *
447 * @return boolean true on success, false on failure
448 */
449 protected function purge()
450 {
451 return false;
452 }
454 }
456 ?>