Completed
Push — master ( b2545f...e44389 )
by Divine Niiquaye
02:25
created

FastCache::save()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 63
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 35
nc 4
nop 3
dl 0
loc 63
rs 8.4266
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\CacheException;
21
use Biurad\Cache\Exceptions\InvalidArgumentException;
22
use Biurad\Cache\Interfaces\FastCacheInterface;
23
use Cache\Adapter\Common\CacheItem as PhpCacheItem;
24
use Cache\Adapter\Common\PhpCachePool;
25
use Closure;
26
use Generator;
27
use Psr\Cache\CacheItemInterface;
28
use Psr\Cache\CacheItemPoolInterface;
29
use Psr\SimpleCache\CacheInterface;
30
use stdClass;
31
use Throwable;
32
33
/**
34
 * Implements the cache for a application.
35
 */
36
class FastCache implements FastCacheInterface
37
{
38
    /** @internal */
39
    public const NAMESPACE_SEPARATOR = "\x00";
40
41
    public const NAMESPACE = 'CACHE_KEY[%s]';
42
43
    /** @var CacheInterface|CacheItemPoolInterface */
44
    private $storage;
45
46
    /** @var string */
47
    private $namespace;
48
49
    /** @var array<string,mixed> */
50
    private $computing = [];
51
52
    /**
53
     * @param CacheInterface|CacheItemPoolInterface $storage
54
     * @param string                                $namespace
55
     */
56
    final public function __construct($storage, string $namespace = self::NAMESPACE)
57
    {
58
        if (
59
            !($storage instanceof CacheInterface || $storage instanceof CacheItemPoolInterface)
0 ignored issues
show
introduced by
$storage is always a sub-type of Psr\Cache\CacheItemPoolInterface.
Loading history...
60
        ) {
61
            throw new CacheException('$storage can only implements psr-6 or psr-16 cache interface');
62
        }
63
64
        $this->storage   = $storage;
65
        $this->namespace = $namespace . self::NAMESPACE_SEPARATOR;
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function getStorage()
72
    {
73
        return $this->storage;
74
    }
75
76
    /**
77
     * Returns cache namespace.
78
     */
79
    public function getNamespace(): string
80
    {
81
        return \substr(\sprintf($this->namespace, ''), 0, -1);
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    public function derive(string $namespace): FastCache
88
    {
89
        return new static($this->storage, $this->namespace . $namespace);
90
    }
91
92
    /**
93
     * {@inheritdoc}
94
     */
95
    public function load($key, callable $fallback = null, ?float $beta = null)
96
    {
97
        $data = $this->doFetch($this->generateKey($key));
98
99
        if ($data instanceof CacheItemInterface) {
100
            $data = $data->isHit() ? $data->get() : null;
101
        }
102
103
        if (null === $data && null !== $fallback) {
104
            return $this->save($key, $fallback, $beta);
105
        }
106
107
        return $data;
108
    }
109
110
    /**
111
     * {@inheritDoc}
112
     */
113
    public function bulkLoad(array $keys, callable $fallback = null, ?float $beta = null): array
114
    {
115
        if (0 === \count($keys)) {
116
            return [];
117
        }
118
119
        foreach ($keys as $key) {
120
            if (!\is_scalar($key)) {
121
                throw new InvalidArgumentException('Only scalar keys are allowed in bulkLoad()');
122
            }
123
        }
124
        $storageKeys = \array_map([$this, 'generateKey'], $keys);
125
        $cacheData   = $this->doFetch($storageKeys);
126
        $result      = [];
127
128
        if ($cacheData instanceof Generator) {
129
            $cacheData = \iterator_to_array($cacheData);
130
        }
131
132
        foreach ($keys as $i => $key) {
133
            $storageKey = $storageKeys[$i];
134
135
            if (isset($cacheData[$storageKey])) {
136
                $result[$key] = $cacheData[$storageKey];
137
            } elseif (null !== $fallback) {
138
                $result[$key] = $this->save(
139
                    $key,
140
                    function (CacheItemInterface $item, bool $save) use ($key, $fallback) {
141
                        return $fallback(...[$key, &$item, &$save]);
142
                    },
143
                    $beta
144
                );
145
            } else {
146
                $result[$key] = null;
147
            }
148
        }
149
150
        return \array_map(
151
            function ($value) {
152
                if ($value instanceof CacheItemInterface) {
153
                    return $value->get();
154
                }
155
156
                return $value;
157
            },
158
            $result
159
        );
160
    }
161
162
    /**
163
     * {@inheritdoc}
164
     *
165
     * @psalm-suppress InaccessibleProperty
166
     */
167
    public function save($key, ?callable $callback = null, ?float $beta = null)
168
    {
169
        $key = $this->generateKey($key);
170
171
        if (null === $callback) {
172
            $this->doDelete($key);
173
174
            return false;
175
        }
176
177
        if (0 > $beta = $beta ?? 1.0) {
178
            throw new InvalidArgumentException(
179
                \sprintf(
180
                    'Argument "$beta" provided to "%s::get()" must be a positive number, %f given.',
181
                    static::class,
182
                    $beta
183
                )
184
            );
185
        }
186
187
        static $setExpired;
188
189
        $setExpired = Closure::bind(
190
            static function (CacheItem $item) {
191
                if (null === $item->expiry) {
0 ignored issues
show
Bug introduced by
The property expiry is declared protected in Biurad\Cache\CacheItem and cannot be accessed from this context.
Loading history...
192
                    return null;
193
                }
194
195
                return (int) (0.1 + $item->expiry - \microtime(true));
196
            },
197
            null,
198
            CacheItem::class
199
        );
200
201
        if ($this->storage instanceof PhpCachePool) {
202
            $setExpired = static function (PhpCacheItem $item) {
203
                return $item->getExpirationTimestamp();
204
            };
205
        }
206
207
        $callback = function (CacheItemInterface $item, bool $save) use ($key, $callback) {
208
            // don't wrap nor save recursive calls
209
            if (isset($this->computing[$key])) {
210
                $value = $callback(...[&$item, &$save]);
211
                $save  = false;
0 ignored issues
show
Unused Code introduced by
The assignment to $save is dead and can be removed.
Loading history...
212
213
                return $value;
214
            }
215
216
            $this->computing[$key] = $key;
217
218
            try {
219
                return $value = $callback(...[&$item, &$save]);
0 ignored issues
show
Unused Code introduced by
The assignment to $value is dead and can be removed.
Loading history...
220
            } catch (Throwable $e) {
221
                $this->doDelete($key);
222
223
                throw $e;
224
            } finally {
225
                unset($this->computing[$key]);
226
            }
227
        };
228
229
        return $this->doSave($this->storage, $key, $callback, $setExpired, $beta);
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235
    public function delete($key): void
236
    {
237
        $this->save($key, null);
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243
    public function call(callable $callback, ?float $beta = null)
244
    {
245
        $key = \func_get_args();
246
247
        if (\is_array($callback) && \is_object($callback[0])) {
248
            $key[0][0] = \get_class($callback[0]);
249
        }
250
251
        return $this->load(
252
            $key,
253
            function (CacheItemInterface $item, bool $save) use ($callback, $key) {
254
                $dependencies = \array_merge(\array_slice($key, 1), [&$item, &$save]);
255
256
                return $callback(...$dependencies);
257
            },
258
            $beta
259
        );
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     */
265
    public function wrap(callable $callback, ?float $beta = null): callable
266
    {
267
        return function () use ($callback, $beta) {
268
            return $this->call($callback, $beta);
269
        };
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function start($key): ?OutputHelper
276
    {
277
        $data = $this->load($key);
278
279
        if (null === $data) {
280
            return new OutputHelper($this, $key);
281
        }
282
        echo $data;
283
284
        return null;
285
    }
286
287
    /**
288
     * Generates internal cache key.
289
     *
290
     * @param mixed $key
291
     *
292
     * @return string
293
     */
294
    protected function generateKey($key): string
295
    {
296
        if (\is_array($key) && \current($key) instanceof Closure) {
297
            $key = \spl_object_id($key[0]);
298
        }
299
300
        $key = \md5(\is_scalar($key) ? (string) $key : \serialize($key));
301
302
        return \strpos($this->namespace, '%s') ? \sprintf($this->namespace, $key) : $this->namespace . $key;
303
    }
304
305
    /**
306
     * Save cache item.
307
     *
308
     * @param CacheInterface|CacheItemPoolInterface $storage
309
     * @param string                                $key
310
     * @param Closure                               $callback
311
     * @param Closure                               $setExpired
312
     * @param null|float                            $beta
313
     *
314
     * @return mixed The corresponding values found in the cache
315
     */
316
    protected function doSave($storage, string $key, Closure $callback, Closure $setExpired, ?float $beta)
317
    {
318
        if ($storage instanceof CacheItemPoolInterface) {
319
            $item = $storage->getItem($key);
320
321
            if (!$item->isHit() || \INF === $beta) {
322
                $save   = true;
323
                $result = $callback(...[$item, $save]);
324
325
                if (false !== $save) {
0 ignored issues
show
introduced by
The condition false !== $save is always true.
Loading history...
326
                    if (!$result instanceof CacheItemInterface) {
327
                        $item->set($result);
328
                        $storage->save($item);
329
                    } else {
330
                        $storage->save($result);
331
                    }
332
                }
333
            }
334
335
            return $item->get();
336
        }
337
338
        $save   = true;
339
        $item   = $storage instanceof PhpCachePool ? new PhpCacheItem($key) : new CacheItem();
340
        $result = $callback(...[$item, $save]);
341
342
        if ($result instanceof CacheItemInterface) {
343
            $result = $result->get();
344
        }
345
346
        $storage->set($key, $result, $setExpired($item));
347
348
        return $result;
349
    }
350
351
    /**
352
     * Fetch cache item.
353
     *
354
     * @param string|string[] $ids The cache identifier to fetch
355
     *
356
     * @return mixed The corresponding values found in the cache
357
     */
358
    protected function doFetch($ids)
359
    {
360
        if ($this->storage instanceof CacheItemPoolInterface) {
361
            return !\is_array($ids) ? $this->storage->getItem($ids) : $this->storage->getItems($ids);
362
        }
363
364
        return !\is_array($ids) ? $this->storage->get($ids) : $this->storage->getMultiple($ids, new stdClass());
365
    }
366
367
    /**
368
     * Remove an item from cache.
369
     *
370
     * @param string $id An identifier that should be removed from cache
371
     *
372
     * @return bool True if the items were successfully removed, false otherwise
373
     */
374
    protected function doDelete(string $id)
375
    {
376
        if ($this->storage instanceof CacheItemPoolInterface) {
377
            return $this->storage->deleteItem($id);
378
        }
379
380
        return $this->storage->delete($id);
381
    }
382
}
383