Commit | Line | Data |
---|---|---|
6a488035 TO |
1 | <?php |
2 | /* | |
3 | +--------------------------------------------------------------------+ | |
bc77d7c0 | 4 | | Copyright CiviCRM LLC. All rights reserved. | |
6a488035 | 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 | | |
6a488035 | 9 | +--------------------------------------------------------------------+ |
d25dd0ee | 10 | */ |
6a488035 TO |
11 | |
12 | /** | |
13 | * | |
14 | * @package CRM | |
ca5cec67 | 15 | * @copyright CiviCRM LLC https://civicrm.org/licensing |
6a488035 TO |
16 | */ |
17 | ||
18 | /** | |
19 | * This caching provider stores all cached items as a "group" in the | |
20 | * "civicrm_cache" table. The entire 'group' may be prefetched when | |
21 | * instantiating the cache provider. | |
22 | */ | |
23 | class CRM_Utils_Cache_SqlGroup implements CRM_Utils_Cache_Interface { | |
24 | ||
be5eea0d TO |
25 | // 6*60*60 |
26 | const DEFAULT_TTL = 21600; | |
27 | ||
28 | const TS_FMT = 'Y-m-d H:i:s'; | |
6714d8d2 SL |
29 | // TODO Consider native implementation. |
30 | use CRM_Utils_Cache_NaiveMultipleTrait; | |
0d64c8fa | 31 | |
6a488035 | 32 | /** |
fe482240 | 33 | * The host name of the memcached server. |
6a488035 TO |
34 | * |
35 | * @var string | |
36 | */ | |
37 | protected $group; | |
38 | ||
39 | /** | |
6714d8d2 | 40 | * @var int |
6a488035 TO |
41 | */ |
42 | protected $componentID; | |
f813f78e | 43 | |
6a488035 | 44 | /** |
041ecc95 | 45 | * In-memory cache to optimize redundant get()s. |
46 | * | |
47 | * @var array | |
6a488035 | 48 | */ |
be5eea0d TO |
49 | protected $valueCache; |
50 | ||
51 | /** | |
041ecc95 | 52 | * In-memory cache to optimize redundant get()s. |
53 | * | |
54 | * @var array | |
be5eea0d TO |
55 | * Note: expiresCache[$key]===NULL means cache-miss |
56 | */ | |
57 | protected $expiresCache; | |
58 | ||
59 | /** | |
041ecc95 | 60 | * Table. |
61 | * | |
be5eea0d TO |
62 | * @var string |
63 | */ | |
64 | protected $table; | |
6a488035 TO |
65 | |
66 | /** | |
fe482240 | 67 | * Constructor. |
6a488035 | 68 | * |
77855840 TO |
69 | * @param array $config |
70 | * An array of configuration params. | |
6a488035 TO |
71 | * - group: string |
72 | * - componentID: int | |
73 | * - prefetch: bool, whether to preemptively read the entire cache group; default: TRUE | |
74 | * | |
77b97be7 EM |
75 | * @throws RuntimeException |
76 | * @return \CRM_Utils_Cache_SqlGroup | |
6a488035 | 77 | */ |
00be9182 | 78 | public function __construct($config) { |
be5eea0d | 79 | $this->table = CRM_Core_DAO_Cache::getTableName(); |
6a488035 TO |
80 | if (isset($config['group'])) { |
81 | $this->group = $config['group']; | |
0db6c3e1 TO |
82 | } |
83 | else { | |
6a488035 TO |
84 | throw new RuntimeException("Cannot construct SqlGroup cache: missing group"); |
85 | } | |
86 | if (isset($config['componentID'])) { | |
87 | $this->componentID = $config['componentID']; | |
0db6c3e1 TO |
88 | } |
89 | else { | |
6a488035 TO |
90 | $this->componentID = NULL; |
91 | } | |
be2fb01f | 92 | $this->valueCache = []; |
6a488035 TO |
93 | if (CRM_Utils_Array::value('prefetch', $config, TRUE)) { |
94 | $this->prefetch(); | |
95 | } | |
96 | } | |
97 | ||
5bc392e6 EM |
98 | /** |
99 | * @param string $key | |
100 | * @param mixed $value | |
858451a9 | 101 | * @param null|int|\DateInterval $ttl |
7c9a45c3 | 102 | * |
858451a9 | 103 | * @return bool |
7c9a45c3 | 104 | * |
105 | * @throws \CRM_Core_Exception | |
106 | * @throws \CRM_Utils_Cache_CacheException | |
107 | * @throws \CRM_Utils_Cache_InvalidArgumentException | |
5bc392e6 | 108 | */ |
858451a9 | 109 | public function set($key, $value, $ttl = NULL) { |
be5eea0d TO |
110 | CRM_Utils_Cache::assertValidKey($key); |
111 | ||
112 | $lock = Civi::lockManager()->acquire("cache.{$this->group}_{$key}._null"); | |
113 | if (!$lock->isAcquired()) { | |
114 | throw new \CRM_Utils_Cache_CacheException("SqlGroup: Failed to acquire lock on cache key."); | |
115 | } | |
116 | ||
13ca7ebf TO |
117 | if (is_int($ttl) && $ttl <= 0) { |
118 | return $this->delete($key); | |
119 | } | |
120 | ||
be5eea0d | 121 | $dataExists = CRM_Core_DAO::singleValueQuery("SELECT COUNT(*) FROM {$this->table} WHERE {$this->where($key)}"); |
13ca7ebf | 122 | $expires = round(microtime(1)) + CRM_Utils_Date::convertCacheTtl($ttl, self::DEFAULT_TTL); |
be5eea0d TO |
123 | |
124 | $dataSerialized = CRM_Core_BAO_Cache::encode($value); | |
125 | ||
126 | // This table has a wonky index, so we cannot use REPLACE or | |
127 | // "INSERT ... ON DUPE". Instead, use SELECT+(INSERT|UPDATE). | |
128 | if ($dataExists) { | |
129 | $sql = "UPDATE {$this->table} SET data = %1, created_date = FROM_UNIXTIME(%2), expired_date = FROM_UNIXTIME(%3) WHERE {$this->where($key)}"; | |
be2fb01f CW |
130 | $args = [ |
131 | 1 => [$dataSerialized, 'String'], | |
132 | 2 => [time(), 'Positive'], | |
133 | 3 => [$expires, 'Positive'], | |
134 | ]; | |
7c9a45c3 | 135 | CRM_Core_DAO::executeQuery($sql, $args, TRUE, NULL, FALSE, FALSE); |
be5eea0d TO |
136 | } |
137 | else { | |
138 | $sql = "INSERT INTO {$this->table} (group_name,path,data,created_date,expired_date) VALUES (%1,%2,%3,FROM_UNIXTIME(%4),FROM_UNIXTIME(%5))"; | |
be2fb01f | 139 | $args = [ |
7c9a45c3 | 140 | 1 => [(string) $this->group, 'String'], |
be5eea0d TO |
141 | 2 => [$key, 'String'], |
142 | 3 => [$dataSerialized, 'String'], | |
143 | 4 => [time(), 'Positive'], | |
144 | 5 => [$expires, 'Positive'], | |
be2fb01f | 145 | ]; |
7c9a45c3 | 146 | CRM_Core_DAO::executeQuery($sql, $args, TRUE, NULL, FALSE, FALSE); |
858451a9 | 147 | } |
be5eea0d TO |
148 | |
149 | $lock->release(); | |
150 | ||
be5eea0d TO |
151 | $this->valueCache[$key] = CRM_Core_BAO_Cache::decode($dataSerialized); |
152 | $this->expiresCache[$key] = $expires; | |
858451a9 | 153 | return TRUE; |
6a488035 TO |
154 | } |
155 | ||
5bc392e6 EM |
156 | /** |
157 | * @param string $key | |
2da67cc5 | 158 | * @param mixed $default |
5bc392e6 EM |
159 | * |
160 | * @return mixed | |
7c9a45c3 | 161 | * |
162 | * @throws \CRM_Utils_Cache_InvalidArgumentException | |
5bc392e6 | 163 | */ |
2da67cc5 | 164 | public function get($key, $default = NULL) { |
be5eea0d TO |
165 | CRM_Utils_Cache::assertValidKey($key); |
166 | if (!isset($this->expiresCache[$key]) || time() >= $this->expiresCache[$key]) { | |
167 | $sql = "SELECT path, data, UNIX_TIMESTAMP(expired_date) as expires FROM {$this->table} WHERE " . $this->where($key); | |
168 | $dao = CRM_Core_DAO::executeQuery($sql); | |
169 | while ($dao->fetch()) { | |
170 | $this->expiresCache[$key] = $dao->expires; | |
171 | $this->valueCache[$key] = CRM_Core_BAO_Cache::decode($dao->data); | |
172 | } | |
6a488035 | 173 | } |
be5eea0d TO |
174 | return (isset($this->expiresCache[$key]) && time() < $this->expiresCache[$key]) ? $this->reobjectify($this->valueCache[$key]) : $default; |
175 | } | |
176 | ||
7c9a45c3 | 177 | /** |
178 | * @param mixed $value | |
179 | * | |
180 | * @return object | |
181 | */ | |
be5eea0d TO |
182 | private function reobjectify($value) { |
183 | return is_object($value) ? unserialize(serialize($value)) : $value; | |
6a488035 TO |
184 | } |
185 | ||
5bc392e6 | 186 | /** |
7c9a45c3 | 187 | * @param string $key |
3fd42bb5 | 188 | * @param mixed $default |
5bc392e6 EM |
189 | * |
190 | * @return mixed | |
191 | */ | |
00be9182 | 192 | public function getFromFrontCache($key, $default = NULL) { |
be5eea0d TO |
193 | if (isset($this->expiresCache[$key]) && time() < $this->expiresCache[$key] && $this->valueCache[$key]) { |
194 | return $this->reobjectify($this->valueCache[$key]); | |
195 | } | |
196 | else { | |
197 | return $default; | |
198 | } | |
199 | } | |
200 | ||
201 | public function has($key) { | |
202 | $this->get($key); | |
203 | return isset($this->expiresCache[$key]) && time() < $this->expiresCache[$key]; | |
20b015e1 TO |
204 | } |
205 | ||
5bc392e6 EM |
206 | /** |
207 | * @param string $key | |
7c9a45c3 | 208 | * |
eec321a4 | 209 | * @return bool |
7c9a45c3 | 210 | * @throws \CRM_Utils_Cache_InvalidArgumentException |
5bc392e6 | 211 | */ |
00be9182 | 212 | public function delete($key) { |
be5eea0d | 213 | CRM_Utils_Cache::assertValidKey($key); |
febc8b36 SL |
214 | // If we are triggering a deletion of a prevNextCache key in the civicrm_cache tabl |
215 | // Alssure that the relevant prev_next_cache values are also removed. | |
77f080cb | 216 | if ($this->group == CRM_Utils_Cache::cleanKey('CiviCRM Search PrevNextCache')) { |
febc8b36 SL |
217 | Civi::service('prevnext')->deleteItem(NULL, $key); |
218 | } | |
be5eea0d TO |
219 | CRM_Core_DAO::executeQuery("DELETE FROM {$this->table} WHERE {$this->where($key)}"); |
220 | unset($this->valueCache[$key]); | |
221 | unset($this->expiresCache[$key]); | |
eec321a4 | 222 | return TRUE; |
6a488035 TO |
223 | } |
224 | ||
00be9182 | 225 | public function flush() { |
77f080cb | 226 | if ($this->group == CRM_Utils_Cache::cleanKey('CiviCRM Search PrevNextCache') && |
febc8b36 | 227 | Civi::service('prevnext') instanceof CRM_Core_PrevNextCache_Sql) { |
b3e4463a | 228 | Civi::service('prevnext')->cleanup(); |
febc8b36 SL |
229 | } |
230 | else { | |
231 | CRM_Core_DAO::executeQuery("DELETE FROM {$this->table} WHERE {$this->where()}"); | |
232 | } | |
be2fb01f CW |
233 | $this->valueCache = []; |
234 | $this->expiresCache = []; | |
124e5288 | 235 | return TRUE; |
6a488035 TO |
236 | } |
237 | ||
c31de879 TO |
238 | public function clear() { |
239 | return $this->flush(); | |
240 | } | |
241 | ||
00be9182 | 242 | public function prefetch() { |
be5eea0d | 243 | $dao = CRM_Core_DAO::executeQuery("SELECT path, data, UNIX_TIMESTAMP(expired_date) AS expires FROM {$this->table} WHERE " . $this->where(NULL)); |
be2fb01f CW |
244 | $this->valueCache = []; |
245 | $this->expiresCache = []; | |
be5eea0d TO |
246 | while ($dao->fetch()) { |
247 | $this->valueCache[$dao->path] = CRM_Core_BAO_Cache::decode($dao->data); | |
248 | $this->expiresCache[$dao->path] = $dao->expires; | |
249 | } | |
be5eea0d TO |
250 | } |
251 | ||
252 | protected function where($path = NULL) { | |
be2fb01f | 253 | $clauses = []; |
be5eea0d TO |
254 | $clauses[] = ('group_name = "' . CRM_Core_DAO::escapeString($this->group) . '"'); |
255 | if ($path) { | |
256 | $clauses[] = ('path = "' . CRM_Core_DAO::escapeString($path) . '"'); | |
257 | } | |
258 | return $clauses ? implode(' AND ', $clauses) : '(1)'; | |
6a488035 | 259 | } |
96025800 | 260 | |
6a488035 | 261 | } |