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

src/FastCache.php (1 issue)

Labels
Severity
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)
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 (empty($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): ?int {
191
                if (null === $item->expiry) {
0 ignored issues
show
The property expiry is declared private 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): ?int {
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;
212
213
                return $value;
214
            }
215
216
            $this->computing[$key] = $key;
217
218
            try {
219
                return $value = $callback(...[&$item, &$save]);
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($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
    private 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 false !== \strpos($this->namespace, '%s')
303
            ? \sprintf($this->namespace, $key) : $this->namespace . $key;
304
    }
305
306
    /**
307
     * Save cache item.
308
     *
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
    private function doSave(string $key, Closure $callback, Closure $setExpired, ?float $beta)
317
    {
318
        $storage = clone $this->storage;
319
320
        if ($storage instanceof CacheItemPoolInterface) {
321
            $item = $storage->getItem($key);
322
323
            if (!$item->isHit() || \INF === $beta) {
324
                $save   = true;
325
                $result = $callback(...[$item, $save]);
326
327
                if (false !== $save) {
328
                    if (!$result instanceof CacheItemInterface) {
329
                        $item->set($result);
330
                        $storage->save($item);
331
                    } else {
332
                        $storage->save($result);
333
                    }
334
                }
335
            }
336
337
            return $item->get();
338
        }
339
340
        $save   = true;
341
        $item   = $storage instanceof PhpCachePool ? new PhpCacheItem($key) : new CacheItem();
342
        $result = $callback(...[$item, $save]);
343
344
        if ($result instanceof CacheItemInterface) {
345
            $result = $result->get();
346
        }
347
348
        $storage->set($key, $result, $setExpired($item));
349
350
        return $result;
351
    }
352
353
    /**
354
     * Fetch cache item.
355
     *
356
     * @param string|string[] $ids The cache identifier to fetch
357
     *
358
     * @return mixed The corresponding values found in the cache
359
     */
360
    private function doFetch($ids)
361
    {
362
        if ($this->storage instanceof CacheItemPoolInterface) {
363
            return !\is_array($ids) ? $this->storage->getItem($ids) : $this->storage->getItems($ids);
364
        }
365
366
        return !\is_array($ids) ? $this->storage->get($ids) : $this->storage->getMultiple($ids, new stdClass());
367
    }
368
369
    /**
370
     * Remove an item from cache.
371
     *
372
     * @param string $id An identifier that should be removed from cache
373
     *
374
     * @return bool True if the items were successfully removed, false otherwise
375
     */
376
    private function doDelete(string $id)
377
    {
378
        if ($this->storage instanceof CacheItemPoolInterface) {
379
            return $this->storage->deleteItem($id);
380
        }
381
382
        return $this->storage->delete($id);
383
    }
384
}
385