Cache::set()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 4
c 2
b 0
f 1
dl 0
loc 6
rs 10
cc 2
nc 2
nop 3
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  mixed $key
156
     * @return string
157
     * @throws InvalidArgumentException
158
     */
159
    protected function checkKey($key): string
160
    {
161
        if (!\is_scalar($key)) {
162
            throw new InvalidArgumentException('non-valid key found');
163
        }
164
        return (string) $key;
165
    }
166
167
    /**
168
     * check expiration time, avoiding stampede situation on **ONE HOT** item
169
     *
170
     * if  item not expired but fall into the stampedeGap (60-120 seconds),
171
     * then stampede percent (5%) chance to be considered stale and trigger
172
     * generate new contend
173
     *
174
     * @param  int $time
175
     * @return bool
176
     */
177
    protected function checkTime(int $time): bool
178
    {
179
        $now = time();
180
181
        // not expired
182
        if ($time > $now) {
183
            return TRUE;
184
        }
185
186
        // just expired, fall in stampedeGap
187
        if ($time > $now - $this->settings['stampedeGap']) {
188
            // 5% chance consider expired to build new cache
189
            return rand(0, 100) > $this->settings['stampedePercent'];
190
        }
191
192
        // expired
193
        return FALSE;
194
    }
195
196
    /**
197
     * TTL +- 5% fluctuation
198
     *
199
     * distributedExpiration **WILL** add expiration fluctuation to **ALL** items
200
     * which will avoid large amount of items expired at the same time
201
     *
202
     * @param  null|int|\DateInterval $ttl
203
     * @return int
204
     */
205
    protected function getTTL($ttl): int
206
    {
207
        if ($ttl instanceof \DateInterval) {
208
            $ttl = (int) $ttl->format('%s');
209
        }
210
211
        if (is_null($ttl)) {
212
            $ttl = $this->settings['defaultTTL'];
213
        }
214
215
        // add fluctuation
216
        $fluctuation = $this->settings['distributedPercent'];
217
        $rand = rand(-$fluctuation, $fluctuation);
218
219
        return (int) round($ttl * (100 + $rand) / 100);
220
    }
221
222
    /**
223
     * Serialize the value
224
     *
225
     * @param  mixed $value
226
     * @return string
227
     */
228
    protected function serialize($value): string
229
    {
230
        return \serialize($value);
231
    }
232
233
    /**
234
     * unserialize the value
235
     *
236
     * @param  string $value
237
     * @return mixed
238
     */
239
    protected function unSerialize(string $value)
240
    {
241
        return \unserialize($value);
242
    }
243
}