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
= $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);
51 [$allKey, $dataKey, , $maxScore] = $this->initCacheKey($cacheKey);
53 while ($dao->fetch()) {
54 [, $entity_id, $data] = array_values($dao->toArray());
56 $this->redis
->zAdd($allKey, $maxScore, $entity_id);
57 $this->redis
->hSet($dataKey, $entity_id, $data);
63 public function fillWithArray($cacheKey, $rows) {
64 [$allKey, $dataKey, , $maxScore] = $this->initCacheKey($cacheKey);
66 foreach ($rows as $row) {
68 $this->redis
->zAdd($allKey, $maxScore, $row['entity_id1']);
69 $this->redis
->hSet($dataKey, $row['entity_id1'], $row['data']);
75 public function fetch($cacheKey, $offset, $rowCount) {
76 $allKey = $this->key($cacheKey, 'all');
77 return $this->redis
->zRange($allKey, $offset, $offset +
$rowCount - 1);
80 public function markSelection($cacheKey, $action, $ids = NULL) {
81 $allKey = $this->key($cacheKey, 'all');
82 $selKey = $this->key($cacheKey, 'sel');
84 if ($action === 'select') {
85 foreach ((array) $ids as $id) {
86 $score = $this->redis
->zScore($allKey, $id);
87 $this->redis
->zAdd($selKey, $score, $id);
90 elseif ($action === 'unselect' && $ids === NULL) {
91 $this->redis
->del($selKey);
92 $this->redis
->expire($selKey, self
::TTL
);
94 elseif ($action === 'unselect' && $ids !== NULL) {
95 foreach ((array) $ids as $id) {
96 $this->redis
->zRem($selKey, $id);
102 * @throws \CRM_Core_Exception
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']);
159 * @throws \CRM_Core_Exception
161 public function deleteItem($id = NULL, $cacheKey = NULL) {
162 if ($id === NULL && $cacheKey !== NULL) {
163 // Delete by cacheKey.
164 $allKey = $this->key($cacheKey, 'all');
165 $selKey = $this->key($cacheKey, 'sel');
166 $dataKey = $this->key($cacheKey, 'data');
167 $this->redis
->del($allKey, $selKey, $dataKey);
169 elseif ($id === NULL && $cacheKey === NULL) {
170 // Delete everything.
171 $keys = $this->redis
->keys($this->prefix
. '*');
172 $this->redis
->del($keys);
174 elseif ($id !== NULL && $cacheKey !== NULL) {
175 // Delete a specific contact, within a specific cache.
176 $this->redis
->zRem($this->key($cacheKey, 'all'), $id);
177 $this->redis
->zRem($this->key($cacheKey, 'sel'), $id);
178 $this->redis
->hDel($this->key($cacheKey, 'data'), $id);
180 elseif ($id !== NULL && $cacheKey === NULL) {
181 // Delete a specific contact, across all prevnext caches.
182 $allKeys = $this->redis
->keys($this->key('*', 'all'));
183 foreach ($allKeys as $allKey) {
184 $parts = explode(\CRM_Utils_Cache
::DELIMITER
, $allKey);
186 $tmpCacheKey = array_pop($parts);
187 $this->deleteItem($id, $tmpCacheKey);
191 throw new CRM_Core_Exception('Not implemented: Redis::deleteItem');
195 public function getCount($cacheKey) {
196 $allKey = $this->key($cacheKey, 'all');
197 return $this->redis
->zCard($allKey);
201 * Construct the full path to a cache item.
203 * @param string $cacheKey
204 * Identifier for this saved search.
205 * Ex: 'abcd1234abcd1234'.
206 * @param string $item
207 * Ex: 'list', 'rel', 'data'.
209 * Ex: 'dmaster/prevnext/abcd1234abcd1234/list'
211 private function key($cacheKey, $item) {
212 return $this->prefix
. $cacheKey . \CRM_Utils_Cache
::DELIMITER
. $item;
216 * Initialize any data-structures or timeouts for the cache-key.
218 * This is non-destructive -- if data already exists, it's preserved.
221 * 0 => string $allItemsCacheKey,
222 * 1 => string $dataItemsCacheKey,
223 * 2 => string $selectedItemsCacheKey,
224 * 3 => int $maxExistingScore
226 private function initCacheKey($cacheKey) {
227 $allKey = $this->key($cacheKey, 'all');
228 $selKey = $this->key($cacheKey, 'sel');
229 $dataKey = $this->key($cacheKey, 'data');
231 $this->redis
->expire($allKey, self
::TTL
);
232 $this->redis
->expire($dataKey, self
::TTL
);
233 $this->redis
->expire($selKey, self
::TTL
);
236 foreach ($this->redis
->zRange($allKey, -1, -1, TRUE) as $lastElem => $lastScore) {
237 $maxScore = $lastScore;
239 return [$allKey, $dataKey, $selKey, $maxScore];
245 public function cleanup() {
246 // Redis already handles cleaning up stale keys.