1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Phoole (PHP7.2+) |
5
|
|
|
* |
6
|
|
|
* @category Library |
7
|
|
|
* @package Phoole\Cache |
8
|
|
|
* @copyright Copyright (c) 2019 Hong Zhang |
9
|
|
|
*/ |
10
|
|
|
declare(strict_types=1); |
11
|
|
|
|
12
|
|
|
namespace Phoole\Cache; |
13
|
|
|
|
14
|
|
|
use Psr\SimpleCache\CacheInterface; |
15
|
|
|
use Phoole\Cache\Adaptor\FileAdaptor; |
16
|
|
|
use Phoole\Cache\Adaptor\AdaptorInterface; |
17
|
|
|
use Phoole\Cache\Exception\NotFoundException; |
18
|
|
|
use Phoole\Cache\Exception\InvalidArgumentException; |
19
|
|
|
use Phoole\Base\Exception\NotFoundException as PhooleNotFoundException; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* Cache |
23
|
|
|
* |
24
|
|
|
* @package Phoole\Cache |
25
|
|
|
*/ |
26
|
|
|
class Cache implements CacheInterface |
27
|
|
|
{ |
28
|
|
|
/** |
29
|
|
|
* @var AdaptorInterface |
30
|
|
|
*/ |
31
|
|
|
protected $adaptor; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var array |
35
|
|
|
*/ |
36
|
|
|
protected $settings = [ |
37
|
|
|
'defaultTTL' => 86400, // default TTL 86400 seconds |
38
|
|
|
'stampedeGap' => 60, // 0-120 seconds |
39
|
|
|
'stampedePercent' => 5, // 5% chance considered stale |
40
|
|
|
'distributedPercent' => 5, // 5% fluctuation of expiration time |
41
|
|
|
]; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Inject adaptor and settings |
45
|
|
|
* |
46
|
|
|
* @param AdaptorInterface $adaptor |
47
|
|
|
* @param array $settings |
48
|
|
|
*/ |
49
|
|
|
public function __construct( |
50
|
|
|
?AdaptorInterface $adaptor = NULL, |
51
|
|
|
array $settings = [] |
52
|
|
|
) { |
53
|
|
|
$this->adaptor = $adaptor ?? new FileAdaptor(); |
54
|
|
|
$this->settings = \array_merge($this->settings, $settings); |
55
|
|
|
} |
56
|
|
|
|
57
|
|
|
/** |
58
|
|
|
* {@inheritDoc} |
59
|
|
|
*/ |
60
|
|
|
public function get($key, $default = NULL) |
61
|
|
|
{ |
62
|
|
|
// verify the key first |
63
|
|
|
$key = $this->checkKey($key); |
64
|
|
|
|
65
|
|
|
// try read using adaptor |
66
|
|
|
try { |
67
|
|
|
list($res, $time) = $this->adaptor->get($key); |
68
|
|
|
|
69
|
|
|
// check expiration time |
70
|
|
|
if ($this->checkTime($time)) { |
71
|
|
|
return $this->unSerialize($res); |
72
|
|
|
} |
73
|
|
|
|
74
|
|
|
throw new NotFoundException("KEY $key Expired"); |
75
|
|
|
} catch (PhooleNotFoundException $e) { |
76
|
|
|
return $default; |
77
|
|
|
} |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* {@inheritDoc} |
82
|
|
|
*/ |
83
|
|
|
public function set($key, $value, $ttl = NULL) |
84
|
|
|
{ |
85
|
|
|
$ttl = $this->getTTL($ttl); |
86
|
|
|
$key = $this->checkKey($key); |
87
|
|
|
$val = $this->serialize($value); |
88
|
|
|
return $value ? $this->adaptor->set($key, $val, $ttl) : FALSE; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* {@inheritDoc} |
93
|
|
|
*/ |
94
|
|
|
public function delete($key) |
95
|
|
|
{ |
96
|
|
|
$key = $this->checkKey($key); |
97
|
|
|
return $this->adaptor->delete($key); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* {@inheritDoc} |
102
|
|
|
*/ |
103
|
|
|
public function clear() |
104
|
|
|
{ |
105
|
|
|
return $this->adaptor->clear(); |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* {@inheritDoc} |
110
|
|
|
*/ |
111
|
|
|
public function getMultiple($keys, $default = NULL) |
112
|
|
|
{ |
113
|
|
|
$result = []; |
114
|
|
|
foreach ($keys as $key) { |
115
|
|
|
$result[$key] = $this->get($key, $default); |
116
|
|
|
} |
117
|
|
|
return $result; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
/** |
121
|
|
|
* {@inheritDoc} |
122
|
|
|
*/ |
123
|
|
|
public function setMultiple($values, $ttl = NULL) |
124
|
|
|
{ |
125
|
|
|
$res = TRUE; |
126
|
|
|
foreach ($values as $key => $value) { |
127
|
|
|
$res &= $this->set($key, $value, $ttl); |
128
|
|
|
} |
129
|
|
|
return (bool) $res; |
130
|
|
|
} |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* {@inheritDoc} |
134
|
|
|
*/ |
135
|
|
|
public function deleteMultiple($keys) |
136
|
|
|
{ |
137
|
|
|
$res = TRUE; |
138
|
|
|
foreach ($keys as $key) { |
139
|
|
|
$res &= $this->delete($key); |
140
|
|
|
} |
141
|
|
|
return (bool) $res; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* {@inheritDoc} |
146
|
|
|
*/ |
147
|
|
|
public function has($key) |
148
|
|
|
{ |
149
|
|
|
return NULL !== $this->get($key); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* Check key is valid or not |
154
|
|
|
* |
155
|
|
|
* @param string $key |
156
|
|
|
* @return string |
157
|
|
|
* @throws InvalidArgumentException |
158
|
|
|
*/ |
159
|
|
|
protected function checkKey($key): string |
160
|
|
|
{ |
161
|
|
|
try { |
162
|
|
|
return (string) $key; |
163
|
|
|
} catch (\Throwable $e) { |
|
|
|
|
164
|
|
|
throw new InvalidArgumentException($e->getMessage()); |
165
|
|
|
} |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* check expiration time, avoiding stampede situation on **ONE HOT** item |
170
|
|
|
* |
171
|
|
|
* if item not expired but fall into the stampedeGap (60-120 seconds), |
172
|
|
|
* then stampede percent (5%) chance to be considered stale and trigger |
173
|
|
|
* generate new contend |
174
|
|
|
* |
175
|
|
|
* @param int $time |
176
|
|
|
* @return bool |
177
|
|
|
*/ |
178
|
|
|
protected function checkTime(int $time): bool |
179
|
|
|
{ |
180
|
|
|
$now = time(); |
181
|
|
|
|
182
|
|
|
// not expired |
183
|
|
|
if ($time > $now) { |
184
|
|
|
return TRUE; |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
// just expired, fall in stampedeGap |
188
|
|
|
if ($time > $now - $this->settings['stampedeGap']) { |
189
|
|
|
// 5% chance consider expired to build new cache |
190
|
|
|
return rand(0, 100) > $this->settings['stampedePercent']; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
// expired |
194
|
|
|
return FALSE; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* TTL +- 5% fluctuation |
199
|
|
|
* |
200
|
|
|
* distributedExpiration **WILL** add expiration fluctuation to **ALL** items |
201
|
|
|
* which will avoid large amount of items expired at the same time |
202
|
|
|
* |
203
|
|
|
* @param null|int|\DateInterval $ttl |
204
|
|
|
* @return int |
205
|
|
|
*/ |
206
|
|
|
protected function getTTL($ttl): int |
207
|
|
|
{ |
208
|
|
|
if ($ttl instanceof \DateInterval) { |
209
|
|
|
$ttl = (int) $ttl->format('%s'); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
if (is_null($ttl)) { |
213
|
|
|
$ttl = $this->settings['defaultTTL']; |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
// add fluctuation |
217
|
|
|
$fluctuation = $this->settings['distributedPercent']; |
218
|
|
|
$rand = rand(-$fluctuation, $fluctuation); |
219
|
|
|
|
220
|
|
|
return (int) round($ttl * (100 + $rand) / 100); |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* Serialize the value |
225
|
|
|
* |
226
|
|
|
* @param mixed $value |
227
|
|
|
* @return string |
228
|
|
|
*/ |
229
|
|
|
protected function serialize($value): string |
230
|
|
|
{ |
231
|
|
|
return \serialize($value); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* unserialize the value |
236
|
|
|
* |
237
|
|
|
* @param string $value |
238
|
|
|
* @return mixed |
239
|
|
|
*/ |
240
|
|
|
protected function unSerialize(string $value) |
241
|
|
|
{ |
242
|
|
|
return \unserialize($value); |
243
|
|
|
} |
244
|
|
|
} |
This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.
Unreachable code is most often the result of
return
,die
orexit
statements that have been added for debug purposes.In the above example, the last
return false
will never be executed, because a return statement has already been met in every possible execution path.