PageRenderTime 46ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

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

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