smarty_cacheresource_keyvaluestore.php 16 KB

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