smarty_cacheresource_keyvaluestore.php 16.7 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
<?php
/**
 * Smarty Internal Plugin
 *
 * @package    Smarty
 * @subpackage Cacher
 */

/**
 * Smarty Cache Handler Base for Key/Value Storage Implementations
 * This class implements the functionality required to use simple key/value stores
 * for hierarchical cache groups. key/value stores like memcache or APC do not support
 * wildcards in keys, therefore a cache group cannot be cleared like "a|*" - which
 * is no problem to filesystem and RDBMS implementations.
 * This implementation is based on the concept of invalidation. While one specific cache
 * can be identified and cleared, any range of caches cannot be identified. For this reason
 * each level of the cache group hierarchy can have its own value in the store. These values
 * are nothing but microtimes, telling us when a particular cache group was cleared for the
 * last time. These keys are evaluated for every cache read to determine if the cache has
 * been invalidated since it was created and should hence be treated as inexistent.
 * Although deep hierarchies are possible, they are not recommended. Try to keep your
 * cache groups as shallow as possible. Anything up 3-5 parents should be ok. So
 * »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
 * cache groups: if your cache groups look somewhat like »a|b|$page|$items|$whatever«
 * consider using »a|b|c|$page-$items-$whatever« instead.
 *
 * @package    Smarty
 * @subpackage Cacher
 * @author     Rodney Rehm
 */
abstract class Smarty_CacheResource_KeyValueStore extends Smarty_CacheResource
{
    /**
     * cache for contents
     *
     * @var array
     */
    protected $contents = array();
    /**
     * cache for timestamps
     *
     * @var array
     */
    protected $timestamps = array();

    /**
     * populate Cached Object with meta data from Resource
     *
     * @param  Smarty_Template_Cached   $cached    cached object
     * @param  Smarty_Internal_Template $_template template object
     *
     * @return void
     */
    public function populate(Smarty_Template_Cached $cached, Smarty_Internal_Template $_template)
    {
        $cached->filepath = $_template->source->uid
            . '#' . $this->sanitize($cached->source->resource)
            . '#' . $this->sanitize($cached->cache_id)
            . '#' . $this->sanitize($cached->compile_id);

        $this->populateTimestamp($cached);
    }

    /**
     * populate Cached Object with timestamp and exists from Resource
     *
     * @param  Smarty_Template_Cached $cached cached object
     *
     * @return void
     */
    public function populateTimestamp(Smarty_Template_Cached $cached)
    {
        if (!$this->fetch($cached->filepath, $cached->source->name, $cached->cache_id, $cached->compile_id, $content, $timestamp, $cached->source->uid)) {
            return;
        }
        $cached->content = $content;
        $cached->timestamp = (int) $timestamp;
        $cached->exists = $cached->timestamp;
    }

    /**
     * Read the cached template and process the header
     *
     * @param  Smarty_Internal_Template $_template template object
     * @param  Smarty_Template_Cached   $cached    cached object
     *
     * @return boolean                 true or false if the cached content does not exist
     */
    public function process(Smarty_Internal_Template $_template, Smarty_Template_Cached $cached = null)
    {
        if (!$cached) {
            $cached = $_template->cached;
        }
        $content = $cached->content ? $cached->content : null;
        $timestamp = $cached->timestamp ? $cached->timestamp : null;
        if ($content === null || !$timestamp) {
            if (!$this->fetch($_template->cached->filepath, $_template->source->name, $_template->cache_id, $_template->compile_id, $content, $timestamp, $_template->source->uid)) {
                return false;
            }
        }
        if (isset($content)) {
            /** @var Smarty_Internal_Template $_smarty_tpl
             * used in evaluated code
             */
            $_smarty_tpl = $_template;
            eval("?>" . $content);

            return true;
        }

        return false;
    }

    /**
     * Write the rendered template output to cache
     *
     * @param  Smarty_Internal_Template $_template template object
     * @param  string                   $content   content to cache
     *
     * @return boolean                  success
     */
    public function writeCachedContent(Smarty_Internal_Template $_template, $content)
    {
        $this->addMetaTimestamp($content);

        return $this->write(array($_template->cached->filepath => $content), $_template->properties['cache_lifetime']);
    }

    /**
     * Empty cache
     * {@internal the $exp_time argument is ignored altogether }}
     *
     * @param  Smarty  $smarty   Smarty object
     * @param  integer $exp_time expiration time [being ignored]
     *
     * @return integer number of cache files deleted [always -1]
     * @uses purge() to clear the whole store
     * @uses invalidate() to mark everything outdated if purge() is inapplicable
     */
    public function clearAll(Smarty $smarty, $exp_time = null)
    {
        if (!$this->purge()) {
            $this->invalidate(null);
        }

        return - 1;
    }

    /**
     * Empty cache for a specific template
     * {@internal the $exp_time argument is ignored altogether}}
     *
     * @param  Smarty  $smarty        Smarty object
     * @param  string  $resource_name template name
     * @param  string  $cache_id      cache id
     * @param  string  $compile_id    compile id
     * @param  integer $exp_time      expiration time [being ignored]
     *
     * @return integer number of cache files deleted [always -1]
     * @uses buildCachedFilepath() to generate the CacheID
     * @uses invalidate() to mark CacheIDs parent chain as outdated
     * @uses delete() to remove CacheID from cache
     */
    public function clear(Smarty $smarty, $resource_name, $cache_id, $compile_id, $exp_time)
    {
        $uid = $this->getTemplateUid($smarty, $resource_name, $cache_id, $compile_id);
        $cid = $uid . '#' . $this->sanitize($resource_name) . '#' . $this->sanitize($cache_id) . '#' . $this->sanitize($compile_id);
        $this->delete(array($cid));
        $this->invalidate($cid, $resource_name, $cache_id, $compile_id, $uid);

        return - 1;
    }

    /**
     * Get template's unique ID
     *
     * @param  Smarty $smarty        Smarty object
     * @param  string $resource_name template name
     * @param  string $cache_id      cache id
     * @param  string $compile_id    compile id
     *
     * @return string filepath of cache file
     */
    protected function getTemplateUid(Smarty $smarty, $resource_name, $cache_id, $compile_id)
    {
        $uid = '';
        if (isset($resource_name)) {
            $tpl = new $smarty->template_class($resource_name, $smarty);
            if ($tpl->source->exists) {
                $uid = $tpl->source->uid;
            }

            // remove from template cache
            if ($smarty->allow_ambiguous_resources) {
                $_templateId = $tpl->source->unique_resource . $tpl->cache_id . $tpl->compile_id;
            } else {
                $_templateId = $smarty->joined_template_dir . '#' . $resource_name . $tpl->cache_id . $tpl->compile_id;
            }
            if (isset($_templateId[150])) {
                $_templateId = sha1($_templateId);
            }
            unset($smarty->template_objects[$_templateId]);
        }

        return $uid;
    }

    /**
     * Sanitize CacheID components
     *
     * @param  string $string CacheID component to sanitize
     *
     * @return string sanitized CacheID component
     */
    protected function sanitize($string)
    {
        // some poeple smoke bad weed
        $string = trim($string, '|');
        if (!$string) {
            return null;
        }

        return preg_replace('#[^\w\|]+#S', '_', $string);
    }

    /**
     * Fetch and prepare a cache object.
     *
     * @param  string  $cid           CacheID to fetch
     * @param  string  $resource_name template name
     * @param  string  $cache_id      cache id
     * @param  string  $compile_id    compile id
     * @param  string  $content       cached content
     * @param  integer &$timestamp    cached timestamp (epoch)
     * @param  string  $resource_uid  resource's uid
     *
     * @return boolean success
     */
    protected function fetch($cid, $resource_name = null, $cache_id = null, $compile_id = null, &$content = null, &$timestamp = null, $resource_uid = null)
    {
        $t = $this->read(array($cid));
        $content = !empty($t[$cid]) ? $t[$cid] : null;
        $timestamp = null;

        if ($content && ($timestamp = $this->getMetaTimestamp($content))) {
            $invalidated = $this->getLatestInvalidationTimestamp($cid, $resource_name, $cache_id, $compile_id, $resource_uid);
            if ($invalidated > $timestamp) {
                $timestamp = null;
                $content = null;
            }
        }

        return !!$content;
    }

    /**
     * Add current microtime to the beginning of $cache_content
     * {@internal the header uses 8 Bytes, the first 4 Bytes are the seconds, the second 4 Bytes are the microseconds}}
     *
     * @param string &$content the content to be cached
     */
    protected function addMetaTimestamp(&$content)
    {
        $mt = explode(" ", microtime());
        $ts = pack("NN", $mt[1], (int) ($mt[0] * 100000000));
        $content = $ts . $content;
    }

    /**
     * Extract the timestamp the $content was cached
     *
     * @param  string &$content the cached content
     *
     * @return float  the microtime the content was cached
     */
    protected function getMetaTimestamp(&$content)
    {
        $s = unpack("N", substr($content, 0, 4));
        $m = unpack("N", substr($content, 4, 4));
        $content = substr($content, 8);

        return $s[1] + ($m[1] / 100000000);
    }

    /**
     * Invalidate CacheID
     *
     * @param  string $cid           CacheID
     * @param  string $resource_name template name
     * @param  string $cache_id      cache id
     * @param  string $compile_id    compile id
     * @param  string $resource_uid  source's uid
     *
     * @return void
     */
    protected function invalidate($cid = null, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null)
    {
        $now = microtime(true);
        $key = null;
        // invalidate everything
        if (!$resource_name && !$cache_id && !$compile_id) {
            $key = 'IVK#ALL';
        } // invalidate all caches by template
        else {
            if ($resource_name && !$cache_id && !$compile_id) {
                $key = 'IVK#TEMPLATE#' . $resource_uid . '#' . $this->sanitize($resource_name);
            } // invalidate all caches by cache group
            else {
                if (!$resource_name && $cache_id && !$compile_id) {
                    $key = 'IVK#CACHE#' . $this->sanitize($cache_id);
                } // invalidate all caches by compile id
                else {
                    if (!$resource_name && !$cache_id && $compile_id) {
                        $key = 'IVK#COMPILE#' . $this->sanitize($compile_id);
                    } // invalidate by combination
                    else {
                        $key = 'IVK#CID#' . $cid;
                    }
                }
            }
        }
        $this->write(array($key => $now));
    }

    /**
     * Determine the latest timestamp known to the invalidation chain
     *
     * @param  string $cid           CacheID to determine latest invalidation timestamp of
     * @param  string $resource_name template name
     * @param  string $cache_id      cache id
     * @param  string $compile_id    compile id
     * @param  string $resource_uid  source's filepath
     *
     * @return float  the microtime the CacheID was invalidated
     */
    protected function getLatestInvalidationTimestamp($cid, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null)
    {
        // abort if there is no CacheID
        if (false && !$cid) {
            return 0;
        }
        // abort if there are no InvalidationKeys to check
        if (!($_cid = $this->listInvalidationKeys($cid, $resource_name, $cache_id, $compile_id, $resource_uid))) {
            return 0;
        }

        // there are no InValidationKeys
        if (!($values = $this->read($_cid))) {
            return 0;
        }
        // make sure we're dealing with floats
        $values = array_map('floatval', $values);

        return max($values);
    }

    /**
     * Translate a CacheID into the list of applicable InvalidationKeys.
     * Splits "some|chain|into|an|array" into array( '#clearAll#', 'some', 'some|chain', 'some|chain|into', ... )
     *
     * @param  string $cid           CacheID to translate
     * @param  string $resource_name template name
     * @param  string $cache_id      cache id
     * @param  string $compile_id    compile id
     * @param  string $resource_uid  source's filepath
     *
     * @return array  list of InvalidationKeys
     * @uses $invalidationKeyPrefix to prepend to each InvalidationKey
     */
    protected function listInvalidationKeys($cid, $resource_name = null, $cache_id = null, $compile_id = null, $resource_uid = null)
    {
        $t = array('IVK#ALL');
        $_name = $_compile = '#';
        if ($resource_name) {
            $_name .= $resource_uid . '#' . $this->sanitize($resource_name);
            $t[] = 'IVK#TEMPLATE' . $_name;
        }
        if ($compile_id) {
            $_compile .= $this->sanitize($compile_id);
            $t[] = 'IVK#COMPILE' . $_compile;
        }
        $_name .= '#';
        // some poeple smoke bad weed
        $cid = trim($cache_id, '|');
        if (!$cid) {
            return $t;
        }
        $i = 0;
        while (true) {
            // determine next delimiter position
            $i = strpos($cid, '|', $i);
            // add complete CacheID if there are no more delimiters
            if ($i === false) {
                $t[] = 'IVK#CACHE#' . $cid;
                $t[] = 'IVK#CID' . $_name . $cid . $_compile;
                $t[] = 'IVK#CID' . $_name . $_compile;
                break;
            }
            $part = substr($cid, 0, $i);
            // add slice to list
            $t[] = 'IVK#CACHE#' . $part;
            $t[] = 'IVK#CID' . $_name . $part . $_compile;
            // skip past delimiter position
            $i ++;
        }

        return $t;
    }

    /**
     * Check is cache is locked for this template
     *
     * @param  Smarty                 $smarty Smarty object
     * @param  Smarty_Template_Cached $cached cached object
     *
     * @return boolean               true or false if cache is locked
     */
    public function hasLock(Smarty $smarty, Smarty_Template_Cached $cached)
    {
        $key = 'LOCK#' . $cached->filepath;
        $data = $this->read(array($key));

        return $data && time() - $data[$key] < $smarty->locking_timeout;
    }

    /**
     * Lock cache for this template
     *
     * @param Smarty                 $smarty Smarty object
     * @param Smarty_Template_Cached $cached cached object
     *
     * @return bool|void
     */
    public function acquireLock(Smarty $smarty, Smarty_Template_Cached $cached)
    {
        $cached->is_locked = true;
        $key = 'LOCK#' . $cached->filepath;
        $this->write(array($key => time()), $smarty->locking_timeout);
    }

    /**
     * Unlock cache for this template
     *
     * @param Smarty                 $smarty Smarty object
     * @param Smarty_Template_Cached $cached cached object
     *
     * @return bool|void
     */
    public function releaseLock(Smarty $smarty, Smarty_Template_Cached $cached)
    {
        $cached->is_locked = false;
        $key = 'LOCK#' . $cached->filepath;
        $this->delete(array($key));
    }

    /**
     * Read values for a set of keys from cache
     *
     * @param  array $keys list of keys to fetch
     *
     * @return array list of values with the given keys used as indexes
     */
    abstract protected function read(array $keys);

    /**
     * Save values for a set of keys to cache
     *
     * @param  array $keys   list of values to save
     * @param  int   $expire expiration time
     *
     * @return boolean true on success, false on failure
     */
    abstract protected function write(array $keys, $expire = null);

    /**
     * Remove values from cache
     *
     * @param  array $keys list of keys to delete
     *
     * @return boolean true on success, false on failure
     */
    abstract protected function delete(array $keys);

    /**
     * Remove *all* values from cache
     *
     * @return boolean true on success, false on failure
     */
    protected function purge()
    {
        return false;
    }
}