Passed
Pull Request — master (#218)
by
unknown
08:09
created

JibitFileCache::store()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 12
c 1
b 0
f 1
nc 3
nop 4
dl 0
loc 20
rs 9.8666
1
<?php
2
namespace Shetabit\Multipay\Drivers\Jibit;
3
4
/**
5
 * PhpFileCache - Light, simple and standalone PHP in-file caching class
6
 * This class was heavily inspired by Simple-PHP-Cache. Huge thanks to Christian Metz
7
 * @license MIT
8
 * @author Wruczek https://github.com/Wruczek
9
 */
10
class JibitFileCache
11
{
12
13
    /**
14
     * Path to the cache directory
15
     * @var string
16
     */
17
    private $cacheDir;
18
19
    /**
20
     * Cache file name
21
     * @var string
22
     */
23
    private $cacheFilename;
24
25
    /**
26
     * Cache file name, hashed with sha1. Used as an actual file name
27
     * @var string
28
     */
29
    private $cacheFilenameHashed;
30
31
    /**
32
     * Cache file extension
33
     * @var string
34
     */
35
    private $cacheFileExtension;
36
37
    /**
38
     * Holds current cache
39
     * @var array
40
     */
41
    private $cacheArray;
42
43
    /**
44
     * If true, cache expire after one second
45
     * @var bool
46
     */
47
    private $devMode;
48
49
    /**
50
     * Cache constructor.
51
     * @param string $cacheDirPath cache directory. Must end with "/"
52
     * @param string $cacheFileName cache file name
53
     * @param string $cacheFileExtension cache file extension. Must end with .php
54
     * @throws \Exception if there is a problem loading the cache
55
     */
56
    public function __construct($cacheDirPath = "cache/", $cacheFileName = "defaultcache", $cacheFileExtension = ".cache.php")
57
    {
58
        $this->setCacheFilename($cacheFileName);
59
        $this->setCacheDir($cacheDirPath);
60
        $this->setCacheFileExtension($cacheFileExtension);
61
        $this->setDevMode(false);
62
63
        $this->reloadFromDisc();
64
    }
65
66
    /**
67
     * Loads cache
68
     * @return array array filled with data
69
     * @throws \Exception if there is a problem loading the cache
70
     */
71
    private function loadCacheFile()
72
    {
73
        $filepath = $this->getCacheFilePath();
74
        $file = @file_get_contents($filepath);
75
76
        if (!$file) {
77
            unlink($filepath);
78
            throw new \Exception("Cannot load cache file! ({$this->getCacheFilename()})");
79
        }
80
81
        // Remove the first line which prevents direct access to the file
82
        $file = $this->stripFirstLine($file);
83
        $data = unserialize($file);
84
85
        if ($data === false) {
86
            unlink($filepath);
87
            throw new \Exception("Cannot unserialize cache file, cache file deleted. ({$this->getCacheFilename()})");
88
        }
89
90
        if (!isset($data["hash-sum"])) {
91
            unlink($filepath);
92
            throw new \Exception("No hash found in cache file, cache file deleted");
93
        }
94
95
        $hash = $data["hash-sum"];
96
        unset($data["hash-sum"]);
97
98
        if ($hash !== $this->getStringHash(serialize($data))) {
99
            unlink($filepath);
100
            throw new \Exception("Cache data miss-hashed, cache file deleted");
101
        }
102
103
        return $data;
104
    }
105
106
    /**
107
     * Saves current cacheArray into the cache file
108
     * @return $this
109
     * @throws \Exception if the file cannot be saved
110
     */
111
    private function saveCacheFile()
112
    {
113
        if (!file_exists($this->getCacheDir())) {
114
            @mkdir($this->getCacheDir());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

114
            /** @scrutinizer ignore-unhandled */ @mkdir($this->getCacheDir());

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
115
        }
116
117
        $cache = $this->cacheArray;
118
        $cache["hash-sum"] = $this->getStringHash(serialize($cache));
119
        $data = serialize($cache);
120
        $firstLine = '<?php die("Access denied") ?>' . PHP_EOL;
121
        $success = file_put_contents($this->getCacheFilePath(), $firstLine . $data) !== false;
122
123
        if (!$success) {
124
            throw new \Exception("Cannot save cache");
125
        }
126
127
        return $this;
128
    }
129
130
    /**
131
     * Stores $data under $key for $expiration seconds
132
     * If $key is already used, then current data will be overwritten
133
     * @param $key string key associated with the current data
134
     * @param $data mixed data to store
135
     * @param $expiration int number of seconds before the $key expires
136
     * @param $permanent bool if true, this item will not be automatically cleared after expiring
137
     * @return $this
138
     * @throws \Exception if the file cannot be saved
139
     */
140
    public function store($key, $data, $expiration = 60, $permanent = false)
141
    {
142
        if (!is_string($key)) {
143
            throw new \InvalidArgumentException('$key must be a string, got type "' . get_class($key) . '" instead');
144
        }
145
146
        if ($this->isDevMode()) {
147
            $expiration = 1;
148
        }
149
150
        $storeData = [
151
            "time" => time(),
152
            "expire" => $expiration,
153
            "data" => $data,
154
            "permanent" => $permanent
155
        ];
156
157
        $this->cacheArray[$key] = $storeData;
158
        $this->saveCacheFile();
159
        return $this;
160
    }
161
162
    /**
163
     * Returns data associated with $key
164
     * @param $key string
165
     * @param bool $meta if true, array will be returned containing metadata alongside data itself
166
     * @return mixed|null returns data if $key is valid and not expired, NULL otherwise
167
     * @throws \Exception if the file cannot be saved
168
     */
169
    public function retrieve($key, $meta = false)
170
    {
171
        $this->eraseExpired();
172
173
        if (!isset($this->cacheArray[$key])) {
174
            return null;
175
        }
176
177
        $data = $this->cacheArray[$key];
178
        return $meta ? $data : @$data["data"];
179
    }
180
181
    /**
182
     * Calls $refreshCallback if $key does not exists or is expired.
183
     * Also returns latest data associated with $key.
184
     * This is basically a shortcut, turns this:
185
     * <code>
186
     * if($cache->isExpired(key)) {
187
     *     $cache->store(key, $newdata, 10);
188
     * }
189
     *
190
     * $data = $cache->retrieve(key);
191
     * </code>
192
     *
193
     * to this:
194
     *
195
     * <code>
196
     * $data = $cache->refreshIfExpired(key, function () {
197
     *    return $newdata;
198
     * }, 10);
199
     * </code>
200
     *
201
     * @param $key
202
     * @param $refreshCallback Callback called when data needs to be refreshed. Should return data to be cached.
203
     * @param int $cacheTime Cache time. Defaults to 60
204
     * @param bool $meta If true, returns data with meta. @see retrieve
205
     * @return mixed|null Data currently stored under key
206
     * @throws \Exception if the file cannot be saved
207
     */
208
    public function refreshIfExpired($key, $refreshCallback, $cacheTime = 60, $meta = false)
209
    {
210
        if ($this->isExpired($key)) {
211
            $this->store($key, $refreshCallback(), $cacheTime);
212
        }
213
214
        return $this->retrieve($key, $meta);
215
    }
216
217
    /**
218
     * Erases data associated with $key
219
     * @param $key string
220
     * @return bool true if $key was found and removed, false otherwise
221
     * @throws \Exception if the file cannot be saved
222
     */
223
    public function eraseKey($key)
224
    {
225
        if (!$this->isCached($key, false)) {
226
            return false;
227
        }
228
229
        unset($this->cacheArray[$key]);
230
        $this->saveCacheFile();
231
        return true;
232
    }
233
234
    /**
235
     * Erases expired keys from cache
236
     * @return int number of erased entries
237
     * @throws \Exception if the file cannot be saved
238
     */
239
    public function eraseExpired()
240
    {
241
        $counter = 0;
242
243
        foreach ($this->cacheArray as $key => $value) {
244
            if (!$value["permanent"] && $this->isExpired($key, false)) {
245
                $this->eraseKey($key);
246
                $counter++;
247
            }
248
        }
249
250
        if ($counter > 0) {
251
            $this->saveCacheFile();
252
        }
253
254
        return $counter;
255
    }
256
257
    /**
258
     * Clears the cache
259
     * @throws \Exception if the file cannot be saved
260
     */
261
    public function clearCache()
262
    {
263
        $this->cacheArray = [];
264
        $this->saveCacheFile();
265
    }
266
267
    /**
268
     * Checks if $key has expired
269
     * @param $key
270
     * @param bool $eraseExpired if true, expired data will
271
     * be cleared before running this function
272
     * @return bool
273
     * @throws \Exception if the file cannot be saved
274
     */
275
    public function isExpired($key, $eraseExpired = true)
276
    {
277
        if ($eraseExpired) {
278
            $this->eraseExpired();
279
        }
280
281
        if (!$this->isCached($key, false)) {
282
            return true;
283
        }
284
285
        $item = $this->cacheArray[$key];
286
287
        return $this->isTimestampExpired($item["time"], $item["expire"]);
288
    }
289
290
    /**
291
     * Checks if $key is cached
292
     * @param $key
293
     * @param bool $eraseExpired if true, expired data will
294
     * be cleared before running this function
295
     * @return bool
296
     * @throws \Exception if the file cannot be saved
297
     */
298
    public function isCached($key, $eraseExpired = true)
299
    {
300
        if ($eraseExpired) {
301
            $this->eraseExpired();
302
        }
303
304
        return isset($this->cacheArray[$key]);
305
    }
306
307
    /**
308
     * Checks if the timestamp expired
309
     * @param $timestamp int
310
     * @param $expiration int number of seconds after the timestamp expires
311
     * @return bool true if the timestamp expired, false otherwise
312
     */
313
    private function isTimestampExpired($timestamp, $expiration)
314
    {
315
        $timeDiff = time() - $timestamp;
316
        return $timeDiff >= $expiration;
317
    }
318
319
    /**
320
     * Prints cache file using var_dump, useful for debugging
321
     */
322
    public function debugCache()
323
    {
324
        if (file_exists($this->getCacheFilePath())) {
325
            var_dump(unserialize($this->stripFirstLine(file_get_contents($this->getCacheFilePath()))));
0 ignored issues
show
Security Debugging Code introduced by
var_dump(unserialize($th...>getCacheFilePath())))) looks like debug code. Are you sure you do not want to remove it?
Loading history...
326
        }
327
    }
328
329
    /**
330
     * Reloads cache from disc. Can be used after changing file name, extension or cache dir
331
     * using functions instead of constructor. (This class loads data once, when is created)
332
     * @throws \Exception if there is a problem loading the cache
333
     */
334
    public function reloadFromDisc()
335
    {
336
        // Try to load the cache, otherwise create a empty array
337
        $this->cacheArray = is_readable($this->getCacheFilePath()) ? $this->loadCacheFile() : [];
338
    }
339
340
    /**
341
     * Returns md5 hash of the given string.
342
     * @param $str string String to be hashed
343
     * @return string MD5 hash
344
     * @throws \InvalidArgumentException if $str is not a string
345
     */
346
    private function getStringHash($str)
347
    {
348
        if (!is_string($str)) {
349
            throw new \InvalidArgumentException('$key must be a string, got type "' . get_class($str) . '" instead');
350
        }
351
352
        return md5($str);
353
    }
354
355
    // Utils
356
357
    /**
358
     * Strips the first line from string
359
     * https://stackoverflow.com/a/7740485
360
     * @param $str
361
     * @return bool|string stripped text without the first line or false on failure
362
     */
363
    private function stripFirstLine($str)
364
    {
365
        $position = strpos($str, "\n");
366
367
        if ($position === false) {
368
            return $str;
369
        }
370
371
        return substr($str, $position + 1);
372
    }
373
374
    // Generic setters and getters below
375
376
    /**
377
     * Returns cache directory
378
     * @return string
379
     */
380
    public function getCacheDir()
381
    {
382
        return $this->cacheDir;
383
    }
384
385
    /**
386
     * Sets new cache directory. If you want to read data from new file, consider calling reloadFromDisc.
387
     * @param string $cacheDir new cache directory. Must end with "/"
388
     * @return $this
389
     */
390
    public function setCacheDir($cacheDir)
391
    {
392
        // Add "/" to the end if its not here
393
        if (substr($cacheDir, -1) !== "/") {
394
            $cacheDir .= "/";
395
        }
396
397
        $this->cacheDir = $cacheDir;
398
        return $this;
399
    }
400
401
    /**
402
     * Returns cache file name, hashed with sha1. Used as an actual file name
403
     * The new value is computed when using setCacheFilename method.
404
     * @return string
405
     */
406
    public function getCacheFilenameHashed()
407
    {
408
        return $this->cacheFilenameHashed;
409
    }
410
411
    /**
412
     * Returns cache file name
413
     * @return string
414
     */
415
    public function getCacheFilename()
416
    {
417
        return $this->cacheFilename;
418
    }
419
420
    /**
421
     * Sets new cache file name. If you want to read data from new file, consider calling reloadFromDisc.
422
     * @param string $cacheFilename
423
     * @return $this
424
     * @throws \InvalidArgumentException if $cacheFilename is not a string
425
     */
426
    public function setCacheFilename($cacheFilename)
427
    {
428
        if (!is_string($cacheFilename)) {
0 ignored issues
show
introduced by
The condition is_string($cacheFilename) is always true.
Loading history...
429
            throw new \InvalidArgumentException('$key must be a string, got type "' . get_class($cacheFilename) . '" instead');
430
        }
431
432
        $this->cacheFilename = $cacheFilename;
433
        $this->cacheFilenameHashed = $this->getStringHash($cacheFilename);
434
        return $this;
435
    }
436
437
    /**
438
     * Returns cache file extension
439
     * @return string
440
     */
441
    public function getCacheFileExtension()
442
    {
443
        return $this->cacheFileExtension;
444
    }
445
446
    /**
447
     * Sets new cache file extension. If you want to read data from new file, consider calling reloadFromDisc.
448
     * @param string $cacheFileExtension new cache file extension. Must end with ".php"
449
     * @return $this
450
     */
451
    public function setCacheFileExtension($cacheFileExtension)
452
    {
453
        // Add ".php" to the end if its not here
454
        if (substr($cacheFileExtension, -4) !== ".php") {
455
            $cacheFileExtension .= ".php";
456
        }
457
458
        $this->cacheFileExtension = $cacheFileExtension;
459
        return $this;
460
    }
461
462
    /**
463
     * Combines directory, filename and extension into a path
464
     * @return string
465
     */
466
    public function getCacheFilePath()
467
    {
468
        return $this->getCacheDir() . $this->getCacheFilenameHashed() . $this->getCacheFileExtension();
469
    }
470
471
    /**
472
     * Returns raw cache array
473
     * @return array
474
     */
475
    public function getCacheArray()
476
    {
477
        return $this->cacheArray;
478
    }
479
480
    /**
481
     * Returns true if dev mode is on
482
     * If dev mode is on, cache expire after one second
483
     * @return bool
484
     */
485
    public function isDevMode()
486
    {
487
        return $this->devMode;
488
    }
489
490
    /**
491
     * Sets dev mode on or off
492
     * If dev mode is on, cache expire after one second
493
     * @param $devMode
494
     * @return $this
495
     */
496
    public function setDevMode($devMode)
497
    {
498
        $this->devMode = $devMode;
499
        return $this;
500
    }
501
}
502