Issues (70)

src/Adapters/MemoryStore.php (1 issue)

1
<?php
2
3
namespace MatthiasMullie\Scrapbook\Adapters;
4
5
use MatthiasMullie\Scrapbook\Adapters\Collections\MemoryStore as Collection;
6
use MatthiasMullie\Scrapbook\KeyValueStore;
7
8
/**
9
 * No-storage cache: all values will be "cached" in memory, in a simple PHP
10
 * array. Values will only be valid for 1 request: whatever is in memory at the
11
 * end of the request just dies. Other requests will start from a blank slate.
12
 *
13
 * This is mainly useful for testing purposes, where this class can let you test
14
 * application logic against cache, without having to run a cache server.
15
 *
16
 * @author Matthias Mullie <[email protected]>
17
 * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved
18
 * @license LICENSE MIT
19
 */
20
class MemoryStore implements KeyValueStore
21
{
22
    /**
23
     * @var array
24
     */
25
    protected $items = array();
26
27
    /**
28
     * @var int
29
     */
30
    protected $limit = 0;
31
32
    /**
33
     * @var int
34
     */
35
    protected $size = 0;
36
37
    /**
38
     * @param int|string $limit Memory limit in bytes (defaults to 10% of memory_limit)
39
     */
40
    public function __construct($limit = null)
41
    {
42
        if (null === $limit) {
43
            $phpLimit = ini_get('memory_limit');
44
            if ($phpLimit <= 0) {
45
                $this->limit = PHP_INT_MAX;
46
            } else {
47
                $this->limit = (int) ($this->shorthandToBytes($phpLimit) / 10);
48
            }
49
        } else {
50
            $this->limit = $this->shorthandToBytes($limit);
51
        }
52
    }
53
54
    /**
55
     * {@inheritdoc}
56
     */
57
    public function get($key, &$token = null)
58
    {
59
        if (!$this->exists($key)) {
60
            $token = null;
61
62
            return false;
63
        }
64
65
        $value = $this->items[$key][0];
66
67
        // use serialized version of stored value as CAS token
68
        $token = $value;
69
70
        return unserialize($value);
71
    }
72
73
    /**
74
     * {@inheritdoc}
75
     */
76
    public function getMulti(array $keys, array &$tokens = null)
77
    {
78
        $items = array();
79
        $tokens = array();
80
81
        foreach ($keys as $key) {
82
            if (!$this->exists($key)) {
83
                // omit missing keys from return array
84
                continue;
85
            }
86
87
            $items[$key] = $this->get($key, $token);
88
            $tokens[$key] = $token;
89
        }
90
91
        return $items;
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97
    public function set($key, $value, $expire = 0)
98
    {
99
        $this->size -= isset($this->items[$key]) ? strlen($this->items[$key][0]) : 0;
100
101
        $value = serialize($value);
102
        $expire = $this->normalizeTime($expire);
103
        $this->items[$key] = array($value, $expire);
104
105
        $this->size += strlen($value);
106
        $this->lru($key);
107
        $this->evict();
108
109
        return true;
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115
    public function setMulti(array $items, $expire = 0)
116
    {
117
        $success = array();
118
        foreach ($items as $key => $value) {
119
            $success[$key] = $this->set($key, $value, $expire);
120
        }
121
122
        return $success;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function delete($key)
129
    {
130
        $exists = $this->exists($key);
131
132
        if ($exists) {
133
            $this->size -= strlen($this->items[$key][0]);
134
            unset($this->items[$key]);
135
        }
136
137
        return $exists;
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143
    public function deleteMulti(array $keys)
144
    {
145
        $success = array();
146
147
        foreach ($keys as $key) {
148
            $success[$key] = $this->delete($key);
149
        }
150
151
        return $success;
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157
    public function add($key, $value, $expire = 0)
158
    {
159
        if ($this->exists($key)) {
160
            return false;
161
        }
162
163
        return $this->set($key, $value, $expire);
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    public function replace($key, $value, $expire = 0)
170
    {
171
        if (!$this->exists($key)) {
172
            return false;
173
        }
174
175
        return $this->set($key, $value, $expire);
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     */
181
    public function cas($token, $key, $value, $expire = 0)
182
    {
183
        if (!$this->exists($key)) {
184
            return false;
185
        }
186
187
        $this->get($key, $comparison);
188
        if ($comparison !== $token) {
189
            return false;
190
        }
191
192
        return $this->set($key, $value, $expire);
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function increment($key, $offset = 1, $initial = 0, $expire = 0)
199
    {
200
        if ($offset <= 0 || $initial < 0) {
201
            return false;
202
        }
203
204
        return $this->doIncrement($key, $offset, $initial, $expire);
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function decrement($key, $offset = 1, $initial = 0, $expire = 0)
211
    {
212
        if ($offset <= 0 || $initial < 0) {
213
            return false;
214
        }
215
216
        return $this->doIncrement($key, -$offset, $initial, $expire);
217
    }
218
219
    /**
220
     * {@inheritdoc}
221
     */
222
    public function touch($key, $expire)
223
    {
224
        $expire = $this->normalizeTime($expire);
225
226
        // get current value & re-save it, with new expiration
227
        $value = $this->get($key, $token);
228
229
        return $this->cas($token, $key, $value, $expire);
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235
    public function flush()
236
    {
237
        $this->items = array();
238
        $this->size = 0;
239
240
        return true;
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246
    public function getCollection($name)
247
    {
248
        return new Collection($this, $name);
249
    }
250
251
    /**
252
     * Checks if a value exists in cache and is not yet expired.
253
     *
254
     * @param string $key
255
     *
256
     * @return bool
257
     */
258
    protected function exists($key)
259
    {
260
        if (!array_key_exists($key, $this->items)) {
261
            // key not in cache
262
            return false;
263
        }
264
265
        $expire = $this->items[$key][1];
266
        if (0 !== $expire && $expire < time()) {
267
            // not permanent & already expired
268
            $this->size -= strlen($this->items[$key][0]);
269
            unset($this->items[$key]);
270
271
            return false;
272
        }
273
274
        $this->lru($key);
275
276
        return true;
277
    }
278
279
    /**
280
     * Shared between increment/decrement: both have mostly the same logic
281
     * (decrement just increments a negative value), but need their validation
282
     * split up (increment won't accept negative values).
283
     *
284
     * @param string $key
285
     * @param int    $offset
286
     * @param int    $initial
287
     * @param int    $expire
288
     *
289
     * @return int|bool
290
     */
291
    protected function doIncrement($key, $offset, $initial, $expire)
292
    {
293
        if (!$this->exists($key)) {
294
            $this->set($key, $initial, $expire);
295
296
            return $initial;
297
        }
298
299
        $value = $this->get($key);
300
        if (!is_numeric($value) || $value < 0) {
301
            return false;
302
        }
303
304
        $value += $offset;
305
        // value can never be lower than 0
306
        $value = max(0, $value);
307
        $this->set($key, $value, $expire);
308
309
        return $value;
310
    }
311
312
    /**
313
     * Times can be:
314
     * * relative (in seconds) to current time, within 30 days
315
     * * absolute unix timestamp
316
     * * 0, for infinity.
317
     *
318
     * The first case (relative time) will be normalized into a fixed absolute
319
     * timestamp.
320
     *
321
     * @param int $time
322
     *
323
     * @return int
324
     */
325
    protected function normalizeTime($time)
326
    {
327
        // 0 = infinity
328
        if (!$time) {
329
            return 0;
330
        }
331
332
        // relative time in seconds, <30 days
333
        if ($time < 30 * 24 * 60 * 60) {
334
            $time += time();
335
        }
336
337
        return $time;
338
    }
339
340
    /**
341
     * This cache uses least recently used algorithm. This is to be called
342
     * with the key to be marked as just used.
343
     */
344
    protected function lru($key)
345
    {
346
        // move key that has just been used to last position in the array
347
        $value = $this->items[$key];
348
        unset($this->items[$key]);
349
        $this->items[$key] = $value;
350
    }
351
352
    /**
353
     * Least recently used cache values will be evicted from cache should
354
     * it fill up too much.
355
     */
356
    protected function evict()
357
    {
358
        while ($this->size > $this->limit && !empty($this->items)) {
359
            $item = array_shift($this->items);
360
            $this->size -= strlen($item[0]);
361
        }
362
    }
363
364
    /**
365
     * Understands shorthand byte values (as used in e.g. memory_limit ini
366
     * setting) and converts them into bytes.
367
     *
368
     * @see http://php.net/manual/en/faq.using.php#faq.using.shorthandbytes
369
     *
370
     * @param string|int $shorthand Amount of bytes (int) or shorthand value (e.g. 512M)
371
     *
372
     * @return int
373
     */
374
    protected function shorthandToBytes($shorthand)
375
    {
376
        if (is_numeric($shorthand)) {
377
            // make sure that when float(1.234E17) is passed in, it doesn't get
378
            // cast to string('1.234E17'), then to int(1)
379
            return $shorthand;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $shorthand also could return the type string which is incompatible with the documented return type integer.
Loading history...
380
        }
381
382
        $units = array('B' => 1024, 'M' => pow(1024, 2), 'G' => pow(1024, 3));
383
384
        return (int) preg_replace_callback('/^([0-9]+)('.implode('|', array_keys($units)).')$/', function ($match) use ($units) {
385
            return $match[1] * $units[$match[2]];
386
        }, $shorthand);
387
    }
388
}
389