123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592 |
- <?php
- /**
- * Copyright © Magento, Inc. All rights reserved.
- * See COPYING.txt for license details.
- */
- /**
- * Tables declaration:
- *
- * CREATE TABLE IF NOT EXISTS `cache` (
- * `id` VARCHAR(255) NOT NULL,
- * `data` mediumblob,
- * `create_time` int(11),
- * `update_time` int(11),
- * `expire_time` int(11),
- * PRIMARY KEY (`id`),
- * KEY `IDX_EXPIRE_TIME` (`expire_time`)
- * )ENGINE=InnoDB DEFAULT CHARSET=utf8;
- *
- * CREATE TABLE IF NOT EXISTS `cache_tag` (
- * `tag` VARCHAR(255) NOT NULL,
- * `cache_id` VARCHAR(255) NOT NULL,
- * KEY `IDX_TAG` (`tag`),
- * KEY `IDX_CACHE_ID` (`cache_id`),
- * CONSTRAINT `FK_CORE_CACHE_TAG` FOREIGN KEY (`cache_id`)
- * REFERENCES `cache` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
- * ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
- */
- namespace Magento\Framework\Cache\Backend;
- /**
- * Database cache backend.
- */
- class Database extends \Zend_Cache_Backend implements \Zend_Cache_Backend_ExtendedInterface
- {
- /**
- * Available options
- *
- * @var array available options
- */
- protected $_options = [
- 'adapter' => '',
- 'adapter_callback' => '',
- 'data_table' => '',
- 'data_table_callback' => '',
- 'tags_table' => '',
- 'tags_table_callback' => '',
- 'store_data' => true,
- 'infinite_loop_flag' => false,
- ];
- /**
- * @var \Magento\Framework\DB\Adapter\AdapterInterface
- */
- protected $_connection = null;
- /**
- * Constructor
- *
- * @param array $options associative array of options
- */
- public function __construct($options = [])
- {
- parent::__construct($options);
- if (empty($this->_options['adapter_callback'])) {
- if (!$this->_options['adapter'] instanceof \Magento\Framework\DB\Adapter\AdapterInterface) {
- \Zend_Cache::throwException(
- 'Option "adapter" should be declared and extend \Magento\Framework\DB\Adapter\AdapterInterface!'
- );
- }
- }
- if (empty($this->_options['data_table']) && empty($this->_options['data_table_callback'])) {
- \Zend_Cache::throwException('Option "data_table" or "data_table_callback" should be declared!');
- }
- if (empty($this->_options['tags_table']) && empty($this->_options['tags_table_callback'])) {
- \Zend_Cache::throwException('Option "tags_table" or "tags_table_callback" should be declared!');
- }
- }
- /**
- * Get DB adapter
- *
- * @return \Magento\Framework\DB\Adapter\AdapterInterface
- */
- protected function _getConnection()
- {
- if (!$this->_connection) {
- if (!empty($this->_options['adapter_callback'])) {
- $connection = call_user_func($this->_options['adapter_callback']);
- } else {
- $connection = $this->_options['adapter'];
- }
- if (!$connection instanceof \Magento\Framework\DB\Adapter\AdapterInterface) {
- \Zend_Cache::throwException(
- 'DB Adapter should be declared and extend \Magento\Framework\DB\Adapter\AdapterInterface'
- );
- } else {
- $this->_connection = $connection;
- }
- }
- return $this->_connection;
- }
- /**
- * Get table name where data is stored
- *
- * @return string
- */
- protected function _getDataTable()
- {
- if (empty($this->_options['data_table'])) {
- $this->setOption('data_table', call_user_func($this->_options['data_table_callback']));
- if (empty($this->_options['data_table'])) {
- \Zend_Cache::throwException('Failed to detect data_table option');
- }
- }
- return $this->_options['data_table'];
- }
- /**
- * Get table name where tags are stored
- *
- * @return string
- */
- protected function _getTagsTable()
- {
- if (empty($this->_options['tags_table'])) {
- $this->setOption('tags_table', call_user_func($this->_options['tags_table_callback']));
- if (empty($this->_options['tags_table'])) {
- \Zend_Cache::throwException('Failed to detect tags_table option');
- }
- }
- return $this->_options['tags_table'];
- }
- /**
- * Test if a cache is available for the given id and (if yes) return it (false else)
- *
- * Note : return value is always "string" (unserialization is done by the core not by the backend)
- *
- * @param string $id Cache id
- * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
- * @return string|false cached datas
- */
- public function load($id, $doNotTestCacheValidity = false)
- {
- if ($this->_options['store_data'] && !$this->_options['infinite_loop_flag']) {
- $this->_options['infinite_loop_flag'] = true;
- $select = $this->_getConnection()->select()->from(
- $this->_getDataTable(),
- 'data'
- )->where('id=:cache_id');
- if (!$doNotTestCacheValidity) {
- $select->where('expire_time=0 OR expire_time>?', time());
- }
- $result = $this->_getConnection()->fetchOne($select, ['cache_id' => $id]);
- $this->_options['infinite_loop_flag'] = false;
- return $result;
- } else {
- return false;
- }
- }
- /**
- * Test if a cache is available or not (for the given id)
- *
- * @param string $id cache id
- * @return mixed|false (a cache is not available) or "last modified" timestamp (int) of the available cache record
- */
- public function test($id)
- {
- if ($this->_options['store_data'] && !$this->_options['infinite_loop_flag']) {
- $this->_options['infinite_loop_flag'] = true;
- $select = $this->_getConnection()->select()->from(
- $this->_getDataTable(),
- 'update_time'
- )->where(
- 'id=:cache_id'
- )->where(
- 'expire_time=0 OR expire_time>?',
- time()
- );
- $result = $this->_getConnection()->fetchOne($select, ['cache_id' => $id]);
- $this->_options['infinite_loop_flag'] = false;
- return $result;
- } else {
- return false;
- }
- }
- /**
- * Save some string datas into a cache record
- *
- * Note : $data is always "string" (serialization is done by the
- * core not by the backend)
- *
- * @param string $data Datas to cache
- * @param string $id Cache id
- * @param string[] $tags Array of strings, the cache record will be tagged by each string entry
- * @param int|bool $specificLifetime Integer to set a specific lifetime or null for infinite lifetime
- * @return bool true if no problem
- */
- public function save($data, $id, $tags = [], $specificLifetime = false)
- {
- $result = false;
- if (!$this->_options['infinite_loop_flag']) {
- $this->_options['infinite_loop_flag'] = true;
- $result = true;
- if ($this->_options['store_data']) {
- $connection = $this->_getConnection();
- $dataTable = $this->_getDataTable();
- $lifetime = $this->getLifetime($specificLifetime);
- $time = time();
- $expire = $lifetime === 0 || $lifetime === null ? 0 : $time + $lifetime;
- $idCol = $connection->quoteIdentifier('id');
- $dataCol = $connection->quoteIdentifier('data');
- $createCol = $connection->quoteIdentifier('create_time');
- $updateCol = $connection->quoteIdentifier('update_time');
- $expireCol = $connection->quoteIdentifier('expire_time');
- $query = "INSERT INTO {$dataTable} ({$idCol}, {$dataCol}, {$createCol}, {$updateCol}, {$expireCol}) " .
- "VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE {$dataCol}=VALUES({$dataCol}), " .
- "{$updateCol}=VALUES({$updateCol}), {$expireCol}=VALUES({$expireCol})";
- $result = $connection->query($query, [$id, $data, $time, $time, $expire])->rowCount();
- }
- if ($result) {
- $result = $this->_saveTags($id, $tags);
- }
- $this->_options['infinite_loop_flag'] = false;
- }
- return $result;
- }
- /**
- * Remove a cache record
- *
- * @param string $id Cache id
- * @return boolean True if no problem
- */
- public function remove($id)
- {
- if ($this->_options['store_data'] && !$this->_options['infinite_loop_flag']) {
- $this->_options['infinite_loop_flag'] = true;
- $result = $this->_getConnection()->delete($this->_getDataTable(), ['id=?' => $id]);
- $this->_options['infinite_loop_flag'] = false;
- return $result;
- }
- return false;
- }
- /**
- * Clean some cache records
- *
- * Available modes are :
- * \Zend_Cache::CLEANING_MODE_ALL (default) => remove all cache entries ($tags is not used)
- * \Zend_Cache::CLEANING_MODE_OLD => remove too old cache entries ($tags is not used)
- * \Zend_Cache::CLEANING_MODE_MATCHING_TAG => remove cache entries matching all given tags
- * ($tags can be an array of strings or a single string)
- * \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG => remove cache entries not {matching one of the given tags}
- * ($tags can be an array of strings or a single string)
- * \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG => remove cache entries matching any given tags
- * ($tags can be an array of strings or a single string)
- *
- * @param string $mode Clean mode
- * @param string[] $tags Array of tags
- * @return boolean true if no problem
- */
- public function clean($mode = \Zend_Cache::CLEANING_MODE_ALL, $tags = [])
- {
- if (!$this->_options['infinite_loop_flag']) {
- $this->_options['infinite_loop_flag'] = true;
- $connection = $this->_getConnection();
- switch ($mode) {
- case \Zend_Cache::CLEANING_MODE_ALL:
- $result = $this->cleanAll($connection);
- break;
- case \Zend_Cache::CLEANING_MODE_OLD:
- $result = $this->cleanOld($connection);
- break;
- case \Zend_Cache::CLEANING_MODE_MATCHING_TAG:
- case \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
- case \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
- $result = $this->_cleanByTags($mode, $tags);
- break;
- default:
- \Zend_Cache::throwException('Invalid mode for clean() method');
- break;
- }
- $this->_options['infinite_loop_flag'] = false;
- }
- return $result;
- }
- /**
- * Return an array of stored cache ids
- *
- * @return string[] array of stored cache ids (string)
- */
- public function getIds()
- {
- if ($this->_options['store_data']) {
- $select = $this->_getConnection()->select()->from($this->_getDataTable(), 'id');
- return $this->_getConnection()->fetchCol($select);
- } else {
- return [];
- }
- }
- /**
- * Return an array of stored tags
- *
- * @return string[] array of stored tags (string)
- */
- public function getTags()
- {
- $select = $this->_getConnection()->select()->from($this->_getTagsTable(), 'tag')->distinct(true);
- return $this->_getConnection()->fetchCol($select);
- }
- /**
- * Return an array of stored cache ids which match given tags
- *
- * In case of multiple tags, a logical AND is made between tags
- *
- * @param string[] $tags array of tags
- * @return string[] array of matching cache ids (string)
- */
- public function getIdsMatchingTags($tags = [])
- {
- $select = $this->_getConnection()->select()->from(
- $this->_getTagsTable(),
- 'cache_id'
- )->distinct(
- true
- )->where(
- 'tag IN(?)',
- $tags
- )->group(
- 'cache_id'
- )->having(
- 'COUNT(cache_id)=' . count($tags)
- );
- return $this->_getConnection()->fetchCol($select);
- }
- /**
- * Return an array of stored cache ids which don't match given tags
- *
- * In case of multiple tags, a logical OR is made between tags
- *
- * @param string[] $tags array of tags
- * @return string[] array of not matching cache ids (string)
- */
- public function getIdsNotMatchingTags($tags = [])
- {
- return array_diff($this->getIds(), $this->getIdsMatchingAnyTags($tags));
- }
- /**
- * Return an array of stored cache ids which match any given tags
- *
- * In case of multiple tags, a logical AND is made between tags
- *
- * @param string[] $tags array of tags
- * @return string[] array of any matching cache ids (string)
- */
- public function getIdsMatchingAnyTags($tags = [])
- {
- $select = $this->_getConnection()->select()->from(
- $this->_getTagsTable(),
- 'cache_id'
- )->distinct(
- true
- )->where(
- 'tag IN(?)',
- $tags
- );
- return $this->_getConnection()->fetchCol($select);
- }
- /**
- * Return the filling percentage of the backend storage
- *
- * @return int integer between 0 and 100
- */
- public function getFillingPercentage()
- {
- return 1;
- }
- /**
- * Return an array of metadatas for the given cache id
- *
- * The array must include these keys :
- * - expire : the expire timestamp
- * - tags : a string array of tags
- * - mtime : timestamp of last modification time
- *
- * @param string $id cache id
- * @return array|false array of metadatas (false if the cache id is not found)
- */
- public function getMetadatas($id)
- {
- $select = $this->_getConnection()->select()->from($this->_getTagsTable(), 'tag')->where('cache_id=?', $id);
- $tags = $this->_getConnection()->fetchCol($select);
- $select = $this->_getConnection()->select()->from($this->_getDataTable())->where('id=?', $id);
- $data = $this->_getConnection()->fetchRow($select);
- $res = false;
- if ($data) {
- $res = ['expire' => $data['expire_time'], 'mtime' => $data['update_time'], 'tags' => $tags];
- }
- return $res;
- }
- /**
- * Give (if possible) an extra lifetime to the given cache id
- *
- * @param string $id cache id
- * @param int $extraLifetime
- * @return boolean true if ok
- */
- public function touch($id, $extraLifetime)
- {
- if ($this->_options['store_data']) {
- return $this->_getConnection()->update(
- $this->_getDataTable(),
- ['expire_time' => new \Zend_Db_Expr('expire_time+' . $extraLifetime)],
- ['id=?' => $id, 'expire_time = 0 OR expire_time>?' => time()]
- );
- } else {
- return true;
- }
- }
- /**
- * Return an associative array of capabilities (booleans) of the backend
- *
- * The array must include these keys :
- * - automatic_cleaning (is automating cleaning necessary)
- * - tags (are tags supported)
- * - expired_read (is it possible to read expired cache records
- * (for doNotTestCacheValidity option for example))
- * - priority does the backend deal with priority when saving
- * - infinite_lifetime (is infinite lifetime can work with this backend)
- * - get_list (is it possible to get the list of cache ids and the complete list of tags)
- *
- * @return array associative of with capabilities
- */
- public function getCapabilities()
- {
- return [
- 'automatic_cleaning' => true,
- 'tags' => true,
- 'expired_read' => true,
- 'priority' => false,
- 'infinite_lifetime' => true,
- 'get_list' => true
- ];
- }
- /**
- * Save tags related to specific id
- *
- * @param string $id
- * @param string[] $tags
- * @return bool
- */
- protected function _saveTags($id, $tags)
- {
- if (!is_array($tags)) {
- $tags = [$tags];
- }
- if (empty($tags)) {
- return true;
- }
- $connection = $this->_getConnection();
- $tagsTable = $this->_getTagsTable();
- $select = $connection->select()->from($tagsTable, 'tag')->where('cache_id=?', $id)->where('tag IN(?)', $tags);
- $existingTags = $connection->fetchCol($select);
- $insertTags = array_diff($tags, $existingTags);
- if (!empty($insertTags)) {
- $query = 'INSERT IGNORE INTO ' . $tagsTable . ' (tag, cache_id) VALUES ';
- $bind = [];
- $lines = [];
- foreach ($insertTags as $tag) {
- $lines[] = '(?, ?)';
- $bind[] = $tag;
- $bind[] = $id;
- }
- $query .= implode(',', $lines);
- $connection->query($query, $bind);
- }
- $result = true;
- return $result;
- }
- /**
- * Remove cache data by tags with specified mode
- *
- * @param string $mode
- * @param string[] $tags
- * @return bool
- * @SuppressWarnings(PHPMD.CyclomaticComplexity)
- */
- protected function _cleanByTags($mode, $tags)
- {
- if ($this->_options['store_data']) {
- $connection = $this->_getConnection();
- $select = $connection->select()->from($this->_getTagsTable(), 'cache_id');
- switch ($mode) {
- case \Zend_Cache::CLEANING_MODE_MATCHING_TAG:
- $select->where('tag IN (?)', $tags)->group('cache_id')->having('COUNT(cache_id)=' . count($tags));
- break;
- case \Zend_Cache::CLEANING_MODE_NOT_MATCHING_TAG:
- $select->where('tag NOT IN (?)', $tags);
- break;
- case \Zend_Cache::CLEANING_MODE_MATCHING_ANY_TAG:
- $select->where('tag IN (?)', $tags);
- break;
- default:
- \Zend_Cache::throwException('Invalid mode for _cleanByTags() method');
- break;
- }
- $result = true;
- $ids = [];
- $counter = 0;
- $stmt = $connection->query($select);
- while ($row = $stmt->fetch()) {
- $ids[] = $row['cache_id'];
- $counter++;
- if ($counter > 100) {
- $result = $result && $connection->delete($this->_getDataTable(), ['id IN (?)' => $ids]);
- $ids = [];
- $counter = 0;
- }
- }
- if (!empty($ids)) {
- $result = $result && $connection->delete($this->_getDataTable(), ['id IN (?)' => $ids]);
- }
- return $result;
- } else {
- return true;
- }
- }
- /**
- * Clean all cache entries
- *
- * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
- * @return bool
- */
- private function cleanAll(\Magento\Framework\DB\Adapter\AdapterInterface $connection)
- {
- if ($this->_options['store_data']) {
- $result = $connection->query('TRUNCATE TABLE ' . $this->_getDataTable());
- } else {
- $result = true;
- }
- $result = $result && $connection->query('TRUNCATE TABLE ' . $this->_getTagsTable());
- return $result;
- }
- /**
- * Clean old cache entries
- *
- * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection
- * @return bool
- */
- private function cleanOld(\Magento\Framework\DB\Adapter\AdapterInterface $connection)
- {
- if ($this->_options['store_data']) {
- $result = $connection->delete(
- $this->_getDataTable(),
- ['expire_time> ?' => 0, 'expire_time<= ?' => time()]
- );
- return $result;
- } else {
- $result = true;
- return $result;
- }
- }
- }
|