ItemCache   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 96
dl 0
loc 288
rs 9.36
c 1
b 0
f 0
wmc 38

13 Methods

Rating   Name   Duplication   Size   Complexity  
A delete() 0 7 2
A _gcMaxItems() 0 6 2
A _write() 0 10 4
A __construct() 0 14 2
A _isCacheFull() 0 7 2
A compareUpdated() 0 9 2
A __destruct() 0 6 2
A reset() 0 4 1
A set() 0 33 6
A _gcOutdated() 0 11 3
A get() 0 13 4
A _sort() 0 11 3
A _read() 0 20 5
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Saito - The Threaded Web Forum
7
 *
8
 * @copyright Copyright (c) the Saito Project Developers
9
 * @link https://github.com/Schlaefer/Saito
10
 * @license http://opensource.org/licenses/MIT
11
 */
12
13
namespace Saito\Cache;
14
15
use Stopwatch\Lib\Stopwatch;
16
17
/**
18
 * Class ItemCache
19
 *
20
 * Caches items and saves them persistently. Offers max item age and size
21
 * constrains.
22
 */
23
class ItemCache
24
{
25
26
    protected $_cache = null;
27
28
    /**
29
     * @var null|SaitoCacheEngineInterface if null cache is only active for this request
30
     */
31
    protected $_CacheEngine = null;
32
33
    protected $_settings = [
34
        'duration' => null,
35
        'maxItems' => null,
36
        // +/- percentage maxItems can deviate before gc is triggered
37
        'maxItemsFuzzy' => 0.06,
38
    ];
39
40
    protected $_gcFuzzy;
41
42
    protected $_gcMax;
43
44
    protected $_gcMin;
45
46
    protected $_name;
47
48
    protected $_now;
49
50
    protected $_oldestPersistent = 0;
51
52
    protected $_updated = false;
53
54
    /**
55
     * Constructor
56
     *
57
     * @param string $name name
58
     * @param SaitoCacheEngineInterface $CacheEngine engine
59
     * @param array $options options
60
     */
61
    public function __construct(
62
        $name,
63
        SaitoCacheEngineInterface $CacheEngine = null,
64
        $options = []
65
    ) {
66
        $this->_settings = $options + $this->_settings;
67
        $this->_now = time();
68
        $this->_name = $name;
69
        $this->_CacheEngine = $CacheEngine;
70
71
        if ($this->_settings['maxItems']) {
72
            $this->_gcFuzzy = $this->_settings['maxItemsFuzzy'];
73
            $this->_gcMax = (int)($this->_settings['maxItems'] * (1 + $this->_gcFuzzy));
74
            $this->_gcMin = (int)($this->_gcMax * (1 - $this->_gcFuzzy));
75
        }
76
    }
77
78
    /**
79
     * Deconstruct
80
     *
81
     * @return void
82
     */
83
    public function __destruct()
84
    {
85
        if ($this->_cache === null) {
86
            return;
87
        }
88
        $this->_write();
89
    }
90
91
    /**
92
     * Delete
93
     *
94
     * @param string $key key
95
     * @return void
96
     */
97
    public function delete($key)
98
    {
99
        if ($this->_cache === null) {
100
            $this->_read();
101
        }
102
        $this->_updated = true;
103
        unset($this->_cache[$key]);
104
    }
105
106
    /**
107
     * Getter
108
     *
109
     * @param string|null $key key
110
     * @return null|string
111
     */
112
    public function get($key = null)
113
    {
114
        if ($this->_cache === null) {
115
            $this->_read();
116
        }
117
        if ($key === null) {
118
            return $this->_cache;
119
        }
120
        if (!isset($this->_cache[$key])) {
121
            return null;
122
        }
123
124
        return $this->_cache[$key]['content'];
125
    }
126
127
    /**
128
     * compare updated
129
     *
130
     * @param string $key key
131
     * @param int $timestamp timestamp
132
     * @param callable $comp compare function
133
     * @return mixed
134
     * @throws \InvalidArgumentException
135
     */
136
    public function compareUpdated($key, $timestamp, callable $comp)
137
    {
138
        if (!isset($this->_cache[$key])) {
139
            throw new \InvalidArgumentException();
140
        }
141
142
        return $comp(
143
            $this->_cache[$key]['metadata']['content_last_updated'],
144
            $timestamp
145
        );
146
    }
147
148
    /**
149
     * Setter
150
     *
151
     * @param string $key key
152
     * @param mixed $content content
153
     * @param null $timestamp timestamp
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $timestamp is correct as it would always require null to be passed?
Loading history...
154
     * @return void
155
     */
156
    public function set($key, $content, $timestamp = null)
157
    {
158
        if ($this->_cache === null) {
159
            $this->_read();
160
        }
161
162
        if ($timestamp === null) {
0 ignored issues
show
introduced by
The condition $timestamp === null is always true.
Loading history...
163
            $timestamp = $this->_now;
164
        }
165
166
        /*
167
         * Don't fill the cache with entries which are removed by the maxItemsGC.
168
         * Old entries may trigger a store to disk on each request without
169
         * adding new cache data.
170
         */
171
        // there's no upper limit
172
        if (!$this->_gcMax) {
173
            $this->_updated = true;
174
            // the new entry is not older than the oldest existing
175
        } elseif ($timestamp > $this->_oldestPersistent) {
176
            $this->_updated = true;
177
            // there's still room in lower maxItemsGc limit
178
        } elseif (count($this->_cache) < $this->_gcMin) {
179
            $this->_updated = true;
180
        }
181
182
        $metadata = [
183
            'created' => $this->_now,
184
            'content_last_updated' => $timestamp,
185
        ];
186
187
        $data = ['metadata' => $metadata, 'content' => $content];
188
        $this->_cache[$key] = $data;
189
    }
190
191
    /**
192
     * Is cache full
193
     *
194
     * @return bool
195
     */
196
    protected function _isCacheFull()
197
    {
198
        if (!$this->_gcMax) {
199
            return false;
200
        }
201
202
        return count($this->_cache) >= $this->_settings['maxItems'];
203
    }
204
205
    /**
206
     * read
207
     *
208
     * @return void
209
     */
210
    protected function _read()
211
    {
212
        if ($this->_CacheEngine === null) {
213
            $this->_cache = [];
214
215
            return;
216
        }
217
        Stopwatch::start("ItemCache read: {$this->_name}");
218
        $this->_cache = $this->_CacheEngine->read($this->_name);
219
        if (empty($this->_cache)) {
220
            $this->_cache = [];
221
        }
222
        if ($this->_settings['duration']) {
223
            $this->_gcOutdated();
224
        }
225
        if (count($this->_cache) > 0) {
226
            $oldest = reset($this->_cache);
227
            $this->_oldestPersistent = $oldest['metadata']['content_last_updated'];
228
        }
229
        Stopwatch::stop("ItemCache read: {$this->_name}");
230
    }
231
232
    /**
233
     * Reset
234
     *
235
     * @return void
236
     */
237
    public function reset()
238
    {
239
        $this->_updated = true;
240
        $this->_cache = [];
241
    }
242
243
    /**
244
     * gc outdated
245
     *
246
     * @return void
247
     */
248
    protected function _gcOutdated()
249
    {
250
        Stopwatch::start("ItemCache _gcOutdated: {$this->_name}");
251
        $expired = time() - $this->_settings['duration'];
252
        foreach ($this->_cache as $key => $item) {
253
            if ($item['metadata']['created'] < $expired) {
254
                unset($this->_cache[$key]);
255
                $this->_updated = true;
256
            }
257
        }
258
        Stopwatch::stop("ItemCache _gcOutdated: {$this->_name}");
259
    }
260
261
    /**
262
     * garbage collection max items
263
     *
264
     * costly function for larger arrays, relieved by maxItemsFuzzy
265
     *
266
     * @return void
267
     */
268
    protected function _gcMaxItems()
269
    {
270
        if (count($this->_cache) <= $this->_gcMax) {
271
            return;
272
        }
273
        $this->_cache = array_slice($this->_cache, 0, $this->_gcMin, true);
274
    }
275
276
    /**
277
     * sorts for 'content_last_updated', oldest on top
278
     *
279
     * @return void
280
     */
281
    protected function _sort()
282
    {
283
        // keep items which were last used/updated
284
        uasort(
285
            $this->_cache,
286
            function ($a, $b) {
287
                if ($a['metadata']['content_last_updated'] === $b['metadata']['content_last_updated']) {
288
                    return 0;
289
                }
290
291
                return ($a['metadata']['content_last_updated'] < $b['metadata']['content_last_updated']) ? 1 : -1;
292
            }
293
        );
294
    }
295
296
    /**
297
     * Write
298
     *
299
     * @return void
300
     */
301
    protected function _write()
302
    {
303
        if ($this->_CacheEngine === null || !$this->_updated) {
304
            return;
305
        }
306
        $this->_sort();
307
        if ($this->_gcMax) {
308
            $this->_gcMaxItems();
309
        }
310
        $this->_CacheEngine->write($this->_name, $this->_cache);
311
    }
312
}
313