Database.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. /**
  7. * Tables declaration:
  8. *
  9. * CREATE TABLE IF NOT EXISTS `cache` (
  10. * `id` VARCHAR(255) NOT NULL,
  11. * `data` mediumblob,
  12. * `create_time` int(11),
  13. * `update_time` int(11),
  14. * `expire_time` int(11),
  15. * PRIMARY KEY (`id`),
  16. * KEY `IDX_EXPIRE_TIME` (`expire_time`)
  17. * )ENGINE=InnoDB DEFAULT CHARSET=utf8;
  18. *
  19. * CREATE TABLE IF NOT EXISTS `cache_tag` (
  20. * `tag` VARCHAR(255) NOT NULL,
  21. * `cache_id` VARCHAR(255) NOT NULL,
  22. * KEY `IDX_TAG` (`tag`),
  23. * KEY `IDX_CACHE_ID` (`cache_id`),
  24. * CONSTRAINT `FK_CORE_CACHE_TAG` FOREIGN KEY (`cache_id`)
  25. * REFERENCES `cache` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
  26. * ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  27. */
  28. namespace Magento\Framework\Cache\Backend;
  29. /**
  30. * Database cache backend.
  31. */
  32. class Database extends \Zend_Cache_Backend implements \Zend_Cache_Backend_ExtendedInterface
  33. {
  34. /**
  35. * Available options
  36. *
  37. * @var array available options
  38. */
  39. protected $_options = [
  40. 'adapter' => '',
  41. 'adapter_callback' => '',
  42. 'data_table' => '',
  43. 'data_table_callback' => '',
  44. 'tags_table' => '',
  45. 'tags_table_callback' => '',
  46. 'store_data' => true,
  47. 'infinite_loop_flag' => false,
  48. ];
  49. /**
  50. * @var \Magento\Framework\DB\Adapter\AdapterInterface
  51. */
  52. protected $_connection = null;
  53. /**
  54. * Constructor
  55. *
  56. * @param array $options associative array of options
  57. */
  58. public function __construct($options = [])
  59. {
  60. parent::__construct($options);
  61. if (empty($this->_options['adapter_callback'])) {
  62. if (!$this->_options['adapter'] instanceof \Magento\Framework\DB\Adapter\AdapterInterface) {
  63. \Zend_Cache::throwException(
  64. 'Option "adapter" should be declared and extend \Magento\Framework\DB\Adapter\AdapterInterface!'
  65. );
  66. }
  67. }
  68. if (empty($this->_options['data_table']) && empty($this->_options['data_table_callback'])) {
  69. \Zend_Cache::throwException('Option "data_table" or "data_table_callback" should be declared!');
  70. }
  71. if (empty($this->_options['tags_table']) && empty($this->_options['tags_table_callback'])) {
  72. \Zend_Cache::throwException('Option "tags_table" or "tags_table_callback" should be declared!');
  73. }
  74. }
  75. /**
  76. * Get DB adapter
  77. *
  78. * @return \Magento\Framework\DB\Adapter\AdapterInterface
  79. */
  80. protected function _getConnection()
  81. {
  82. if (!$this->_connection) {
  83. if (!empty($this->_options['adapter_callback'])) {
  84. $connection = call_user_func($this->_options['adapter_callback']);
  85. } else {
  86. $connection = $this->_options['adapter'];
  87. }
  88. if (!$connection instanceof \Magento\Framework\DB\Adapter\AdapterInterface) {
  89. \Zend_Cache::throwException(
  90. 'DB Adapter should be declared and extend \Magento\Framework\DB\Adapter\AdapterInterface'
  91. );
  92. } else {
  93. $this->_connection = $connection;
  94. }
  95. }
  96. return $this->_connection;
  97. }
  98. /**
  99. * Get table name where data is stored
  100. *
  101. * @return string
  102. */
  103. protected function _getDataTable()
  104. {
  105. if (empty($this->_options['data_table'])) {
  106. $this->setOption('data_table', call_user_func($this->_options['data_table_callback']));
  107. if (empty($this->_options['data_table'])) {
  108. \Zend_Cache::throwException('Failed to detect data_table option');
  109. }
  110. }
  111. return $this->_options['data_table'];
  112. }
  113. /**
  114. * Get table name where tags are stored
  115. *
  116. * @return string
  117. */
  118. protected function _getTagsTable()
  119. {
  120. if (empty($this->_options['tags_table'])) {
  121. $this->setOption('tags_table', call_user_func($this->_options['tags_table_callback']));
  122. if (empty($this->_options['tags_table'])) {
  123. \Zend_Cache::throwException('Failed to detect tags_table option');
  124. }
  125. }
  126. return $this->_options['tags_table'];
  127. }
  128. /**
  129. * Test if a cache is available for the given id and (if yes) return it (false else)
  130. *
  131. * Note : return value is always "string" (unserialization is done by the core not by the backend)
  132. *
  133. * @param string $id Cache id
  134. * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
  135. * @return string|false cached datas
  136. */
  137. public function load($id, $doNotTestCacheValidity = false)
  138. {
  139. if ($this->_options['store_data'] && !$this->_options['infinite_loop_flag']) {
  140. $this->_options['infinite_loop_flag'] = true;
  141. $select = $this->_getConnection()->select()->from(
  142. $this->_getDataTable(),
  143. 'data'
  144. )->where('id=:cache_id');
  145. if (!$doNotTestCacheValidity) {
  146. $select->where('expire_time=0 OR expire_time>?', time());
  147. }
  148. $result = $this->_getConnection()->fetchOne($select, ['cache_id' => $id]);
  149. $this->_options['infinite_loop_flag'] = false;
  150. return $result;
  151. } else {
  152. return false;
  153. }
  154. }
  155. /**
  156. * Test if a cache is available or not (for the given id)
  157. *
  158. * @param string $id cache id
  159. * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record
  160. */
  161. public function test($id)
  162. {
  163. if ($this->_options['store_data'] && !$this->_options['infinite_loop_flag']) {
  164. $this->_options['infinite_loop_flag'] = true;
  165. $select = $this->_getConnection()->select()->from(
  166. $this->_getDataTable(),
  167. 'update_time'
  168. )->where(
  169. 'id=:cache_id'
  170. )->where(
  171. 'expire_time=0 OR expire_time>?',
  172. time()
  173. );
  174. $result = $this->_getConnection()->fetchOne($select, ['cache_id' => $id]);
  175. $this->_options['infinite_loop_flag'] = false;
  176. return $result;
  177. } else {
  178. return false;
  179. }
  180. }
  181. /**
  182. * Save some string datas into a cache record
  183. *
  184. * Note : $data is always "string" (serialization is done by the
  185. * core not by the backend)
  186. *
  187. * @param string $data Datas to cache
  188. * @param string $id Cache id
  189. * @param string[] $tags Array of strings, the cache record will be tagged by each string entry
  190. * @param int|bool $specificLifetime Integer to set a specific lifetime or null for infinite lifetime
  191. * @return bool true if no problem
  192. */
  193. public function save($data, $id, $tags = [], $specificLifetime = false)
  194. {
  195. $result = false;
  196. if (!$this->_options['infinite_loop_flag']) {
  197. $this->_options['infinite_loop_flag'] = true;
  198. $result = true;
  199. if ($this->_options['store_data']) {
  200. $connection = $this->_getConnection();
  201. $dataTable = $this->_getDataTable();
  202. $lifetime = $this->getLifetime($specificLifetime);
  203. $time = time();
  204. $expire = $lifetime === 0 || $lifetime === null ? 0 : $time + $lifetime;
  205. $idCol = $connection->quoteIdentifier('id');
  206. $dataCol = $connection->quoteIdentifier('data');
  207. $createCol = $connection->quoteIdentifier('create_time');
  208. $updateCol = $connection->quoteIdentifier('update_time');
  209. $expireCol = $connection->quoteIdentifier('expire_time');
  210. $query = "INSERT INTO {$dataTable} ({$idCol}, {$dataCol}, {$createCol}, {$updateCol}, {$expireCol}) " .
  211. "VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE {$dataCol}=VALUES({$dataCol}), " .
  212. "{$updateCol}=VALUES({$updateCol}), {$expireCol}=VALUES({$expireCol})";
  213. $result = $connection->query($query, [$id, $data, $time, $time, $expire])->rowCount();
  214. }
  215. if ($result) {
  216. $result = $this->_saveTags($id, $tags);
  217. }
  218. $this->_options['infinite_loop_flag'] = false;
  219. }
  220. return $result;
  221. }
  222. /**
  223. * Remove a cache record
  224. *
  225. * @param string $id Cache id
  226. * @return boolean True if no problem
  227. */
  228. public function remove($id)
  229. {
  230. if ($this->_options['store_data'] && !$this->_options['infinite_loop_flag']) {
  231. $this->_options['infinite_loop_flag'] = true;
  232. $result = $this->_getConnection()->delete($this->_getDataTable(), ['id=?' => $id]);
  233. $this->_options['infinite_loop_flag'] = false;
  234. return $result;
  235. }
  236. return false;
  237. }
  238. /**
  239. * Clean some cache records
  240. *
  241. * Available modes are :
  242. * \Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used)
  243. * \Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used)
  244. * \Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags
  245. * ($tags can be an array of strings or a single string)
  246. * \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
  247. * ($tags can be an array of strings or a single string)
  248. * \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
  249. * ($tags can be an array of strings or a single string)
  250. *
  251. * @param string $mode Clean mode
  252. * @param string[] $tags Array of tags
  253. * @return boolean true if no problem
  254. */
  255. public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = [])
  256. {
  257. if (!$this->_options['infinite_loop_flag']) {
  258. $this->_options['infinite_loop_flag'] = true;
  259. $connection = $this->_getConnection();
  260. switch ($mode) {
  261. case \Zend_Cache::CLEANING_MODE_ALL:
  262. $result = $this->cleanAll($connection);
  263. break;
  264. case \Zend_Cache::CLEANING_MODE_OLD:
  265. $result = $this->cleanOld($connection);
  266. break;
  267. case \Zend_Cache::CLEANING_MODE_MATCHING_TAG:
  268. case \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
  269. case \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
  270. $result = $this->_cleanByTags($mode, $tags);
  271. break;
  272. default:
  273. \Zend_Cache::throwException('Invalid mode for clean() method');
  274. break;
  275. }
  276. $this->_options['infinite_loop_flag'] = false;
  277. }
  278. return $result;
  279. }
  280. /**
  281. * Return an array of stored cache ids
  282. *
  283. * @return string[] array of stored cache ids (string)
  284. */
  285. public function getIds()
  286. {
  287. if ($this->_options['store_data']) {
  288. $select = $this->_getConnection()->select()->from($this->_getDataTable(), 'id');
  289. return $this->_getConnection()->fetchCol($select);
  290. } else {
  291. return [];
  292. }
  293. }
  294. /**
  295. * Return an array of stored tags
  296. *
  297. * @return string[] array of stored tags (string)
  298. */
  299. public function getTags()
  300. {
  301. $select = $this->_getConnection()->select()->from($this->_getTagsTable(), 'tag')->distinct(true);
  302. return $this->_getConnection()->fetchCol($select);
  303. }
  304. /**
  305. * Return an array of stored cache ids which match given tags
  306. *
  307. * In case of multiple tags, a logical AND is made between tags
  308. *
  309. * @param string[] $tags array of tags
  310. * @return string[] array of matching cache ids (string)
  311. */
  312. public function getIdsMatchingTags($tags = [])
  313. {
  314. $select = $this->_getConnection()->select()->from(
  315. $this->_getTagsTable(),
  316. 'cache_id'
  317. )->distinct(
  318. true
  319. )->where(
  320. 'tag IN(?)',
  321. $tags
  322. )->group(
  323. 'cache_id'
  324. )->having(
  325. 'COUNT(cache_id)=' . count($tags)
  326. );
  327. return $this->_getConnection()->fetchCol($select);
  328. }
  329. /**
  330. * Return an array of stored cache ids which don't match given tags
  331. *
  332. * In case of multiple tags, a logical OR is made between tags
  333. *
  334. * @param string[] $tags array of tags
  335. * @return string[] array of not matching cache ids (string)
  336. */
  337. public function getIdsNotMatchingTags($tags = [])
  338. {
  339. return array_diff($this->getIds(), $this->getIdsMatchingAnyTags($tags));
  340. }
  341. /**
  342. * Return an array of stored cache ids which match any given tags
  343. *
  344. * In case of multiple tags, a logical AND is made between tags
  345. *
  346. * @param string[] $tags array of tags
  347. * @return string[] array of any matching cache ids (string)
  348. */
  349. public function getIdsMatchingAnyTags($tags = [])
  350. {
  351. $select = $this->_getConnection()->select()->from(
  352. $this->_getTagsTable(),
  353. 'cache_id'
  354. )->distinct(
  355. true
  356. )->where(
  357. 'tag IN(?)',
  358. $tags
  359. );
  360. return $this->_getConnection()->fetchCol($select);
  361. }
  362. /**
  363. * Return the filling percentage of the backend storage
  364. *
  365. * @return int integer between 0 and 100
  366. */
  367. public function getFillingPercentage()
  368. {
  369. return 1;
  370. }
  371. /**
  372. * Return an array of metadatas for the given cache id
  373. *
  374. * The array must include these keys :
  375. * - expire : the expire timestamp
  376. * - tags : a string array of tags
  377. * - mtime : timestamp of last modification time
  378. *
  379. * @param string $id cache id
  380. * @return array|false array of metadatas (false if the cache id is not found)
  381. */
  382. public function getMetadatas($id)
  383. {
  384. $select = $this->_getConnection()->select()->from($this->_getTagsTable(), 'tag')->where('cache_id=?', $id);
  385. $tags = $this->_getConnection()->fetchCol($select);
  386. $select = $this->_getConnection()->select()->from($this->_getDataTable())->where('id=?', $id);
  387. $data = $this->_getConnection()->fetchRow($select);
  388. $res = false;
  389. if ($data) {
  390. $res = ['expire' => $data['expire_time'], 'mtime' => $data['update_time'], 'tags' => $tags];
  391. }
  392. return $res;
  393. }
  394. /**
  395. * Give (if possible) an extra lifetime to the given cache id
  396. *
  397. * @param string $id cache id
  398. * @param int $extraLifetime
  399. * @return boolean true if ok
  400. */
  401. public function touch($id, $extraLifetime)
  402. {
  403. if ($this->_options['store_data']) {
  404. return $this->_getConnection()->update(
  405. $this->_getDataTable(),
  406. ['expire_time' => new \Zend_Db_Expr('expire_time+' . $extraLifetime)],
  407. ['id=?' => $id, 'expire_time = 0 OR expire_time>?' => time()]
  408. );
  409. } else {
  410. return true;
  411. }
  412. }
  413. /**
  414. * Return an associative array of capabilities (booleans) of the backend
  415. *
  416. * The array must include these keys :
  417. * - automatic_cleaning (is automating cleaning necessary)
  418. * - tags (are tags supported)
  419. * - expired_read (is it possible to read expired cache records
  420. * (for doNotTestCacheValidity option for example))
  421. * - priority does the backend deal with priority when saving
  422. * - infinite_lifetime (is infinite lifetime can work with this backend)
  423. * - get_list (is it possible to get the list of cache ids and the complete list of tags)
  424. *
  425. * @return array associative of with capabilities
  426. */
  427. public function getCapabilities()
  428. {
  429. return [
  430. 'automatic_cleaning' => true,
  431. 'tags' => true,
  432. 'expired_read' => true,
  433. 'priority' => false,
  434. 'infinite_lifetime' => true,
  435. 'get_list' => true
  436. ];
  437. }
  438. /**
  439. * Save tags related to specific id
  440. *
  441. * @param string $id
  442. * @param string[] $tags
  443. * @return bool
  444. */
  445. protected function _saveTags($id, $tags)
  446. {
  447. if (!is_array($tags)) {
  448. $tags = [$tags];
  449. }
  450. if (empty($tags)) {
  451. return true;
  452. }
  453. $connection = $this->_getConnection();
  454. $tagsTable = $this->_getTagsTable();
  455. $select = $connection->select()->from($tagsTable, 'tag')->where('cache_id=?', $id)->where('tag IN(?)', $tags);
  456. $existingTags = $connection->fetchCol($select);
  457. $insertTags = array_diff($tags, $existingTags);
  458. if (!empty($insertTags)) {
  459. $query = 'INSERT IGNORE INTO ' . $tagsTable . ' (tag, cache_id) VALUES ';
  460. $bind = [];
  461. $lines = [];
  462. foreach ($insertTags as $tag) {
  463. $lines[] = '(?, ?)';
  464. $bind[] = $tag;
  465. $bind[] = $id;
  466. }
  467. $query .= implode(',', $lines);
  468. $connection->query($query, $bind);
  469. }
  470. $result = true;
  471. return $result;
  472. }
  473. /**
  474. * Remove cache data by tags with specified mode
  475. *
  476. * @param string $mode
  477. * @param string[] $tags
  478. * @return bool
  479. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  480. */
  481. protected function _cleanByTags($mode, $tags)
  482. {
  483. if ($this->_options['store_data']) {
  484. $connection = $this->_getConnection();
  485. $select = $connection->select()->from($this->_getTagsTable(), 'cache_id');
  486. switch ($mode) {
  487. case \Zend_Cache::CLEANING_MODE_MATCHING_TAG:
  488. $select->where('tag IN (?)', $tags)->group('cache_id')->having('COUNT(cache_id)=' . count($tags));
  489. break;
  490. case \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
  491. $select->where('tag NOT IN (?)', $tags);
  492. break;
  493. case \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
  494. $select->where('tag IN (?)', $tags);
  495. break;
  496. default:
  497. \Zend_Cache::throwException('Invalid mode for _cleanByTags() method');
  498. break;
  499. }
  500. $result = true;
  501. $ids = [];
  502. $counter = 0;
  503. $stmt = $connection->query($select);
  504. while ($row = $stmt->fetch()) {
  505. $ids[] = $row['cache_id'];
  506. $counter++;
  507. if ($counter > 100) {
  508. $result = $result && $connection->delete($this->_getDataTable(), ['id IN (?)' => $ids]);
  509. $ids = [];
  510. $counter = 0;
  511. }
  512. }
  513. if (!empty($ids)) {
  514. $result = $result && $connection->delete($this->_getDataTable(), ['id IN (?)' => $ids]);
  515. }
  516. return $result;
  517. } else {
  518. return true;
  519. }
  520. }
  521. /**
  522. * Clean all cache entries
  523. *
  524. * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
  525. * @return bool
  526. */
  527. private function cleanAll(\Magento\Framework\DB\Adapter\AdapterInterface $connection)
  528. {
  529. if ($this->_options['store_data']) {
  530. $result = $connection->query('TRUNCATE TABLE ' . $this->_getDataTable());
  531. } else {
  532. $result = true;
  533. }
  534. $result = $result && $connection->query('TRUNCATE TABLE ' . $this->_getTagsTable());
  535. return $result;
  536. }
  537. /**
  538. * Clean old cache entries
  539. *
  540. * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
  541. * @return bool
  542. */
  543. private function cleanOld(\Magento\Framework\DB\Adapter\AdapterInterface $connection)
  544. {
  545. if ($this->_options['store_data']) {
  546. $result = $connection->delete(
  547. $this->_getDataTable(),
  548. ['expire_time> ?' => 0, 'expire_time<= ?' => time()]
  549. );
  550. return $result;
  551. } else {
  552. $result = true;
  553. return $result;
  554. }
  555. }
  556. }