3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
9 +--------------------------------------------------------------------+
13 * Class CRM_Core_PrevNextCache_Memory
15 * Store the previous/next cache in a Redis set.
17 * Each logical prev-next cache corresponds to three distinct items in Redis:
18 * - "{prefix}/{qfKey}/list" - Sorted set of `entity_id`, with all entities
19 * - "{prefix}/{qfkey}/sel" - Sorted set of `entity_id`, with only entities marked by user
20 * - "{prefix}/{qfkey}/data" - Hash mapping from `entity_id` to `data`
22 * @link https://github.com/phpredis/phpredis
24 class CRM_Core_PrevNextCache_Redis
implements CRM_Core_PrevNextCache_Interface
{
39 * CRM_Core_PrevNextCache_Redis constructor.
40 * @param array $settings
42 public function __construct($settings) {
43 $this->redis
= CRM_Utils_Cache_Redis
::connect($settings);
44 $this->prefix
= isset($settings['prefix']) ?
$settings['prefix'] : '';
45 $this->prefix
.= \CRM_Utils_Cache
::DELIMITER
. 'prevnext' . \CRM_Utils_Cache
::DELIMITER
;
48 public function fillWithSql($cacheKey, $sql, $sqlParams = []) {
49 $dao = CRM_Core_DAO
::executeQuery($sql, $sqlParams, FALSE, NULL, FALSE, TRUE, TRUE);
50 if (is_a($dao, 'DB_Error')) {
51 throw new CRM_Core_Exception($dao->message
);
54 list($allKey, $dataKey, , $maxScore) = $this->initCacheKey($cacheKey);
56 while ($dao->fetch()) {
57 list (, $entity_id, $data) = array_values($dao->toArray());
59 $this->redis
->zAdd($allKey, $maxScore, $entity_id);
60 $this->redis
->hSet($dataKey, $entity_id, $data);
66 public function fillWithArray($cacheKey, $rows) {
67 list($allKey, $dataKey, , $maxScore) = $this->initCacheKey($cacheKey);
69 foreach ($rows as $row) {
71 $this->redis
->zAdd($allKey, $maxScore, $row['entity_id1']);
72 $this->redis
->hSet($dataKey, $row['entity_id1'], $row['data']);
78 public function fetch($cacheKey, $offset, $rowCount) {
79 $allKey = $this->key($cacheKey, 'all');
80 return $this->redis
->zRange($allKey, $offset, $offset +
$rowCount - 1);
83 public function markSelection($cacheKey, $action, $ids = NULL) {
84 $allKey = $this->key($cacheKey, 'all');
85 $selKey = $this->key($cacheKey, 'sel');
87 if ($action === 'select') {
88 foreach ((array) $ids as $id) {
89 $score = $this->redis
->zScore($allKey, $id);
90 $this->redis
->zAdd($selKey, $score, $id);
93 elseif ($action === 'unselect' && $ids === NULL) {
94 $this->redis
->delete($selKey);
95 $this->redis
->setTimeout($selKey, self
::TTL
);
97 elseif ($action === 'unselect' && $ids !== NULL) {
98 foreach ((array) $ids as $id) {
99 $this->redis
->zDelete($selKey, $id);
104 public function getSelection($cacheKey, $action = 'get') {
105 $allKey = $this->key($cacheKey, 'all');
106 $selKey = $this->key($cacheKey, 'sel');
108 if ($action === 'get') {
110 foreach ($this->redis
->zRange($selKey, 0, -1) as $entity_id) {
111 $result[$entity_id] = 1;
113 return [$cacheKey => $result];
115 elseif ($action === 'getall') {
117 foreach ($this->redis
->zRange($allKey, 0, -1) as $entity_id) {
118 $result[$entity_id] = 1;
120 return [$cacheKey => $result];
123 throw new \
CRM_Core_Exception("Unrecognized action: $action");
127 public function getPositions($cacheKey, $id1) {
128 $allKey = $this->key($cacheKey, 'all');
129 $dataKey = $this->key($cacheKey, 'data');
131 $rank = $this->redis
->zRank($allKey, $id1);
132 if (!is_int($rank) ||
$rank < 0) {
133 return ['foundEntry' => 0];
136 $pos = ['foundEntry' => 1];
140 foreach ($this->redis
->zRange($allKey, $rank - 1, $rank - 1) as $value) {
141 $pos['prev']['id1'] = $value;
143 $pos['prev']['data'] = $this->redis
->hGet($dataKey, $pos['prev']['id1']);
146 $count = $this->getCount($cacheKey);
147 if ($count > $rank +
1) {
149 foreach ($this->redis
->zRange($allKey, $rank +
1, $rank +
1) as $value) {
150 $pos['next']['id1'] = $value;
152 $pos['next']['data'] = $this->redis
->hGet($dataKey, $pos['next']['id1']);
158 public function deleteItem($id = NULL, $cacheKey = NULL) {
159 if ($id === NULL && $cacheKey !== NULL) {
160 // Delete by cacheKey.
161 $allKey = $this->key($cacheKey, 'all');
162 $selKey = $this->key($cacheKey, 'sel');
163 $dataKey = $this->key($cacheKey, 'data');
164 $this->redis
->delete($allKey, $selKey, $dataKey);
166 elseif ($id === NULL && $cacheKey === NULL) {
167 // Delete everything.
168 $keys = $this->redis
->keys($this->prefix
. '*');
169 $this->redis
->del($keys);
171 elseif ($id !== NULL && $cacheKey !== NULL) {
172 // Delete a specific contact, within a specific cache.
173 $this->redis
->zDelete($this->key($cacheKey, 'all'), $id);
174 $this->redis
->zDelete($this->key($cacheKey, 'sel'), $id);
175 $this->redis
->hDel($this->key($cacheKey, 'data'), $id);
177 elseif ($id !== NULL && $cacheKey === NULL) {
178 // Delete a specific contact, across all prevnext caches.
179 $allKeys = $this->redis
->keys($this->key('*', 'all'));
180 foreach ($allKeys as $allKey) {
181 $parts = explode(\CRM_Utils_Cache
::DELIMITER
, $allKey);
183 $tmpCacheKey = array_pop($parts);
184 $this->deleteItem($id, $tmpCacheKey);
188 throw new CRM_Core_Exception("Not implemented: Redis::deleteItem");
192 public function getCount($cacheKey) {
193 $allKey = $this->key($cacheKey, 'all');
194 return $this->redis
->zSize($allKey);
198 * Construct the full path to a cache item.
200 * @param string $cacheKey
201 * Identifier for this saved search.
202 * Ex: 'abcd1234abcd1234'.
203 * @param string $item
204 * Ex: 'list', 'rel', 'data'.
206 * Ex: 'dmaster/prevnext/abcd1234abcd1234/list'
208 private function key($cacheKey, $item) {
209 return $this->prefix
. $cacheKey . \CRM_Utils_Cache
::DELIMITER
. $item;
213 * Initialize any data-structures or timeouts for the cache-key.
215 * This is non-destructive -- if data already exists, it's preserved.
218 * 0 => string $allItemsCacheKey,
219 * 1 => string $dataItemsCacheKey,
220 * 2 => string $selectedItemsCacheKey,
221 * 3 => int $maxExistingScore
223 private function initCacheKey($cacheKey) {
224 $allKey = $this->key($cacheKey, 'all');
225 $selKey = $this->key($cacheKey, 'sel');
226 $dataKey = $this->key($cacheKey, 'data');
228 $this->redis
->setTimeout($allKey, self
::TTL
);
229 $this->redis
->setTimeout($dataKey, self
::TTL
);
230 $this->redis
->setTimeout($selKey, self
::TTL
);
233 foreach ($this->redis
->zRange($allKey, -1, -1, TRUE) as $lastElem => $lastScore) {
234 $maxScore = $lastScore;
236 return [$allKey, $dataKey, $selKey, $maxScore];
242 public function cleanup() {
243 // Redis already handles cleaning up stale keys.