MongoDb.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. /**
  7. * MongoDb cache backend
  8. */
  9. namespace Magento\Framework\Cache\Backend;
  10. class MongoDb extends \Zend_Cache_Backend implements \Zend_Cache_Backend_ExtendedInterface
  11. {
  12. /**
  13. * Infinite expiration time
  14. */
  15. const EXPIRATION_TIME_INFINITE = 0;
  16. /**#@+
  17. * Available comparison modes. Used for composing queries to search by tags
  18. */
  19. const COMPARISON_MODE_MATCHING_TAG = \Zend_Cache::CLEANING_MODE_MATCHING_TAG;
  20. const COMPARISON_MODE_NOT_MATCHING_TAG = \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG;
  21. const COMPARISON_MODE_MATCHING_ANY_TAG = \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG;
  22. /**#@-*/
  23. /**#@-*/
  24. protected $_collection = null;
  25. /**
  26. * List of available options
  27. *
  28. * @var array
  29. */
  30. protected $_options = [
  31. 'connection_string' => 'mongodb://localhost:27017', // MongoDB connection string
  32. 'mongo_options' => [], // MongoDB connection options
  33. 'db' => '', // Name of a database to be used for cache storage
  34. 'collection' => 'cache', // Name of a collection to be used for cache storage
  35. ];
  36. /**
  37. * @param array $options
  38. */
  39. public function __construct(array $options = [])
  40. {
  41. if (!extension_loaded('mongo') || !version_compare(\Mongo::VERSION, '1.2.11', '>=')) {
  42. \Zend_Cache::throwException(
  43. "At least 1.2.11 version of 'mongo' extension is required for using MongoDb cache backend"
  44. );
  45. }
  46. if (empty($options['db'])) {
  47. \Zend_Cache::throwException("'db' option is not specified");
  48. }
  49. parent::__construct($options);
  50. }
  51. /**
  52. * Get collection
  53. *
  54. * @return \MongoCollection
  55. */
  56. protected function _getCollection()
  57. {
  58. if (null === $this->_collection) {
  59. $connection = new \Mongo($this->_options['connection_string'], $this->_options['mongo_options']);
  60. $database = $connection->selectDB($this->_options['db']);
  61. $this->_collection = $database->selectCollection($this->_options['collection']);
  62. }
  63. return $this->_collection;
  64. }
  65. /**
  66. * Return an array of stored cache ids
  67. *
  68. * @return string[] array of stored cache ids (string)
  69. */
  70. public function getIds()
  71. {
  72. return array_keys(iterator_to_array($this->_getCollection()->find([], ['_id'])));
  73. }
  74. /**
  75. * Return an array of stored tags
  76. *
  77. * @return string[] array of stored tags (string)
  78. */
  79. public function getTags()
  80. {
  81. $result = $this->_getCollection()->distinct('tags');
  82. return $result ?: [];
  83. }
  84. /**
  85. * Return an array of stored cache ids which match given tags
  86. *
  87. * In case of multiple tags, a logical AND is made between tags
  88. *
  89. * @param string[] $tags array of tags
  90. * @return string[] array of matching cache ids (string)
  91. */
  92. public function getIdsMatchingTags($tags = [])
  93. {
  94. $query = $this->_getQueryMatchingTags($tags, self::COMPARISON_MODE_MATCHING_TAG);
  95. if (empty($query)) {
  96. return [];
  97. }
  98. $result = $this->_getCollection()->find($query, ['_id']);
  99. return array_keys(iterator_to_array($result));
  100. }
  101. /**
  102. * Return an array of stored cache ids which don't match given tags
  103. *
  104. * In case of multiple tags, a logical OR is made between tags
  105. *
  106. * @param string[] $tags array of tags
  107. * @return string[] array of not matching cache ids (string)
  108. */
  109. public function getIdsNotMatchingTags($tags = [])
  110. {
  111. $query = $this->_getQueryMatchingTags($tags, self::COMPARISON_MODE_NOT_MATCHING_TAG);
  112. if (empty($query)) {
  113. return [];
  114. }
  115. $result = $this->_getCollection()->find($query, ['_id']);
  116. return array_keys(iterator_to_array($result));
  117. }
  118. /**
  119. * Return an array of stored cache ids which match any given tags
  120. *
  121. * In case of multiple tags, a logical AND is made between tags
  122. *
  123. * @param string[] $tags array of tags
  124. * @return string[] array of any matching cache ids (string)
  125. */
  126. public function getIdsMatchingAnyTags($tags = [])
  127. {
  128. $query = $this->_getQueryMatchingTags($tags, self::COMPARISON_MODE_MATCHING_ANY_TAG);
  129. if (empty($query)) {
  130. return [];
  131. }
  132. $result = $this->_getCollection()->find($query, ['_id']);
  133. return array_keys(iterator_to_array($result));
  134. }
  135. /**
  136. * Get query to filter by specified tags and comparison mode
  137. *
  138. * @param string[] $tags
  139. * @param string $comparisonMode
  140. * @return array
  141. */
  142. protected function _getQueryMatchingTags(array $tags, $comparisonMode)
  143. {
  144. $operators = [
  145. self::COMPARISON_MODE_MATCHING_TAG => '$and',
  146. self::COMPARISON_MODE_NOT_MATCHING_TAG => '$nor',
  147. self::COMPARISON_MODE_MATCHING_ANY_TAG => '$or',
  148. ];
  149. if (!isset($operators[$comparisonMode])) {
  150. \Zend_Cache::throwException("Incorrect comparison mode specified: {$comparisonMode}");
  151. }
  152. $operator = $operators[$comparisonMode];
  153. $query = [];
  154. foreach ($tags as $tag) {
  155. $query[$operator][] = ['tags' => $this->_quoteString($tag)];
  156. }
  157. return $query;
  158. }
  159. /**
  160. * Return the filling percentage of the backend storage
  161. *
  162. * @return int integer between 0 and 100
  163. * TODO: implement basing on info from MongoDB server
  164. */
  165. public function getFillingPercentage()
  166. {
  167. return 1;
  168. }
  169. /**
  170. * Return an array of metadatas for the given cache id
  171. *
  172. * The array must include these keys :
  173. * - expire : the expire timestamp
  174. * - tags : a string array of tags
  175. * - mtime : timestamp of last modification time
  176. *
  177. * @param string $cacheId cache id
  178. * @return array|false array of metadatas (false if the cache id is not found)
  179. */
  180. public function getMetadatas($cacheId)
  181. {
  182. $result = $this->_getCollection()->findOne(
  183. ['_id' => $this->_quoteString($cacheId)],
  184. ['expire', 'tags', 'mtime']
  185. );
  186. return $result === null ? false : $result;
  187. }
  188. /**
  189. * Give (if possible) an extra lifetime to the given cache id
  190. *
  191. * @param string $cacheId cache id
  192. * @param int $extraLifetime
  193. * @return boolean true if ok
  194. */
  195. public function touch($cacheId, $extraLifetime)
  196. {
  197. $time = time();
  198. $condition = ['_id' => $this->_quoteString($cacheId), 'expire' => ['$gt' => $time]];
  199. $update = ['$set' => ['mtime' => $time], '$inc' => ['expire' => (int)$extraLifetime]];
  200. return $this->_getCollection()->update($condition, $update);
  201. }
  202. /**
  203. * Return an associative array of capabilities (booleans) of the backend
  204. *
  205. * The array must include these keys :
  206. * - automatic_cleaning (is automating cleaning necessary)
  207. * - tags (are tags supported)
  208. * - expired_read (is it possible to read expired cache records
  209. * (for doNotTestCacheValidity option for example))
  210. * - priority does the backend deal with priority when saving
  211. * - infinite_lifetime (is infinite lifetime can work with this backend)
  212. * - get_list (is it possible to get the list of cache ids and the complete list of tags)
  213. *
  214. * @return array associative of with capabilities
  215. */
  216. public function getCapabilities()
  217. {
  218. return [
  219. 'automatic_cleaning' => true,
  220. 'tags' => true,
  221. 'expired_read' => true,
  222. 'priority' => false,
  223. 'infinite_lifetime' => true,
  224. 'get_list' => true
  225. ];
  226. }
  227. /**
  228. * Test if a cache is available for the given id and (if yes) return it (false else)
  229. *
  230. * Note : return value is always "string" (unserialization is done by the core not by the backend)
  231. *
  232. * @param string $cacheId Cache id
  233. * @param boolean $notTestCacheValidity If set to true, the cache validity won't be tested
  234. * @return string|bool cached data. Return false if nothing found
  235. */
  236. public function load($cacheId, $notTestCacheValidity = false)
  237. {
  238. $query = ['_id' => $this->_quoteString($cacheId)];
  239. if (!$notTestCacheValidity) {
  240. $query['$or'] = [
  241. ['expire' => self::EXPIRATION_TIME_INFINITE],
  242. ['expire' => ['$gt' => time()]],
  243. ];
  244. }
  245. $result = $this->_getCollection()->findOne($query, ['data']);
  246. return $result ? $result['data']->bin : false;
  247. }
  248. /**
  249. * Test if a cache is available or not (for the given id)
  250. *
  251. * @param string $cacheId cache id
  252. * @return int|bool "last modified" timestamp of the available cache record or false if cache is not available
  253. */
  254. public function test($cacheId)
  255. {
  256. $result = $this->_getCollection()->findOne(
  257. [
  258. '_id' => $this->_quoteString($cacheId),
  259. '$or' => [
  260. ['expire' => self::EXPIRATION_TIME_INFINITE],
  261. ['expire' => ['$gt' => time()]],
  262. ],
  263. ],
  264. ['mtime']
  265. );
  266. return $result ? $result['mtime'] : false;
  267. }
  268. /**
  269. * Save some string data into a cache record
  270. *
  271. * Note : $data is always "string" (serialization is done by the
  272. * core not by the backend)
  273. *
  274. * @param string $data Datas to cache
  275. * @param string $cacheId Cache id
  276. * @param string[] $tags Array of strings, the cache record will be tagged by each string entry
  277. * @param int|bool $specificLifetime If != false, set a specific lifetime (null => infinite lifetime)
  278. * @return boolean true if no problem
  279. */
  280. public function save($data, $cacheId, $tags = [], $specificLifetime = false)
  281. {
  282. $lifetime = $this->getLifetime($specificLifetime);
  283. $time = time();
  284. $expire = $lifetime === null ? self::EXPIRATION_TIME_INFINITE : $time + $lifetime;
  285. $tags = array_map([$this, '_quoteString'], $tags);
  286. $document = [
  287. '_id' => $this->_quoteString($cacheId),
  288. 'data' => new \MongoBinData($this->_quoteString($data), \MongoBinData::BYTE_ARRAY),
  289. 'tags' => $tags,
  290. 'mtime' => $time,
  291. 'expire' => $expire,
  292. ];
  293. return $this->_getCollection()->save($document);
  294. }
  295. /**
  296. * Remove a cache record
  297. *
  298. * @param string $cacheId Cache id
  299. * @return boolean True if no problem
  300. */
  301. public function remove($cacheId)
  302. {
  303. return $this->_getCollection()->remove(['_id' => $this->_quoteString($cacheId)]);
  304. }
  305. /**
  306. * Clean some cache records
  307. *
  308. * Available modes are :
  309. * \Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used)
  310. * \Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used)
  311. * \Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags
  312. * ($tags can be an array of strings or a single string)
  313. * \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
  314. * ($tags can be an array of strings or a single string)
  315. * \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
  316. * ($tags can be an array of strings or a single string)
  317. *
  318. * @param string $mode Clean mode
  319. * @param string[] $tags Array of tags
  320. * @return bool true if no problem
  321. */
  322. public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = [])
  323. {
  324. $result = false;
  325. switch ($mode) {
  326. case \Zend_Cache::CLEANING_MODE_ALL:
  327. $result = $this->_getCollection()->drop();
  328. $result = (bool)$result['ok'];
  329. break;
  330. case \Zend_Cache::CLEANING_MODE_OLD:
  331. $query = ['expire' => ['$ne' => self::EXPIRATION_TIME_INFINITE, '$lte' => time()]];
  332. break;
  333. case \Zend_Cache::CLEANING_MODE_MATCHING_TAG:
  334. case \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
  335. case \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
  336. $query = $this->_getQueryMatchingTags((array)$tags, $mode);
  337. break;
  338. default:
  339. \Zend_Cache::throwException('Unsupported cleaning mode: ' . $mode);
  340. }
  341. if (!empty($query)) {
  342. $result = $this->_getCollection()->remove($query);
  343. }
  344. return $result;
  345. }
  346. /**
  347. * Quote specified value to be used in query as string
  348. *
  349. * @param string $value
  350. * @return string
  351. */
  352. protected function _quoteString($value)
  353. {
  354. return (string)$value;
  355. }
  356. }