1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace NokitaKaze\KeyValue; |
4
|
|
|
|
5
|
|
|
use NokitaKaze\Mutex\MutexInterface; |
6
|
|
|
use NokitaKaze\Mutex\FileMutex; |
7
|
|
|
use Psr\SimpleCache\CacheInterface; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* Class AbstractStorage |
11
|
|
|
* @package NokitaKaze\KeyValue |
12
|
|
|
* @doc http://www.php-fig.org/psr/psr-16/ |
13
|
|
|
*/ |
14
|
|
|
abstract class AbstractStorage implements CacheInterface { |
15
|
|
|
const StorageTemporary = 0; |
16
|
|
|
const StoragePersistent = 1; |
17
|
|
|
const RegionDomain = 0;// Только в контексте этого HostName |
18
|
|
|
const RegionServer = 1;// В контексте ВСЕГО дедика |
19
|
|
|
const RegionFolder = 2;// В контексте папки домена (для разных хостов, запущенных из одной папки) |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var string Префикс к названиям полей ключей для бесконфликтного использования в глобалсе |
23
|
|
|
*/ |
24
|
|
|
protected $_prefix; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var KeyValueSettings|object Настройки, переданные в конструктор и конструктором исправленные |
28
|
|
|
*/ |
29
|
|
|
protected $settings; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var MutexInterface[]|null[] |
33
|
|
|
* Мьютекс, созданный через MutexInterface, настройки задаются конкретной реализацией |
34
|
|
|
*/ |
35
|
|
|
protected $_locks = []; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var string|null |
39
|
|
|
*/ |
40
|
|
|
protected $_last_used_lock_key = null; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @param KeyValueSettings|object $settings |
44
|
|
|
*/ |
45
|
|
|
abstract function __construct($settings); |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Дёргаем значение из kv-хранилища |
49
|
|
|
* |
50
|
|
|
* @param string $key Название ключа |
51
|
|
|
* @param string|mixed $default_value Дефолтное значение, если настоящее недоступно |
52
|
|
|
* |
53
|
|
|
* @return string|mixed |
54
|
|
|
*/ |
55
|
75 |
|
function get_value($key, $default_value = '') { |
56
|
75 |
|
$data = $this->get_value_full($key); |
57
|
|
|
|
58
|
75 |
|
return is_null($data) ? $default_value : $data->value; |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Формируем datum типа объект, который мы будем вносить в наше kv-хранилище |
63
|
|
|
* |
64
|
|
|
* @param string|integer $key Ключ |
65
|
|
|
* @param mixed $value Новое значение |
66
|
|
|
* @param double $ttl Кол-во секунд, после которых значение будет считаться просроченным |
67
|
|
|
* |
68
|
|
|
* @return object|KeyValueDatum |
69
|
|
|
*/ |
70
|
140 |
|
protected function form_datum_value($key, $value, $ttl) { |
71
|
140 |
|
$backtrace = debug_backtrace(); |
72
|
|
|
$data = (object) [ |
73
|
140 |
|
'key' => $key, |
74
|
140 |
|
'time_create' => microtime(true), |
75
|
140 |
|
'time_expires' => microtime(true) + $ttl, |
76
|
140 |
|
'host' => isset($_SERVER, $_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : null, |
77
|
140 |
|
'value' => $value, |
78
|
140 |
|
'init_file' => isset($backtrace[1], $backtrace[1]['file']) ? $backtrace[1]['file'] : null, |
79
|
140 |
|
'init_line' => isset($backtrace[1], $backtrace[1]['line']) ? $backtrace[1]['line'] : null, |
80
|
140 |
|
'pid' => function_exists('posix_getpid') ? posix_getpid() : null,// @todo windows |
81
|
28 |
|
]; |
82
|
140 |
|
unset($backtrace); |
83
|
|
|
|
84
|
140 |
|
return $data; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Пишем новое значение для key в нашем kv-хранилище |
89
|
|
|
* |
90
|
|
|
* @param string $key Название ключа |
91
|
|
|
* @param mixed $value Новое значение |
92
|
|
|
* @param double|integer $ttl Кол-во секунд, после которых значение будет считаться просроченным |
93
|
|
|
* |
94
|
|
|
* @throws KeyValueException |
95
|
|
|
*/ |
96
|
|
|
abstract function set_value($key, $value, $ttl = 315576000); |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* @param string $key |
100
|
|
|
* |
101
|
|
|
* @return object|null |
102
|
|
|
*/ |
103
|
|
|
abstract protected function get_value_full_clear($key); |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Получение полных данных по вхождению в kv-хранилище |
107
|
|
|
* |
108
|
|
|
* @param string $key Название ключа |
109
|
|
|
* |
110
|
|
|
* @return object|KeyValueDatum|null |
111
|
|
|
*/ |
112
|
130 |
|
function get_value_full($key) { |
113
|
130 |
|
$ts1 = microtime(true); |
114
|
130 |
|
$data = $this->get_value_full_clear($key); |
115
|
130 |
|
self::add_profiling(microtime(true) - $ts1, static::class, 'get_value_full_clear'); |
116
|
130 |
|
if (is_null($data)) { |
117
|
125 |
|
return null; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
// Проверяем expires |
121
|
115 |
|
if (!isset($data->time_expires) or ($data->time_expires < microtime(true))) { |
122
|
23 |
|
return null; |
123
|
|
|
} |
124
|
|
|
|
125
|
107 |
|
return $data; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Удаляем вхождение kv-хранилища |
130
|
|
|
* |
131
|
|
|
* @param string $key Название ключа |
132
|
|
|
*/ |
133
|
|
|
abstract function delete_value($key); |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Стандартная стратегия выбора префиксов |
137
|
|
|
*/ |
138
|
525 |
|
function standard_prefix_strategy() { |
139
|
525 |
|
if (!isset($this->settings->region_type)) { |
140
|
425 |
|
$this->settings->region_type = self::RegionDomain; |
141
|
85 |
|
} |
142
|
525 |
|
if (isset($this->settings->prefix)) { |
143
|
5 |
|
$this->_prefix = $this->settings->prefix; |
144
|
|
|
|
145
|
5 |
|
return; |
146
|
|
|
} |
147
|
|
|
|
148
|
525 |
|
$this->_prefix = self::get_default_prefix_from_environment($this->settings->region_type); |
149
|
520 |
|
} |
150
|
|
|
|
151
|
|
|
/** |
152
|
|
|
* @param integer $region_type |
153
|
|
|
* |
154
|
|
|
* @return string |
155
|
|
|
* @throws KeyValueException |
156
|
|
|
*/ |
157
|
525 |
|
static function get_default_prefix_from_environment($region_type = self::RegionDomain) { |
158
|
|
|
switch ($region_type) { |
159
|
525 |
|
case self::RegionDomain: |
160
|
460 |
|
return hash('sha256', preg_replace('|^(www\\.)?([a-z0-9.-]+)(\\:[0-9]+)?|', '$2', |
161
|
460 |
|
strtolower(self::get_environment('host')))).'_'; |
162
|
65 |
|
case self::RegionServer: |
163
|
30 |
|
return ''; |
164
|
35 |
|
case self::RegionFolder: |
165
|
|
|
// @hint Мы используем sha256, а не sha512, потому что иначе у нас просто не хватит длины |
166
|
30 |
|
return hash('sha256', strtolower(self::get_environment('root'))).'_'; |
167
|
1 |
|
default: |
168
|
5 |
|
throw new KeyValueException('Constructor settings is malformed. Region type can not be equal '. |
169
|
5 |
|
$region_type, 1); |
170
|
1 |
|
} |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* @param string $key |
175
|
|
|
* |
176
|
|
|
* @return string |
177
|
|
|
* @throws KeyValueException |
178
|
|
|
*/ |
179
|
495 |
|
static function get_environment($key) { |
180
|
198 |
|
switch ($key) { |
181
|
495 |
|
case 'host': |
182
|
465 |
|
return isset($_SERVER, $_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ''; |
183
|
36 |
|
case 'root': |
184
|
35 |
|
return isset($_SERVER, $_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : ''; |
185
|
1 |
|
default: |
186
|
5 |
|
throw new KeyValueException('malformed get_environment parameter', 2); |
187
|
1 |
|
} |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
/** |
191
|
|
|
* @return string |
192
|
|
|
*/ |
193
|
115 |
|
function get_prefix() { |
194
|
115 |
|
return $this->_prefix; |
195
|
|
|
} |
196
|
|
|
|
197
|
|
|
/** |
198
|
|
|
* Время просрочки значения, если есть |
199
|
|
|
* |
200
|
|
|
* @param string $key |
201
|
|
|
* |
202
|
|
|
* @return double|null |
203
|
|
|
*/ |
204
|
15 |
|
function get_expires_time($key) { |
205
|
15 |
|
$data = $this->get_value_full($key); |
206
|
15 |
|
if (is_null($data)) { |
207
|
5 |
|
return null; |
208
|
|
|
} else { |
209
|
15 |
|
return $data->time_expires; |
210
|
|
|
} |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Обновляем срок жизни записи |
215
|
|
|
* |
216
|
|
|
* @param string $key Название ключа |
217
|
|
|
* @param double $ttl Кол-во секунд, после которых значение будет считаться просроченным |
218
|
|
|
* @throws KeyValueException |
219
|
|
|
*/ |
220
|
15 |
|
function set_expires_time($key, $ttl) { |
221
|
15 |
|
$data = $this->get_value_full($key); |
222
|
15 |
|
if (is_null($data)) { |
223
|
5 |
|
return; |
224
|
|
|
} |
225
|
15 |
|
$this->set_value($key, $data->value, $ttl); |
226
|
15 |
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* Время задания значения |
230
|
|
|
* |
231
|
|
|
* @param string $key |
232
|
|
|
* |
233
|
|
|
* @return double|null |
234
|
|
|
*/ |
235
|
45 |
|
function get_change_time($key) { |
236
|
45 |
|
$data = $this->get_value_full($key); |
237
|
45 |
|
if (is_null($data)) { |
238
|
15 |
|
return null; |
239
|
|
|
} else { |
240
|
45 |
|
return $data->time_create; |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
|
244
|
|
|
/** |
245
|
|
|
* Время задания значения |
246
|
|
|
* |
247
|
|
|
* @param string $key |
248
|
|
|
* |
249
|
|
|
* @return boolean |
250
|
|
|
*/ |
251
|
85 |
|
function is_exist($key) { |
252
|
85 |
|
return !is_null($this->get_value_full($key)); |
253
|
|
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* @var double[][] |
257
|
|
|
*/ |
258
|
|
|
private static $_profiling = []; |
259
|
|
|
|
260
|
|
|
/** |
261
|
|
|
* @param double $time |
262
|
|
|
* @param string $class |
263
|
|
|
* @param string $action |
264
|
|
|
*/ |
265
|
170 |
|
protected static function add_profiling($time, $class, $action) { |
266
|
170 |
|
if (!array_key_exists($class, self::$_profiling)) { |
267
|
35 |
|
self::$_profiling[$class] = [$action => $time]; |
268
|
166 |
|
} elseif (!array_key_exists($action, self::$_profiling[$class])) { |
269
|
30 |
|
self::$_profiling[$class][$action] = $time; |
270
|
6 |
|
} else { |
271
|
160 |
|
self::$_profiling[$class][$action] += $time; |
272
|
|
|
} |
273
|
170 |
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Весь профайлинг |
277
|
|
|
* |
278
|
|
|
* @return double[]|double[][] |
279
|
|
|
*/ |
280
|
5 |
|
static function get_profiling() { |
281
|
|
|
$data = [ |
282
|
5 |
|
'class' => [], |
283
|
1 |
|
'action' => [], |
284
|
1 |
|
'all' => 0, |
285
|
1 |
|
]; |
286
|
5 |
|
foreach (self::$_profiling as $class => &$obj) { |
287
|
5 |
|
if (!array_key_exists($class, $data['class'])) { |
288
|
5 |
|
$data['class'][$class] = 0; |
289
|
1 |
|
} |
290
|
5 |
|
foreach ($obj as $action => &$time) { |
291
|
5 |
|
if (array_key_exists($action, $data['action'])) { |
292
|
5 |
|
$data['action'][$action] += $time; |
293
|
1 |
|
} else { |
294
|
5 |
|
$data['action'][$action] = $time; |
295
|
|
|
} |
296
|
5 |
|
$data['class'][$class] += $time; |
297
|
5 |
|
$data['all'] += $time; |
298
|
1 |
|
} |
299
|
1 |
|
} |
300
|
|
|
|
301
|
5 |
|
return $data; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* Создаём мьютекс, соответствующий ключу и кладём его в _locks |
306
|
|
|
* |
307
|
|
|
* @param string $key |
308
|
|
|
*/ |
309
|
5 |
|
protected function create_lock($key) { |
310
|
|
|
// @todo Любой мьютекс |
311
|
|
|
// @todo отдельный метод для роутинга мьютексов |
312
|
5 |
|
$this->_locks[$key] = new FileMutex([ |
313
|
5 |
|
'name' => 'ascetkey_'.$key, |
314
|
5 |
|
'prefix' => '', |
315
|
1 |
|
]); |
316
|
5 |
|
} |
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* Лочим мьютекс, соответствующий ключу |
320
|
|
|
* |
321
|
|
|
* @param string $key |
322
|
|
|
*/ |
323
|
155 |
|
protected function get_lock($key) { |
324
|
155 |
|
if (!isset($this->_locks[$key])) { |
325
|
155 |
|
$this->create_lock($key); |
326
|
31 |
|
} |
327
|
155 |
|
$this->_locks[$key]->get_lock(); |
328
|
155 |
|
$this->_last_used_lock_key = $key; |
329
|
155 |
|
} |
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* Снимаем мьютекс |
333
|
|
|
*/ |
334
|
155 |
|
protected function release_lock() { |
335
|
155 |
|
if (isset($this->_locks[$this->_last_used_lock_key])) { |
336
|
155 |
|
$this->_locks[$this->_last_used_lock_key]->release_lock(); |
337
|
31 |
|
} |
338
|
155 |
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* Удаляем все созданные мьютексы, не удаляя сам объект Storage |
342
|
|
|
* |
343
|
|
|
* @param integer|null $minimal_count_for_delete |
344
|
|
|
*/ |
345
|
25 |
|
function clear_mutex_list($minimal_count_for_delete = null) { |
346
|
25 |
|
if (is_null($minimal_count_for_delete) or (count($this->_locks) >= $minimal_count_for_delete)) { |
347
|
25 |
|
$this->_locks = []; |
348
|
5 |
|
} |
349
|
25 |
|
} |
350
|
|
|
|
351
|
|
|
/** |
352
|
|
|
* PSR-16 wrapper |
353
|
|
|
*/ |
354
|
|
|
|
355
|
|
|
/** |
356
|
|
|
* @param string $key |
357
|
|
|
* |
358
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
359
|
|
|
*/ |
360
|
335 |
|
protected function is_key_valid($key) { |
361
|
335 |
|
if (!is_string($key) and !is_integer($key)) { |
362
|
120 |
|
throw new KeyValueException('Invalid Argument'); |
363
|
|
|
} |
364
|
215 |
|
if (preg_match('_[\\{\\}\\(\\)\\/\\@:]_', $key)) { |
365
|
120 |
|
throw new KeyValueException('Key contains forbidden symbol'); |
366
|
|
|
} |
367
|
95 |
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* @param string $key |
371
|
|
|
* @param null $default |
372
|
|
|
* |
373
|
|
|
* @return mixed |
374
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
375
|
|
|
*/ |
376
|
160 |
|
function get($key, $default = null) { |
377
|
160 |
|
$this->is_key_valid($key); |
378
|
|
|
|
379
|
100 |
|
return $this->get_value($key, $default); |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* @param string $key |
384
|
|
|
* |
385
|
|
|
* @return boolean |
386
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
387
|
|
|
*/ |
388
|
85 |
|
function delete($key) { |
389
|
85 |
|
$this->is_key_valid($key); |
390
|
25 |
|
$this->delete_value($key); |
391
|
|
|
|
392
|
25 |
|
return true; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
/** |
396
|
|
|
* @param string $key |
397
|
|
|
* @param mixed $value |
398
|
|
|
* @param null $ttl |
399
|
|
|
* |
400
|
|
|
* @return boolean |
401
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
402
|
|
|
*/ |
403
|
135 |
|
function set($key, $value, $ttl = null) { |
404
|
135 |
|
$this->is_key_valid($key); |
405
|
|
|
try { |
406
|
75 |
|
$this->set_value($key, $value, !is_null($ttl) ? $ttl : 10 * 365.25 * 24 * 3600); |
407
|
19 |
|
} catch (KeyValueException $e) { |
408
|
5 |
|
return false; |
409
|
|
|
} |
410
|
|
|
|
411
|
70 |
|
return true; |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
/** |
415
|
|
|
* @codeCoverageIgnore |
416
|
|
|
*/ |
417
|
|
|
function clear() { |
418
|
|
|
// @hint It should be implemented in children |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
/** |
422
|
|
|
* @param \iterable $keys |
423
|
|
|
* @param null $default |
424
|
|
|
* |
425
|
|
|
* @return array |
426
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
427
|
|
|
*/ |
428
|
30 |
|
function getMultiple($keys, $default = null) { |
429
|
30 |
|
$data = []; |
430
|
30 |
|
foreach ($keys as &$key) { |
431
|
30 |
|
$data[$key] = $this->get($key, $default); |
432
|
6 |
|
} |
433
|
|
|
|
434
|
30 |
|
return $data; |
|
|
|
|
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
/** |
438
|
|
|
* @param \iterable $keys |
439
|
|
|
* @param null $ttl |
440
|
|
|
* |
441
|
|
|
* @return boolean |
442
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
443
|
|
|
*/ |
444
|
30 |
|
function setMultiple($keys, $ttl = null) { |
445
|
30 |
|
$u = true; |
446
|
30 |
|
foreach ($keys as $key => &$value) { |
447
|
30 |
|
if (!$this->set($key, $value, $ttl)) { |
448
|
12 |
|
$u = false; |
449
|
|
|
} |
450
|
6 |
|
} |
451
|
|
|
|
452
|
30 |
|
return $u; |
453
|
|
|
} |
454
|
|
|
|
455
|
|
|
/** |
456
|
|
|
* @param iterable $keys |
457
|
|
|
* |
458
|
|
|
* @return boolean |
459
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
460
|
|
|
*/ |
461
|
90 |
|
function deleteMultiple($keys) { |
462
|
90 |
|
$u = true; |
463
|
90 |
|
foreach ($keys as &$key) { |
464
|
90 |
|
if (!$this->delete($key)) { |
465
|
|
|
// @codeCoverageIgnoreStart |
466
|
|
|
$u = false; |
467
|
|
|
// @codeCoverageIgnoreEnd |
468
|
|
|
} |
469
|
18 |
|
} |
470
|
|
|
|
471
|
90 |
|
return $u; |
472
|
|
|
} |
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* @param string $key |
476
|
|
|
* |
477
|
|
|
* @throws \Psr\SimpleCache\InvalidArgumentException |
478
|
|
|
* @return boolean |
479
|
|
|
*/ |
480
|
145 |
|
function has($key) { |
481
|
145 |
|
$this->is_key_valid($key); |
482
|
|
|
|
483
|
85 |
|
return $this->is_exist($key); |
484
|
|
|
} |
485
|
|
|
} |
486
|
|
|
|
487
|
|
|
?> |
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.