FastCache::call()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 12
ccs 0
cts 8
cp 0
rs 10
cc 3
nc 2
nop 2
crap 12
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Biurad 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 Cache\Adapter\Common\HasExpirationTimestampInterface;
23
use Psr\Cache\CacheItemInterface;
24
use Psr\Cache\CacheItemPoolInterface;
25
use Psr\SimpleCache\CacheInterface;
26
use Phpfastcache\Core\Item\ExtendedCacheItemInterface;
27
28
/**
29
 * An advanced caching system using PSR-6 or PSR-16.
30
 *
31
 * @final
32
 *
33
 * @author Divine Niiquaye Ibok <[email protected]>
34
 */
35
class FastCache
36
{
37
    private const NAMESPACE = '';
38
39
    /** @var CacheInterface|CacheItemPoolInterface */
40
    private $storage;
41
42
    /** @var string */
43
    private $namespace;
44
45
    /** @var array<string,mixed> */
46
    private $computing = [];
47
48
    /** @var string */
49
    private $cacheItemClass = CacheItem::class;
50
51
    /**
52
     * @param CacheInterface|CacheItemPoolInterface $storage
53
     */
54
    final public function __construct($storage, string $namespace = self::NAMESPACE)
55
    {
56
        if (!($storage instanceof CacheInterface || $storage instanceof CacheItemPoolInterface)) {
0 ignored issues
show
introduced by
$storage is always a sub-type of Psr\Cache\CacheItemPoolInterface.
Loading history...
57
            throw new CacheException('$storage can only implements PSR-6 or PSR-16 cache interface.');
58
        }
59
60
        $this->storage = $storage;
61
        $this->namespace = $namespace;
62
    }
63
64
    /**
65
     * Set a custom cache item class.
66
     */
67
    public function setCacheItem(string $cacheItemClass): void
68
    {
69
        if (\is_subclass_of($cacheItemClass, CacheItemInterface::class)) {
70
            $this->cacheItemClass = $cacheItemClass;
71
        }
72
    }
73
74
    /**
75
     * @return CacheInterface|CacheItemPoolInterface
76
     */
77
    public function getStorage()
78
    {
79
        return $this->storage;
80
    }
81
82
    /**
83
     * Returns cache namespace.
84
     */
85
    public function getNamespace(): string
86
    {
87
        return $this->namespace;
88
    }
89
90
    /**
91
     * Returns new nested cache object.
92
     */
93
    public function derive(string $namespace): self
94
    {
95
        return new static($this->storage, $this->namespace . $namespace);
96
    }
97
98
    /**
99
     * Reads the specified item from the cache or generate it.
100
     *
101
     * @return mixed
102
     */
103
    public function load(string $key, callable $fallback = null, ?float $beta = null)
104
    {
105
        $data = $this->doFetch($this->namespace . $key);
106
107
        if ($data instanceof CacheItemInterface) {
108
            $data = $data->isHit() ? $data->get() : null;
109
        }
110
111
        if (null === $data && null !== $fallback) {
112
            return $this->save($key, $fallback, $beta);
113
        }
114
115
        return $data;
116
    }
117
118
    /**
119
     * Reads multiple items from the cache.
120
     *
121
     * @param array<string,mixed> $keys
122
     *
123
     * @return array<int|string,mixed>
124
     */
125
    public function bulkLoad(array $keys, callable $fallback = null, ?float $beta = null): array
126
    {
127
        if (empty($keys)) {
128
            return [];
129
        }
130
131
        $result = [];
132
133
        foreach ($keys as $key) {
134
            if (!\is_string($key)) {
135
                throw new \InvalidArgumentException('Only string keys are allowed in bulkLoad().');
136
            }
137
138
            $result[$key] = $this->load(
139
                \md5($key), // encode key
140
                static function (CacheItemInterface $item) use ($key, $fallback) {
141
                    return $fallback($key, $item);
142
                },
143
                $beta
144
            );
145
        }
146
147
        return $result;
148
    }
149
150
    /**
151
     * Writes an item into the cache.
152
     *
153
     * @return mixed value itself
154
     */
155
    public function save(string $key, callable $callback, ?float $beta = null)
156
    {
157
        $key = $this->namespace . $key;
158
159
        if (0 > $beta = $beta ?? 1.0) {
160
            throw new InvalidArgumentException(
161
                \sprintf('Argument "$beta" provided to "%s::save()" must be a positive number, %f given.', __CLASS__, $beta)
162
            );
163
        }
164
165
        return $this->doSave($key, $callback, $beta);
166
    }
167
168
    /**
169
     * Remove an item from the cache.
170
     */
171
    public function delete(string $key): bool
172
    {
173
        return $this->doDelete($key);
174
    }
175
176
    /**
177
     * Caches results of function/method calls.
178
     *
179
     * @return mixed
180
     */
181
    public function call(callable $callback, ...$arguments)
182
    {
183
        $key = $arguments;
184
185
        if (\is_array($callback) && \is_object($callback[0])) {
186
            $key[0][0] = \get_class($callback[0]);
187
        }
188
189
        return $this->load(
190
            $this->generateKey($key),
191
            static function (CacheItemInterface $item) use ($callback, $key) {
192
                return $callback(...$key + [$item]);
193
            }
194
        );
195
    }
196
197
    /**
198
     * Alias of `call` method wrapped with a closure.
199
     *
200
     * @see {@call}
201
     *
202
     * @return callable so arguments can be passed into for final results
203
     */
204
    public function wrap(callable $callback /* ... arguments passed to $callback */): callable
205
    {
206
        return function () use ($callback) {
207
            return $this->call($callback, ...\func_get_args());
208
        };
209
    }
210
211
    /**
212
     * Starts the output cache.
213
     */
214
    public function start(string $key): ?OutputHelper
215
    {
216
        $data = $this->load($key);
217
218
        if (null === $data) {
219
            return new OutputHelper($this, $key);
220
        }
221
        echo $data;
222
223
        return null;
224
    }
225
226
    /**
227
     * Generates internal cache key.
228
     *
229
     * @param mixed $key
230
     */
231
    private function generateKey($key): string
232
    {
233
        if (\is_object($key)) {
234
            $key = \spl_object_id($key);
235
        } elseif (\is_array($key)) {
236
            $key = \md5(\implode('', $key));
237
        }
238
239
        return $this->namespace . (string) $key;
240
    }
241
242
    /**
243
     * Save cache item.
244
     *
245
     * @return mixed The corresponding values found in the cache
246
     */
247
    private function doSave(string $key, callable $callback, ?float $beta)
248
    {
249
        $storage = $this->storage;
250
251
        if ($storage instanceof CacheItemPoolInterface) {
252
            $item = $storage->getItem($key);
253
254
            if (!$item->isHit() || \INF === $beta) {
255
                $result = $this->doCreate($item, $callback, $expiry);
256
257
                if (!$result instanceof CacheItemInterface) {
258
                    $result = $item->set($result);
259
                }
260
261
                $storage->save($result);
262
            }
263
264
            return $item->get();
265
        }
266
267
        $result = $this->doCreate(new $this->cacheItemClass(), $callback, $expiry);
268
269
        if ($result instanceof CacheItemInterface) {
270
            $result = $result->get();
271
        }
272
273
        $storage->set($key, $result, $expiry);
274
275
        return $result;
276
    }
277
278
    /**
279
     * @param int $expiry
280
     *
281
     * @return mixed|CacheItemInterface
282
     */
283
    private function doCreate(CacheItemInterface $item, callable $callback, int &$expiry = null)
284
    {
285
        $key = $item->getKey();
286
287
        // don't wrap nor save recursive calls
288
        if (isset($this->computing[$key])) {
289
            throw new CacheException(\sprintf('Duplicated cache key found "%s", causing a circular reference.', $key));
290
        }
291
292
        $this->computing[$key] = true;
293
294
        try {
295
            $item = $callback($item);
296
297
            // Find expiration time ...
298
            if ($item instanceof ExtendedCacheItemInterface) {
299
                $expiry = $item->getTtl();
300
            } elseif ($item instanceof CacheItemInterface) {
301
                if ($item instanceof HasExpirationTimestampInterface) {
302
                    $maxAge = $item->getExpirationTimestamp();
303
                } elseif (\method_exists($item, 'getExpiry')) {
304
                    $maxAge = $item->getExpiry();
305
                }
306
307
                if (isset($maxAge)) {
308
                    $expiry = (int) (0.1 + $maxAge - \microtime(true));
309
                }
310
            }
311
312
            return $item;
313
        } catch (\Throwable $e) {
314
            $this->doDelete($key);
315
316
            throw $e;
317
        } finally {
318
            unset($this->computing[$key]);
319
        }
320
    }
321
322
    /**
323
     * Fetch cache item.
324
     *
325
     * @param string|string[] $ids The cache identifier to fetch
326
     *
327
     * @return mixed The corresponding values found in the cache
328
     */
329
    private function doFetch($ids)
330
    {
331
        $fetchMethod = $this->storage instanceof CacheItemPoolInterface
332
            ? 'getItem' . (\is_array($ids) ? 's' : null)
333
            : 'get' . (!\is_array($ids) ? 'Multiple' : null);
334
335
        return $this->storage->{$fetchMethod}($ids);
336
    }
337
338
    /**
339
     * Remove an item from cache.
340
     *
341
     * @param string $id An identifier that should be removed from cache
342
     *
343
     * @return bool True if the items were successfully removed, false otherwise
344
     */
345
    private function doDelete(string $id)
346
    {
347
        if ($this->storage instanceof CacheItemPoolInterface) {
348
            $deleteItem = 'Item';
349
        }
350
351
        return $this->storage->{'delete' . $deleteItem ?? null}($id);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $deleteItem does not seem to be defined for all execution paths leading up to this point.
Loading history...
352
    }
353
}
354