Completed
Push — master ( df428d...59132f )
by Divine Niiquaye
02:39
created

CacheItemPool   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 256
Duplicated Lines 0 %

Test Coverage

Coverage 96.74%

Importance

Changes 0
Metric Value
eloc 91
dl 0
loc 256
rs 9.6
c 0
b 0
f 0
ccs 89
cts 92
cp 0.9674
wmc 35

14 Methods

Rating   Name   Duplication   Size   Complexity  
B createCacheItem() 0 30 8
A getItem() 0 11 2
A hasItem() 0 3 1
A save() 0 5 1
A getId() 0 11 2
A clear() 0 10 2
A __destruct() 0 4 2
A commit() 0 23 4
A deleteItem() 0 3 1
A mergeByLifetime() 0 28 5
A __construct() 0 6 1
A getItems() 0 9 2
A deleteItems() 0 19 3
A saveDeferred() 0 5 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of BiuradPHP opensource projects.
7
 *
8
 * PHP version 7.1 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Biurad\Cache;
19
20
use Biurad\Cache\Exceptions\InvalidArgumentException;
21
use Closure;
22
use Doctrine\Common\Cache\Cache as DoctrineCache;
23
use Doctrine\Common\Cache\FlushableCache;
24
use Doctrine\Common\Cache\MultiOperationCache;
25
use Psr\Cache\CacheItemInterface;
26
use Psr\Cache\CacheItemPoolInterface;
27
28
class CacheItemPool implements CacheItemPoolInterface
29
{
30
    /** @var DoctrineCache */
31
    private $cache;
32
33
    /**
34
     * @var Closure needs to be set by class, signature is function(string <key>, mixed <value>, bool <isHit>)
35
     */
36
    private $createCacheItem;
37
38
    /**
39
     * @var Closure needs to be set by class, signature is function(array <deferred>, array <&expiredIds>)
40
     */
41
    private $mergeByLifetime;
42
43
    /** @var array<string,CacheItemInterface> */
44
    private $deferred = [];
45
46
    /** @var array<string,string> */
47
    private $ids = [];
48
49
    /**
50
     * Cache Constructor.
51
     *
52
     * @param DoctrineCache $doctrine
53
     */
54 316
    public function __construct(DoctrineCache $doctrine)
55
    {
56 316
        $this->cache = $doctrine;
57
58 316
        $this->createCacheItem();
59 316
        $this->mergeByLifetime(Closure::fromCallable([$this, 'getId']));
60 316
    }
61
62
    /**
63
     * @codeCoverageIgnore
64
     */
65
    public function __destruct()
66
    {
67
        if (!empty($this->deferred)) {
68
            $this->commit();
69
        }
70
    }
71
72
    /**
73
     * {@inheritdoc}
74
     */
75 226
    public function getItem($key): CacheItemInterface
76
    {
77 226
        if (!empty($this->deferred)) {
78 7
            $this->commit();
79
        }
80
81 226
        $id    = $this->getId($key);
82 138
        $isHit = $this->cache->contains($id);
83 138
        $value = $this->cache->fetch($id);
84
85 138
        return ($this->createCacheItem)($id, $value, $isHit);
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91 65
    public function getItems(array $keys = [])
92
    {
93 65
        $items = [];
94
95 65
        foreach ($keys as $key) {
96 64
            $items[$key] = $this->getItem($key);
97
        }
98
99 29
        return $items;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105 48
    public function hasItem($key): bool
106
    {
107 48
        return $this->getItem($key)->isHit();
108
    }
109
110
    /**
111
     * {@inheritdoc}
112
     */
113 315
    public function clear(): bool
114
    {
115
        // Clear the deferred items
116 315
        $this->deferred = [];
117
118 315
        if ($this->cache instanceof FlushableCache) {
119 315
            return $this->cache->flushAll();
120
        }
121
122
        return false;
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128 41
    public function deleteItem($key): bool
129
    {
130 41
        return $this->deleteItems([$key]);
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136 79
    public function deleteItems(array $keys): bool
137
    {
138 79
        $deleted = true;
139
140 79
        foreach ($keys as $key) {
141 79
            $key = $this->getId($key);
142
143
            // Delete form deferred
144 44
            unset($this->deferred[$key]);
145
146
            // We have to commit here to be able to remove deferred hierarchy items
147 44
            $this->commit();
148
149 44
            if (!$this->cache->delete($key)) {
150
                $deleted = false;
151
            }
152
        }
153
154 9
        return $deleted;
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 56
    public function save(CacheItemInterface $item): bool
161
    {
162 56
        $this->saveDeferred($item);
163
164 56
        return $this->commit();
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170 80
    public function saveDeferred(CacheItemInterface $item): bool
171
    {
172 80
        $this->deferred[$item->getKey()] = $item;
173
174 80
        return true;
175
    }
176
177
    /**
178
     * {@inheritdoc}
179
     */
180 115
    public function commit(): bool
181
    {
182 115
        $ok             = true;
183 115
        $byLifetime     = $this->mergeByLifetime;
184 115
        $expiredIds     = [];
185 115
        $byLifetime     = $byLifetime($this->deferred, $expiredIds);
186
187 115
        \assert($this->cache instanceof MultiOperationCache);
188
189 115
        if (!empty($expiredIds)) {
190 5
            $this->cache->deleteMultiple($expiredIds);
191
        }
192
193 115
        foreach ($byLifetime as $lifetime => $values) {
194 76
            if ($this->cache->saveMultiple($values, $lifetime)) {
195 76
                continue;
196
            }
197
198
            $ok = false;
199
        }
200 115
        $this->deferred = [];
201
202 115
        return $ok;
203
    }
204
205
    /**
206
     * @param mixed $key
207
     *
208
     * @return string
209
     */
210 296
    private function getId($key): string
211
    {
212 296
        if (!\is_string($key)) {
213 77
            throw new InvalidArgumentException(
214 77
                \sprintf('Cache key must be string, "%s" given', \gettype($key))
215
            );
216
        }
217
218 247
        $key = CacheItem::validateKey($key);
219
220 173
        return $this->ids[$key] ?? $this->ids[$key] = $key;
221
    }
222
223 316
    private function createCacheItem(): void
224
    {
225 316
        $this->createCacheItem = Closure::bind(
226 316
            static function (string $key, $value, bool $isHit): CacheItemInterface {
227 138
                $item = new CacheItem();
228 138
                $item->key   = $key;
0 ignored issues
show
Bug introduced by
The property key is declared private in Biurad\Cache\CacheItem and cannot be accessed from this context.
Loading history...
229 138
                $item->value = $v = $isHit ? $value : null;
0 ignored issues
show
Bug introduced by
The property value is declared private in Biurad\Cache\CacheItem and cannot be accessed from this context.
Loading history...
230 138
                $item->isHit = $isHit;
0 ignored issues
show
Bug introduced by
The property isHit is declared private in Biurad\Cache\CacheItem and cannot be accessed from this context.
Loading history...
231 138
                $item->defaultLifetime = 0;
0 ignored issues
show
Bug introduced by
The property defaultLifetime is declared private in Biurad\Cache\CacheItem and cannot be accessed from this context.
Loading history...
232
233
                // Detect wrapped values that encode for their expiry and creation duration
234
                // For compactness, these values are packed in the key of an array using
235
                // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F
236
                // @codeCoverageIgnoreStart
237
                if (
238
                    \is_array($v) &&
239
                    1 === \count($v) &&
240
                    10 === \strlen($k = (string) \key($v)) &&
241
                    "\x9D" === $k[0] &&
242
                    "\0" === $k[5] &&
243
                    "\x5F" === $k[9]
244
                ) {
245
                    $item->value = $v[$k];
246
                }
247
                // @codeCoverageIgnoreEnd
248
249 138
                return $item;
250 316
            },
251 316
            null,
252 316
            CacheItem::class
253
        );
254 316
    }
255
256 316
    private function mergeByLifetime(callable $getId): void
257
    {
258 316
        $this->mergeByLifetime = Closure::bind(
259 316
            static function ($deferred, &$expiredIds) use ($getId): array {
260 115
                $byLifetime = [];
261 115
                $now = \microtime(true);
262 115
                $expiredIds = [];
263
264 115
                foreach ($deferred as $key => $item) {
265 79
                    $key = (string) $key;
266
267 79
                    if (null === $item->expiry) {
268 71
                        $ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0;
269 9
                    } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
270 5
                        $expiredIds[] = $getId($key);
271
272 5
                        continue;
273
                    }
274
275
                    // For compactness, expiry and creation duration are packed in the key of an array,
276
                    // using magic numbers as separators
277 76
                    $byLifetime[$ttl][$getId($key)] = $item->value;
278
                }
279
280 115
                return $byLifetime;
281 316
            },
282 316
            null,
283 316
            CacheItem::class
284
        );
285 316
    }
286
}
287