Passed
Push — master ( d8bef9...691f6c )
by Divine Niiquaye
06:32
created

src/FastCache.php (2 issues)

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 BiuradPHP\Cache;
19
20
use BiuradPHP\Cache\Exceptions\CacheException;
21
use BiuradPHP\Cache\Exceptions\InvalidArgumentException;
22
use BiuradPHP\Cache\Interfaces\FastCacheInterface;
23
use Cache\Adapter\Common\PhpCacheItem;
0 ignored issues
show
The type Cache\Adapter\Common\PhpCacheItem was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use Cache\Adapter\Common\PhpCachePool;
0 ignored issues
show
The type Cache\Adapter\Common\PhpCachePool was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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 */
50
    private $computing = [];
51
52
    /**
53
     * @param CacheInterface|CacheItemPoolInterface $storage
54
     * @param string                                $namespace
55
     */
56
    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 (string) \substr(\sprintf($this->namespace, null), 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 && $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, true);
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 ($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
    public function save($key, ?callable $callback = null, ?float $beta = null)
166
    {
167
        $key = $this->generateKey($key);
168
169
        if (null === $callback) {
170
            $this->doDelete($key);
171
172
            return;
173
        }
174
175
        if (0 > $beta = $beta ?? 1.0) {
176
            throw new InvalidArgumentException(
177
                \sprintf(
178
                    'Argument "$beta" provided to "%s::get()" must be a positive number, %f given.',
179
                    static::class,
180
                    $beta
181
                )
182
            );
183
        }
184
185
        static $setExpired;
186
187
        $setExpired = Closure::bind(
188
            static function (CacheItem $item) {
189
                if (null === $item->expiry) {
190
                    return null;
191
                }
192
193
                return (int) (0.1 + $item->expiry - \microtime(true));
194
            },
195
            null,
196
            CacheItem::class
197
        );
198
199
        if ($this->storage instanceof PhpCachePool) {
200
            $setExpired = static function (PhpCacheItem $item) {
201
                return $item->getExpirationTimestamp();
202
            };
203
        }
204
205
        $callback = function (CacheItemInterface $item, bool &$save) use ($key, $callback) {
206
            // don't wrap nor save recursive calls
207
            if (isset($this->computing[$key])) {
208
                $value = $callback(...[&$item, &$save]);
209
                $save  = false;
210
211
                return $value;
212
            }
213
214
            $this->computing[$key] = $key;
215
216
            try {
217
                return $value = $callback(...[&$item, &$save]);
218
            } catch (Throwable $e) {
219
                $this->doDelete($key);
220
221
                throw $e;
222
            } finally {
223
                unset($this->computing[$key]);
224
            }
225
        };
226
227
        if ($this->storage instanceof CacheItemPoolInterface) {
228
            $item = $this->storage->getItem($key);
229
230
            if (!$item->isHit() || \INF === $beta) {
231
                $save   = true;
232
                $result = $callback(...[$item, $save]);
233
234
                if ($save) {
235
                    if (!$result instanceof CacheItemInterface) {
236
                        $item->set($result);
237
                        $this->storage->save($item);
238
                    } else {
239
                        $this->storage->save($result);
240
                    }
241
                }
242
            }
243
244
            return $item->get();
245
        }
246
247
        $save   = true;
248
        $item   = $this->storage instanceof PhpCachePool ? new PhpCacheItem() : new CacheItem();
249
        $result = $callback(...[$item, $save]);
250
251
        if ($result instanceof CacheItemInterface) {
252
            $result = $result->get();
253
        }
254
255
        $this->storage->set($key, $result, $setExpired($item));
256
257
        return $result;
258
    }
259
260
    /**
261
     * {@inheritdoc}
262
     */
263
    public function delete($key): void
264
    {
265
        $this->save($key, null);
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271
    public function call(callable $callback)
272
    {
273
        $key = \func_get_args();
274
275
        if (\is_array($callback) && \is_object($callback[0])) {
276
            $key[0][0] = \get_class($callback[0]);
277
        }
278
279
        return $this->load($key, function (CacheItemInterface $item, bool $save) use ($callback, $key) {
280
            $dependencies = \array_merge(\array_slice($key, 1), [&$item, &$save]);
281
282
            return $callback(...$dependencies);
283
        });
284
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289
    public function wrap(callable $callback, ?float $beta = null): callable
290
    {
291
        return function () use ($callback, $beta) {
292
            $key = [$callback, \func_get_args()];
293
294
            if (\is_array($callback) && \is_object($callback[0])) {
295
                $key[0][0] = \get_class($callback[0]);
296
            }
297
298
            if (null === $data = $this->load($key)) {
299
                $data = $this->save(
300
                    $key,
301
                    function (CacheItemInterface $item, bool $save) use ($callback, $key) {
302
                        $dependencies = \array_merge($key[1], [&$item, &$save]);
303
304
                        return $callback(...$dependencies);
305
                    },
306
                    $beta
307
                );
308
            }
309
310
            return $data;
311
        };
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317
    public function start($key): ?OutputHelper
318
    {
319
        $data = $this->load($key);
320
321
        if (null === $data) {
322
            return new OutputHelper($this, $key);
323
        }
324
        echo $data;
325
326
        return null;
327
    }
328
329
    /**
330
     * Generates internal cache key.
331
     *
332
     * @param mixed $key
333
     *
334
     * @return string
335
     */
336
    protected function generateKey($key): string
337
    {
338
        if (\is_array($key) && \current($key) instanceof Closure) {
339
            $key = \spl_object_id($key[0]);
340
        }
341
342
        $key = \md5(\is_scalar($key) ? (string) $key : \serialize($key));
343
344
        return \strpos($this->namespace, '%s') ? \sprintf($this->namespace, $key) : $this->namespace . $key;
345
    }
346
347
    /**
348
     * Fetch cache item.
349
     *
350
     * @param array|string $id The cache identifier to fetch
351
     *
352
     * @return mixed The corresponding values found in the cache
353
     */
354
    protected function doFetch($ids, bool $multiple = false)
355
    {
356
        if ($this->storage instanceof CacheItemPoolInterface) {
357
            return !$multiple ? $this->storage->getItem($ids) : $this->storage->getItems($ids);
358
        }
359
360
        return !$multiple ? $this->storage->get($ids) : $this->storage->getMultiple($ids, new stdClass());
361
    }
362
363
    /**
364
     * Remove an item from cache.
365
     *
366
     * @param string $id An identifier that should be removed from cache
367
     *
368
     * @return bool True if the items were successfully removed, false otherwise
369
     */
370
    protected function doDelete(string $id)
371
    {
372
        if ($this->storage instanceof CacheItemPoolInterface) {
373
            return $this->storage->deleteItem($id);
374
        }
375
376
        return $this->storage->delete($id);
377
    }
378
}
379