Merge pull request #15759 from tunbola/active-option-values-for-custom-group
[civicrm-core.git] / CRM / Core / PrevNextCache / Redis.php
CommitLineData
751f3d98
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
751f3d98 5 | |
bc77d7c0
TO
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 |
751f3d98
TO
9 +--------------------------------------------------------------------+
10 */
11
12/**
13 * Class CRM_Core_PrevNextCache_Memory
14 *
15 * Store the previous/next cache in a Redis set.
16 *
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`
21 *
22 * @link https://github.com/phpredis/phpredis
23 */
24class CRM_Core_PrevNextCache_Redis implements CRM_Core_PrevNextCache_Interface {
25
26 const TTL = 21600;
27
28 /**
29 * @var Redis
30 */
31 protected $redis;
32
33 /**
34 * @var string
35 */
36 protected $prefix;
37
38 /**
39 * CRM_Core_PrevNextCache_Redis constructor.
40 * @param array $settings
41 */
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;
46 }
47
2c8b42d5
SL
48 public function fillWithSql($cacheKey, $sql, $sqlParams = []) {
49 $dao = CRM_Core_DAO::executeQuery($sql, $sqlParams, FALSE, NULL, FALSE, TRUE, TRUE);
751f3d98
TO
50 if (is_a($dao, 'DB_Error')) {
51 throw new CRM_Core_Exception($dao->message);
52 }
53
54 list($allKey, $dataKey, , $maxScore) = $this->initCacheKey($cacheKey);
55
56 while ($dao->fetch()) {
57 list (, $entity_id, $data) = array_values($dao->toArray());
58 $maxScore++;
59 $this->redis->zAdd($allKey, $maxScore, $entity_id);
60 $this->redis->hSet($dataKey, $entity_id, $data);
61 }
62
751f3d98
TO
63 return TRUE;
64 }
65
66 public function fillWithArray($cacheKey, $rows) {
67 list($allKey, $dataKey, , $maxScore) = $this->initCacheKey($cacheKey);
68
69 foreach ($rows as $row) {
70 $maxScore++;
71 $this->redis->zAdd($allKey, $maxScore, $row['entity_id1']);
72 $this->redis->hSet($dataKey, $row['entity_id1'], $row['data']);
73 }
74
75 return TRUE;
76 }
77
78 public function fetch($cacheKey, $offset, $rowCount) {
79 $allKey = $this->key($cacheKey, 'all');
80 return $this->redis->zRange($allKey, $offset, $offset + $rowCount - 1);
81 }
82
83 public function markSelection($cacheKey, $action, $ids = NULL) {
84 $allKey = $this->key($cacheKey, 'all');
85 $selKey = $this->key($cacheKey, 'sel');
86
87 if ($action === 'select') {
88 foreach ((array) $ids as $id) {
89 $score = $this->redis->zScore($allKey, $id);
90 $this->redis->zAdd($selKey, $score, $id);
91 }
92 }
93 elseif ($action === 'unselect' && $ids === NULL) {
94 $this->redis->delete($selKey);
95 $this->redis->setTimeout($selKey, self::TTL);
96 }
97 elseif ($action === 'unselect' && $ids !== NULL) {
98 foreach ((array) $ids as $id) {
99 $this->redis->zDelete($selKey, $id);
100 }
101 }
102 }
103
104 public function getSelection($cacheKey, $action = 'get') {
105 $allKey = $this->key($cacheKey, 'all');
106 $selKey = $this->key($cacheKey, 'sel');
107
108 if ($action === 'get') {
109 $result = [];
110 foreach ($this->redis->zRange($selKey, 0, -1) as $entity_id) {
111 $result[$entity_id] = 1;
112 }
113 return [$cacheKey => $result];
114 }
115 elseif ($action === 'getall') {
116 $result = [];
117 foreach ($this->redis->zRange($allKey, 0, -1) as $entity_id) {
118 $result[$entity_id] = 1;
119 }
120 return [$cacheKey => $result];
121 }
122 else {
123 throw new \CRM_Core_Exception("Unrecognized action: $action");
124 }
125 }
126
127 public function getPositions($cacheKey, $id1) {
128 $allKey = $this->key($cacheKey, 'all');
129 $dataKey = $this->key($cacheKey, 'data');
130
131 $rank = $this->redis->zRank($allKey, $id1);
132 if (!is_int($rank) || $rank < 0) {
133 return ['foundEntry' => 0];
134 }
135
136 $pos = ['foundEntry' => 1];
137
138 if ($rank > 0) {
139 $pos['prev'] = [];
140 foreach ($this->redis->zRange($allKey, $rank - 1, $rank - 1) as $value) {
141 $pos['prev']['id1'] = $value;
142 }
143 $pos['prev']['data'] = $this->redis->hGet($dataKey, $pos['prev']['id1']);
144 }
145
146 $count = $this->getCount($cacheKey);
147 if ($count > $rank + 1) {
148 $pos['next'] = [];
149 foreach ($this->redis->zRange($allKey, $rank + 1, $rank + 1) as $value) {
150 $pos['next']['id1'] = $value;
151 }
152 $pos['next']['data'] = $this->redis->hGet($dataKey, $pos['next']['id1']);
153 }
154
155 return $pos;
156 }
157
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);
165 }
166 elseif ($id === NULL && $cacheKey === NULL) {
167 // Delete everything.
168 $keys = $this->redis->keys($this->prefix . '*');
169 $this->redis->del($keys);
170 }
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);
176 }
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);
182 array_pop($parts);
183 $tmpCacheKey = array_pop($parts);
184 $this->deleteItem($id, $tmpCacheKey);
185 }
186 }
187 else {
188 throw new CRM_Core_Exception("Not implemented: Redis::deleteItem");
189 }
190 }
191
192 public function getCount($cacheKey) {
193 $allKey = $this->key($cacheKey, 'all');
194 return $this->redis->zSize($allKey);
195 }
196
197 /**
198 * Construct the full path to a cache item.
199 *
200 * @param string $cacheKey
201 * Identifier for this saved search.
202 * Ex: 'abcd1234abcd1234'.
203 * @param string $item
204 * Ex: 'list', 'rel', 'data'.
205 * @return string
206 * Ex: 'dmaster/prevnext/abcd1234abcd1234/list'
207 */
208 private function key($cacheKey, $item) {
209 return $this->prefix . $cacheKey . \CRM_Utils_Cache::DELIMITER . $item;
210 }
211
212 /**
213 * Initialize any data-structures or timeouts for the cache-key.
214 *
215 * This is non-destructive -- if data already exists, it's preserved.
216 *
217 * @return array
218 * 0 => string $allItemsCacheKey,
219 * 1 => string $dataItemsCacheKey,
220 * 2 => string $selectedItemsCacheKey,
221 * 3 => int $maxExistingScore
222 */
223 private function initCacheKey($cacheKey) {
224 $allKey = $this->key($cacheKey, 'all');
225 $selKey = $this->key($cacheKey, 'sel');
226 $dataKey = $this->key($cacheKey, 'data');
227
228 $this->redis->setTimeout($allKey, self::TTL);
229 $this->redis->setTimeout($dataKey, self::TTL);
230 $this->redis->setTimeout($selKey, self::TTL);
231
232 $maxScore = 0;
233 foreach ($this->redis->zRange($allKey, -1, -1, TRUE) as $lastElem => $lastScore) {
234 $maxScore = $lastScore;
235 }
be2fb01f 236 return [$allKey, $dataKey, $selKey, $maxScore];
751f3d98
TO
237 }
238
435fc552
SL
239 /**
240 * @inheritDoc
241 */
242 public function cleanup() {
243 // Redis already handles cleaning up stale keys.
244 return;
245 }
246
751f3d98 247}