PageRenderTime 64ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/cache/storage/redis.php

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