library/smarty/libs/sysplugins/smarty_cacheresource_keyvaluestore.php
changeset 46 f11c31f7fa3e
parent 45 a56e7f9a0463
child 47 03388ec805b4
equal deleted inserted replaced
45:a56e7f9a0463 46:f11c31f7fa3e
     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      * cache for contents
       
    34      *
       
    35      * @var array
       
    36      */
       
    37     protected $contents = array();
       
    38 
       
    39     /**
       
    40      * cache for timestamps
       
    41      *
       
    42      * @var array
       
    43      */
       
    44     protected $timestamps = array();
       
    45 
       
    46     /**
       
    47      * populate Cached Object with meta data from Resource
       
    48      *
       
    49      * @param  Smarty_Template_Cached $cached cached object
       
    50      * @param  Smarty_Internal_Template $_template template object
       
    51      *
       
    52      * @return void
       
    53      */
       
    54     public function populate(Smarty_Template_Cached $cached, Smarty_Internal_Template $_template) {
       
    55         $cached->filepath = $_template->source->uid . '#' . $this->sanitize($cached->source->resource) . '#' . $this->sanitize($cached->cache_id) . '#' . $this->sanitize($cached->compile_id);
       
    56 
       
    57         $this->populateTimestamp($cached);
       
    58     }
       
    59 
       
    60     /**
       
    61      * populate Cached Object with timestamp and exists from Resource
       
    62      *
       
    63      * @param  Smarty_Template_Cached $cached cached object
       
    64      *
       
    65      * @return void
       
    66      */
       
    67     public function populateTimestamp(Smarty_Template_Cached $cached) {
       
    68         if (!$this->fetch($cached->filepath, $cached->source->name, $cached->cache_id, $cached->compile_id, $content, $timestamp, $cached->source->uid)) {
       
    69             return;
       
    70         }
       
    71         $cached->content = $content;
       
    72         $cached->timestamp = (int)$timestamp;
       
    73         $cached->exists = $cached->timestamp;
       
    74     }
       
    75 
       
    76     /**
       
    77      * Read the cached template and process the header
       
    78      *
       
    79      * @param  Smarty_Internal_Template $_template template object
       
    80      * @param  Smarty_Template_Cached $cached cached object
       
    81      *
       
    82      * @return boolean                 true or false if the cached content does not exist
       
    83      */
       
    84     public function process(Smarty_Internal_Template $_template, Smarty_Template_Cached $cached = null) {
       
    85         if (!$cached) {
       
    86             $cached = $_template->cached;
       
    87         }
       
    88         $content = $cached->content ? $cached->content : null;
       
    89         $timestamp = $cached->timestamp ? $cached->timestamp : null;
       
    90         if ($content === null || !$timestamp) {
       
    91             if (!$this->fetch($_template->cached->filepath, $_template->source->name, $_template->cache_id, $_template->compile_id, $content, $timestamp, $_template->source->uid)) {
       
    92                 return false;
       
    93             }
       
    94         }
       
    95         if (isset($content)) {
       
    96             /** @var Smarty_Internal_Template $_smarty_tpl
       
    97              * used in evaluated code
       
    98              */
       
    99             $_smarty_tpl = $_template;
       
   100             eval("?>" . $content);
       
   101 
       
   102             return true;
       
   103         }
       
   104 
       
   105         return false;
       
   106     }
       
   107 
       
   108     /**
       
   109      * Write the rendered template output to cache
       
   110      *
       
   111      * @param  Smarty_Internal_Template $_template template object
       
   112      * @param  string $content content to cache
       
   113      *
       
   114      * @return boolean                  success
       
   115      */
       
   116     public function writeCachedContent(Smarty_Internal_Template $_template, $content) {
       
   117         $this->addMetaTimestamp($content);
       
   118 
       
   119         return $this->write(array($_template->cached->filepath => $content), $_template->properties['cache_lifetime']);
       
   120     }
       
   121 
       
   122     /**
       
   123      * Read cached template from cache
       
   124      *
       
   125      * @param  Smarty_Internal_Template $_template template object
       
   126      *
       
   127      * @return string  content
       
   128      */
       
   129     public function readCachedContent(Smarty_Internal_Template $_template) {
       
   130         $content = $_template->cached->content ? $_template->cached->content : null;
       
   131         $timestamp = null;
       
   132         if ($content === null) {
       
   133             if (!$this->fetch($_template->cached->filepath, $_template->source->name, $_template->cache_id, $_template->compile_id, $content, $timestamp, $_template->source->uid)) {
       
   134                 return false;
       
   135             }
       
   136         }
       
   137         if (isset($content)) {
       
   138             return $content;
       
   139         }
       
   140         return false;
       
   141     }
       
   142 
       
   143     /**
       
   144      * Empty cache
       
   145      * {@internal the $exp_time argument is ignored altogether }}
       
   146      *
       
   147      * @param  Smarty $smarty Smarty object
       
   148      * @param  integer $exp_time expiration time [being ignored]
       
   149      *
       
   150      * @return integer number of cache files deleted [always -1]
       
   151      * @uses purge() to clear the whole store
       
   152      * @uses invalidate() to mark everything outdated if purge() is inapplicable
       
   153      */
       
   154     public function clearAll(Smarty $smarty, $exp_time = null) {
       
   155         if (!$this->purge()) {
       
   156             $this->invalidate(null);
       
   157         }
       
   158 
       
   159         return -1;
       
   160     }
       
   161 
       
   162     /**
       
   163      * Empty cache for a specific template
       
   164      * {@internal the $exp_time argument is ignored altogether}}
       
   165      *
       
   166      * @param  Smarty $smarty Smarty object
       
   167      * @param  string $resource_name template name
       
   168      * @param  string $cache_id cache id
       
   169      * @param  string $compile_id compile id
       
   170      * @param  integer $exp_time expiration time [being ignored]
       
   171      *
       
   172      * @return integer number of cache files deleted [always -1]
       
   173      * @uses buildCachedFilepath() to generate the CacheID
       
   174      * @uses invalidate() to mark CacheIDs parent chain as outdated
       
   175      * @uses delete() to remove CacheID from cache
       
   176      */
       
   177     public function clear(Smarty $smarty, $resource_name, $cache_id, $compile_id, $exp_time) {
       
   178         $uid = $this->getTemplateUid($smarty, $resource_name, $cache_id, $compile_id);
       
   179         $cid = $uid . '#' . $this->sanitize($resource_name) . '#' . $this->sanitize($cache_id) . '#' . $this->sanitize($compile_id);
       
   180         $this->delete(array($cid));
       
   181         $this->invalidate($cid, $resource_name, $cache_id, $compile_id, $uid);
       
   182 
       
   183         return -1;
       
   184     }
       
   185 
       
   186     /**
       
   187      * Get template's unique ID
       
   188      *
       
   189      * @param  Smarty $smarty Smarty object
       
   190      * @param  string $resource_name template name
       
   191      * @param  string $cache_id cache id
       
   192      * @param  string $compile_id compile id
       
   193      *
       
   194      * @return string filepath of cache file
       
   195      */
       
   196     protected function getTemplateUid(Smarty $smarty, $resource_name, $cache_id, $compile_id) {
       
   197         $uid = '';
       
   198         if (isset($resource_name)) {
       
   199             $tpl = new $smarty->template_class($resource_name, $smarty);
       
   200             if ($tpl->source->exists) {
       
   201                 $uid = $tpl->source->uid;
       
   202             }
       
   203 
       
   204             // remove from template cache
       
   205             if ($smarty->allow_ambiguous_resources) {
       
   206                 $_templateId = $tpl->source->unique_resource . $tpl->cache_id . $tpl->compile_id;
       
   207             } else {
       
   208                 $_templateId = $smarty->joined_template_dir . '#' . $resource_name . $tpl->cache_id . $tpl->compile_id;
       
   209             }
       
   210             if (isset($_templateId[150])) {
       
   211                 $_templateId = sha1($_templateId);
       
   212             }
       
   213             unset($smarty->template_objects[$_templateId]);
       
   214         }
       
   215 
       
   216         return $uid;
       
   217     }
       
   218 
       
   219     /**
       
   220      * Sanitize CacheID components
       
   221      *
       
   222      * @param  string $string CacheID component to sanitize
       
   223      *
       
   224      * @return string sanitized CacheID component
       
   225      */
       
   226     protected function sanitize($string) {
       
   227         // some poeple smoke bad weed
       
   228         $string = trim($string, '|');
       
   229         if (!$string) {
       
   230             return null;
       
   231         }
       
   232 
       
   233         return preg_replace('#[^\w\|]+#S', '_', $string);
       
   234     }
       
   235 
       
   236     /**
       
   237      * Fetch and prepare a cache object.
       
   238      *
       
   239      * @param  string $cid CacheID to fetch
       
   240      * @param  string $resource_name template name
       
   241      * @param  string $cache_id cache id
       
   242      * @param  string $compile_id compile id
       
   243      * @param  string $content cached content
       
   244      * @param  integer &$timestamp cached timestamp (epoch)
       
   245      * @param  string $resource_uid resource's uid
       
   246      *
       
   247      * @return boolean success
       
   248      */
       
   249     protected function fetch($cid, $resource_name = null, $cache_id = null, $compile_id = null, &$content = null, &$timestamp = null, $resource_uid = null) {
       
   250         $t = $this->read(array($cid));
       
   251         $content = !empty($t[$cid]) ? $t[$cid] : null;
       
   252         $timestamp = null;
       
   253 
       
   254         if ($content && ($timestamp = $this->getMetaTimestamp($content))) {
       
   255             $invalidated = $this->getLatestInvalidationTimestamp($cid, $resource_name, $cache_id, $compile_id, $resource_uid);
       
   256             if ($invalidated > $timestamp) {
       
   257                 $timestamp = null;
       
   258                 $content = null;
       
   259             }
       
   260         }
       
   261 
       
   262         return !!$content;
       
   263     }
       
   264 
       
   265     /**
       
   266      * Add current microtime to the beginning of $cache_content
       
   267      * {@internal the header uses 8 Bytes, the first 4 Bytes are the seconds, the second 4 Bytes are the microseconds}}
       
   268      *
       
   269      * @param string &$content the content to be cached
       
   270      */
       
   271     protected function addMetaTimestamp(&$content) {
       
   272         $mt = explode(" ", microtime());
       
   273         $ts = pack("NN", $mt[1], (int)($mt[0] * 100000000));
       
   274         $content = $ts . $content;
       
   275     }
       
   276 
       
   277     /**
       
   278      * Extract the timestamp the $content was cached
       
   279      *
       
   280      * @param  string &$content the cached content
       
   281      *
       
   282      * @return float  the microtime the content was cached
       
   283      */
       
   284     protected function getMetaTimestamp(&$content) {
       
   285         extract(unpack('N1s/N1m/a*content', $content));
       
   286         return $s + ($m / 100000000);
       
   287     }
       
   288 
       
   289     /**
       
   290      * Invalidate CacheID
       
   291      *
       
   292      * @param  string $cid CacheID
       
   293      * @param  string $resource_name template name
       
   294      * @param  string $cache_id cache id
       
   295      * @param  string $compile_id compile id
       
   296      * @param  string $resource_uid source's uid
       
   297      *
       
   298      * @return void
       
   299      */
       
   300     protected function invalidate($cid = null, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null) {
       
   301         $now = microtime(true);
       
   302         $key = null;
       
   303         // invalidate everything
       
   304         if (!$resource_name && !$cache_id && !$compile_id) {
       
   305             $key = 'IVK#ALL';
       
   306         } // invalidate all caches by template
       
   307         else {
       
   308             if ($resource_name && !$cache_id && !$compile_id) {
       
   309                 $key = 'IVK#TEMPLATE#' . $resource_uid . '#' . $this->sanitize($resource_name);
       
   310             } // invalidate all caches by cache group
       
   311             else {
       
   312                 if (!$resource_name && $cache_id && !$compile_id) {
       
   313                     $key = 'IVK#CACHE#' . $this->sanitize($cache_id);
       
   314                 } // invalidate all caches by compile id
       
   315                 else {
       
   316                     if (!$resource_name && !$cache_id && $compile_id) {
       
   317                         $key = 'IVK#COMPILE#' . $this->sanitize($compile_id);
       
   318                     } // invalidate by combination
       
   319                     else {
       
   320                         $key = 'IVK#CID#' . $cid;
       
   321                     }
       
   322                 }
       
   323             }
       
   324         }
       
   325         $this->write(array($key => $now));
       
   326     }
       
   327 
       
   328     /**
       
   329      * Determine the latest timestamp known to the invalidation chain
       
   330      *
       
   331      * @param  string $cid CacheID to determine latest invalidation timestamp of
       
   332      * @param  string $resource_name template name
       
   333      * @param  string $cache_id cache id
       
   334      * @param  string $compile_id compile id
       
   335      * @param  string $resource_uid source's filepath
       
   336      *
       
   337      * @return float  the microtime the CacheID was invalidated
       
   338      */
       
   339     protected function getLatestInvalidationTimestamp($cid, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null) {
       
   340         // abort if there is no CacheID
       
   341         if (false && !$cid) {
       
   342             return 0;
       
   343         }
       
   344         // abort if there are no InvalidationKeys to check
       
   345         if (!($_cid = $this->listInvalidationKeys($cid, $resource_name, $cache_id, $compile_id, $resource_uid))) {
       
   346             return 0;
       
   347         }
       
   348 
       
   349         // there are no InValidationKeys
       
   350         if (!($values = $this->read($_cid))) {
       
   351             return 0;
       
   352         }
       
   353         // make sure we're dealing with floats
       
   354         $values = array_map('floatval', $values);
       
   355 
       
   356         return max($values);
       
   357     }
       
   358 
       
   359     /**
       
   360      * Translate a CacheID into the list of applicable InvalidationKeys.
       
   361      * Splits "some|chain|into|an|array" into array( '#clearAll#', 'some', 'some|chain', 'some|chain|into', ... )
       
   362      *
       
   363      * @param  string $cid CacheID to translate
       
   364      * @param  string $resource_name template name
       
   365      * @param  string $cache_id cache id
       
   366      * @param  string $compile_id compile id
       
   367      * @param  string $resource_uid source's filepath
       
   368      *
       
   369      * @return array  list of InvalidationKeys
       
   370      * @uses $invalidationKeyPrefix to prepend to each InvalidationKey
       
   371      */
       
   372     protected function listInvalidationKeys($cid, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null) {
       
   373         $t = array('IVK#ALL');
       
   374         $_name = $_compile = '#';
       
   375         if ($resource_name) {
       
   376             $_name .= $resource_uid . '#' . $this->sanitize($resource_name);
       
   377             $t[] = 'IVK#TEMPLATE' . $_name;
       
   378         }
       
   379         if ($compile_id) {
       
   380             $_compile .= $this->sanitize($compile_id);
       
   381             $t[] = 'IVK#COMPILE' . $_compile;
       
   382         }
       
   383         $_name .= '#';
       
   384         // some poeple smoke bad weed
       
   385         $cid = trim($cache_id, '|');
       
   386         if (!$cid) {
       
   387             return $t;
       
   388         }
       
   389         $i = 0;
       
   390         while (true) {
       
   391             // determine next delimiter position
       
   392             $i = strpos($cid, '|', $i);
       
   393             // add complete CacheID if there are no more delimiters
       
   394             if ($i === false) {
       
   395                 $t[] = 'IVK#CACHE#' . $cid;
       
   396                 $t[] = 'IVK#CID' . $_name . $cid . $_compile;
       
   397                 $t[] = 'IVK#CID' . $_name . $_compile;
       
   398                 break;
       
   399             }
       
   400             $part = substr($cid, 0, $i);
       
   401             // add slice to list
       
   402             $t[] = 'IVK#CACHE#' . $part;
       
   403             $t[] = 'IVK#CID' . $_name . $part . $_compile;
       
   404             // skip past delimiter position
       
   405             $i++;
       
   406         }
       
   407 
       
   408         return $t;
       
   409     }
       
   410 
       
   411     /**
       
   412      * Check is cache is locked for this template
       
   413      *
       
   414      * @param  Smarty $smarty Smarty object
       
   415      * @param  Smarty_Template_Cached $cached cached object
       
   416      *
       
   417      * @return boolean               true or false if cache is locked
       
   418      */
       
   419     public function hasLock(Smarty $smarty, Smarty_Template_Cached $cached) {
       
   420         $key = 'LOCK#' . $cached->filepath;
       
   421         $data = $this->read(array($key));
       
   422 
       
   423         return $data && time() - $data[$key] < $smarty->locking_timeout;
       
   424     }
       
   425 
       
   426     /**
       
   427      * Lock cache for this template
       
   428      *
       
   429      * @param Smarty $smarty Smarty object
       
   430      * @param Smarty_Template_Cached $cached cached object
       
   431      *
       
   432      * @return bool|void
       
   433      */
       
   434     public function acquireLock(Smarty $smarty, Smarty_Template_Cached $cached) {
       
   435         $cached->is_locked = true;
       
   436         $key = 'LOCK#' . $cached->filepath;
       
   437         $this->write(array($key => time()), $smarty->locking_timeout);
       
   438     }
       
   439 
       
   440     /**
       
   441      * Unlock cache for this template
       
   442      *
       
   443      * @param Smarty $smarty Smarty object
       
   444      * @param Smarty_Template_Cached $cached cached object
       
   445      *
       
   446      * @return bool|void
       
   447      */
       
   448     public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached) {
       
   449         $cached->is_locked = false;
       
   450         $key = 'LOCK#' . $cached->filepath;
       
   451         $this->delete(array($key));
       
   452     }
       
   453 
       
   454     /**
       
   455      * Read values for a set of keys from cache
       
   456      *
       
   457      * @param  array $keys list of keys to fetch
       
   458      *
       
   459      * @return array list of values with the given keys used as indexes
       
   460      */
       
   461     abstract protected function read(array $keys);
       
   462 
       
   463     /**
       
   464      * Save values for a set of keys to cache
       
   465      *
       
   466      * @param  array $keys list of values to save
       
   467      * @param  int $expire expiration time
       
   468      *
       
   469      * @return boolean true on success, false on failure
       
   470      */
       
   471     abstract protected function write(array $keys, $expire = null);
       
   472 
       
   473     /**
       
   474      * Remove values from cache
       
   475      *
       
   476      * @param  array $keys list of keys to delete
       
   477      *
       
   478      * @return boolean true on success, false on failure
       
   479      */
       
   480     abstract protected function delete(array $keys);
       
   481 
       
   482     /**
       
   483      * Remove *all* values from cache
       
   484      *
       
   485      * @return boolean true on success, false on failure
       
   486      */
       
   487     protected function purge() {
       
   488         return false;
       
   489     }
       
   490 }