source: trunk/include/smarty/libs/sysplugins/smarty_cacheresource_keyvaluestore.php @ 23384

Last change on this file since 23384 was 23384, checked in by rvelices, 11 years ago

smarty 3 - first pass for tests

File size: 16.1 KB
Line 
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 */
34abstract 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?>
Note: See TracBrowser for help on using the repository browser.