PageRenderTime 35ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/fuel/core/classes/cache/storage/redis.php

https://github.com/cloudmanic/fuel
PHP | 525 lines | 294 code | 76 blank | 155 comment | 29 complexity | 118213680602c73a33a3b919386f6132 MD5 | raw file
  1. <?php
  2. /**
  3. * Fuel
  4. *
  5. * Fuel is a fast, lightweight, community driven PHP5 framework.
  6. *
  7. * @package Fuel
  8. * @version 1.0
  9. * @author Harro "WanWizard" Verton
  10. * @license MIT License
  11. * @copyright 2010 - 2011 Fuel Development Team
  12. * @link http://fuelphp.com
  13. */
  14. namespace Fuel\Core;
  15. class Cache_Storage_Redis extends Cache_Storage_Driver {
  16. /**
  17. * @const string Tag used for opening & closing cache properties
  18. */
  19. const PROPS_TAG = 'Fuel_Cache_Properties';
  20. /**
  21. * @var driver specific configuration
  22. */
  23. protected $config = array();
  24. /*
  25. * @var storage for the redis object
  26. */
  27. protected $redis = false;
  28. // ---------------------------------------------------------------------
  29. public function __construct($identifier, $config)
  30. {
  31. $this->config = isset($config['redis']) ? $config['redis'] : array();
  32. // make sure we have a redis id
  33. $this->config['cache_id'] = $this->_validate_config('cache_id', isset($this->config['cache_id']) ? $this->config['cache_id'] : 'fuel');
  34. // check for an expiration override
  35. $this->expiration = $this->_validate_config('expiration', isset($this->config['expiration']) ? $this->config['expiration'] : $this->expiration);
  36. // make sure we have a redis database configured
  37. $this->config['database'] = $this->_validate_config('database', isset($this->config['database']) ? $this->config['database'] : 'default');
  38. if ($this->redis === false)
  39. {
  40. // get the redis database instance
  41. try
  42. {
  43. $this->redis = \Redis::instance($this->config['database']);
  44. }
  45. catch (Exception $e)
  46. {
  47. throw new \Cache_Exception('Can not connect to the Redis engine. The error message says "'.$e->getMessage().'".');
  48. }
  49. // get the redis version
  50. preg_match('/redis_version:(.*?)\n/', $this->redis->info(), $info);
  51. if (version_compare(trim($info[1]), '1.2') < 0)
  52. {
  53. throw new \Cache_Exception('Version 1.2 or higher of the Redis NoSQL engine is required to use the redis cache driver.');
  54. }
  55. }
  56. parent::__construct($identifier, $config);
  57. }
  58. // ---------------------------------------------------------------------
  59. /**
  60. * Translates a given identifier to a valid redis key
  61. *
  62. * @param string
  63. * @return string
  64. * @throws Cache_Exception
  65. */
  66. protected function identifier_to_key( $identifier )
  67. {
  68. return $this->config['cache_id'].':'.$identifier;
  69. }
  70. // ---------------------------------------------------------------------
  71. /**
  72. * Prepend the cache properties
  73. *
  74. * @return string
  75. */
  76. protected function prep_contents()
  77. {
  78. $properties = array(
  79. 'created' => $this->created,
  80. 'expiration' => $this->expiration,
  81. 'dependencies' => $this->dependencies,
  82. 'content_handler' => $this->content_handler
  83. );
  84. $properties = '{{'.self::PROPS_TAG.'}}'.json_encode($properties).'{{/'.self::PROPS_TAG.'}}';
  85. return $properties . $this->contents;
  86. }
  87. // ---------------------------------------------------------------------
  88. /**
  89. * Remove the prepended cache properties and save them in class properties
  90. *
  91. * @param string
  92. * @throws Cache_Exception
  93. */
  94. protected function unprep_contents($payload)
  95. {
  96. $properties_end = strpos($payload, '{{/'.self::PROPS_TAG.'}}');
  97. if ($properties_end === FALSE)
  98. {
  99. throw new \Cache_Exception('Incorrect formatting');
  100. }
  101. $this->contents = substr($payload, $properties_end + strlen('{{/'.self::PROPS_TAG.'}}'));
  102. $props = substr(substr($payload, 0, $properties_end), strlen('{{'.self::PROPS_TAG.'}}'));
  103. $props = json_decode($props, true);
  104. if ($props === NULL)
  105. {
  106. throw new \Cache_Exception('Properties retrieval failed');
  107. }
  108. $this->created = $props['created'];
  109. $this->expiration = is_null($this->expiration) ? null : (int) ($props['expiration'] - time());
  110. $this->dependencies = $props['dependencies'];
  111. $this->content_handler = $props['content_handler'];
  112. }
  113. // ---------------------------------------------------------------------
  114. /**
  115. * Check if other caches or files have been changed since cache creation
  116. *
  117. * @param array
  118. * @return bool
  119. */
  120. public function check_dependencies(Array $dependencies)
  121. {
  122. foreach($dependencies as $dep)
  123. {
  124. // get the section name and identifier
  125. $sections = explode('.', $dep);
  126. if (count($sections) > 1)
  127. {
  128. $identifier = array_pop($sections);
  129. $sections = '.'.implode('.', $sections);
  130. }
  131. else
  132. {
  133. $identifier = $dep;
  134. $sections = '';
  135. }
  136. // get the cache index
  137. $index = $this->redis->get($this->config['cache_id'].':index:'.$sections);
  138. is_null($index) or $index = $this->_unserialize($index);
  139. // get the key from the index
  140. $key = isset($index[$identifier][0]) ? $index[$identifier] : false;
  141. // key found and newer?
  142. if ($key !== false and $key[1] > $this->created)
  143. {
  144. return false;
  145. }
  146. }
  147. return true;
  148. }
  149. // ---------------------------------------------------------------------
  150. /**
  151. * Delete Cache
  152. */
  153. public function delete()
  154. {
  155. // get the key for the cache identifier
  156. $key = $this->_get_key(true);
  157. // delete the key from the redis server
  158. if ($key and $this->redis->del($key) === false)
  159. {
  160. // do something here?
  161. }
  162. $this->reset();
  163. }
  164. // ---------------------------------------------------------------------
  165. /**
  166. * Purge all caches
  167. *
  168. * @param limit purge to subsection
  169. * @return bool
  170. * @throws Cache_Exception
  171. */
  172. public function delete_all($section)
  173. {
  174. // determine the section index name
  175. $section = empty($section) ? '' : '.'.$section;
  176. // get the directory index
  177. $index = $this->redis->get($this->config['cache_id'].':dir:');
  178. is_null($index) or $index = $this->_unserialize($index);
  179. if (is_array($index))
  180. {
  181. if (!empty($section))
  182. {
  183. // limit the delete if we have a valid section
  184. $dirs = array();
  185. foreach ($index as $entry)
  186. {
  187. if ($entry == $section or strpos($entry, $section.'.') === 0)
  188. {
  189. $dirs[] = $entry;
  190. }
  191. }
  192. }
  193. else
  194. {
  195. // else delete the entire contents of the cache
  196. $dirs = $index;
  197. }
  198. // loop through the selected indexes
  199. foreach ($dirs as $dir)
  200. {
  201. // get the stored cache entries for this index
  202. $list = $this->redis->get($this->config['cache_id'].':index:'.$dir);
  203. if (is_null($list))
  204. {
  205. $list = array();
  206. }
  207. else
  208. {
  209. $list = $this->_unserialize($list);
  210. }
  211. // delete all stored keys
  212. foreach($list as $item)
  213. {
  214. $this->redis->del($item[0]);
  215. }
  216. // and delete the index itself
  217. $this->redis->del($this->config['cache_id'].':index:'.$dir);
  218. }
  219. // update the directory index
  220. $this->redis->set($this->config['cache_id'].':dir:', $this->_serialize(array_diff($index, $dirs)));
  221. }
  222. }
  223. // ---------------------------------------------------------------------
  224. /**
  225. * Save a cache, this does the generic pre-processing
  226. *
  227. * @return bool
  228. */
  229. protected function _set()
  230. {
  231. // get the key for the cache identifier
  232. $key = $this->_get_key();
  233. // write the cache
  234. $this->redis->set($key, $this->prep_contents());
  235. if (!empty($this->expiration))
  236. {
  237. $this->redis->expireat($key, $this->expiration);
  238. }
  239. }
  240. // ---------------------------------------------------------------------
  241. /**
  242. * Load a cache, this does the generic post-processing
  243. *
  244. * @return bool
  245. */
  246. protected function _get()
  247. {
  248. // get the key for the cache identifier
  249. $key = $this->_get_key();
  250. // fetch the session data from the redis server
  251. $payload = $this->redis->get($key);
  252. try
  253. {
  254. $this->unprep_contents($payload);
  255. }
  256. catch(Cache_Exception $e)
  257. {
  258. return false;
  259. }
  260. return true;
  261. }
  262. // ---------------------------------------------------------------------
  263. /**
  264. * get's the memcached key belonging to the cache identifier
  265. *
  266. * @access private
  267. * @param bool if true, remove the key retrieved from the index
  268. * @return string
  269. */
  270. private function _get_key($remove = false)
  271. {
  272. // get the section name and identifier
  273. $sections = explode('.', $this->identifier);
  274. if (count($sections) > 1)
  275. {
  276. $identifier = array_pop($sections);
  277. $sections = '.'.implode('.', $sections);
  278. }
  279. else
  280. {
  281. $identifier = $this->identifier;
  282. $sections = '';
  283. }
  284. // get the cache index
  285. $index = $this->redis->get($this->config['cache_id'].':index:'.$sections);
  286. is_null($index) or $index = $this->_unserialize($index);
  287. // get the key from the index
  288. $key = isset($index[$identifier][0]) ? $index[$identifier][0] : false;
  289. if ($remove === true)
  290. {
  291. if ( $key !== false )
  292. {
  293. unset($index[$identifier]);
  294. $this->redis->set($this->config['cache_id'].':index:'.$sections, $this->_serialize($index));
  295. }
  296. }
  297. else
  298. {
  299. if ( $key === false )
  300. {
  301. // create a new key
  302. $key = $this->_new_key();
  303. if ( ! is_array($index))
  304. {
  305. // create a new index and store the key
  306. $this->redis->set($this->config['cache_id'].':index:'.$sections, $this->_serialize(array($identifier => array($key,$this->created))));
  307. }
  308. else
  309. {
  310. // add the key to the index
  311. $index[$identifier] = array($key,$this->created);
  312. $this->redis->set($this->config['cache_id'].':index:'.$sections, $this->_serialize($index));
  313. }
  314. // get the directory index
  315. $index = $this->redis->get($this->config['cache_id'].':dir:');
  316. is_null($index) or $index = $this->_unserialize($index);
  317. if (is_array($index))
  318. {
  319. if ( ! in_array($sections, $index))
  320. {
  321. $index[] = $sections;
  322. }
  323. }
  324. else
  325. {
  326. $index = array($sections);
  327. }
  328. // update the directory index
  329. $this->redis->set($this->config['cache_id'].':dir:', $this->_serialize($index));
  330. }
  331. }
  332. return $key;
  333. }
  334. // ---------------------------------------------------------------------
  335. /**
  336. * generate a new unique key for the current identifier
  337. *
  338. * @access private
  339. * @return string
  340. */
  341. private function _new_key()
  342. {
  343. $key = '';
  344. while (strlen($key) < 32)
  345. {
  346. $key .= mt_rand(0, mt_getrandmax());
  347. }
  348. return $this->config['cache_id'].'_'.uniqid($key);
  349. }
  350. // ---------------------------------------------------------------------
  351. /**
  352. * validate a driver config value
  353. *
  354. * @param string name of the config variable to validate
  355. * @param mixed value
  356. * @access private
  357. * @return mixed
  358. */
  359. private function _validate_config($name, $value)
  360. {
  361. switch ($name)
  362. {
  363. case 'database':
  364. // do we have a database config
  365. if ( empty($value) OR ! is_array($value))
  366. {
  367. $value = 'default';
  368. }
  369. break;
  370. case 'cache_id':
  371. if ( empty($value) OR ! is_string($value))
  372. {
  373. $value = 'fuel';
  374. }
  375. break;
  376. case 'expiration':
  377. if ( empty($value) OR ! is_numeric($value))
  378. {
  379. $value = null;
  380. }
  381. break;
  382. default:
  383. break;
  384. }
  385. return $value;
  386. }
  387. // --------------------------------------------------------------------
  388. /**
  389. * Serialize an array
  390. *
  391. * This function first converts any slashes found in the array to a temporary
  392. * marker, so when it gets unserialized the slashes will be preserved
  393. *
  394. * @access private
  395. * @param array
  396. * @return string
  397. */
  398. protected function _serialize($data)
  399. {
  400. if (is_array($data))
  401. {
  402. foreach ($data as $key => $val)
  403. {
  404. if (is_string($val))
  405. {
  406. $data[$key] = str_replace('\\', '{{slash}}', $val);
  407. }
  408. }
  409. }
  410. else
  411. {
  412. if (is_string($data))
  413. {
  414. $data = str_replace('\\', '{{slash}}', $data);
  415. }
  416. }
  417. return serialize($data);
  418. }
  419. // --------------------------------------------------------------------
  420. /**
  421. * Unserialize
  422. *
  423. * This function unserializes a data string, then converts any
  424. * temporary slash markers back to actual slashes
  425. *
  426. * @access private
  427. * @param array
  428. * @return string
  429. */
  430. protected function _unserialize($data)
  431. {
  432. $data = @unserialize(stripslashes($data));
  433. if (is_array($data))
  434. {
  435. foreach ($data as $key => $val)
  436. {
  437. if (is_string($val))
  438. {
  439. $data[$key] = str_replace('{{slash}}', '\\', $val);
  440. }
  441. }
  442. return $data;
  443. }
  444. return (is_string($data)) ? str_replace('{{slash}}', '\\', $data) : $data;
  445. }
  446. }
  447. /* End of file file.php */