Completed
Push — master ( dfda1d...2f160c )
by Lars
04:17 queued 18s
created

Cache::existsItem()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.2098

Importance

Changes 0
Metric Value
dl 0
loc 15
ccs 5
cts 7
cp 0.7143
rs 9.7666
c 0
b 0
f 0
cc 3
nc 3
nop 1
crap 3.2098
1
<?php
2
3
declare(strict_types=1);
4
5
namespace voku\cache;
6
7
use voku\cache\Exception\InvalidArgumentException;
8
9
/**
10
 * Cache: global-cache class
11
 *
12
 * can use different cache-adapter:
13
 * - Redis
14
 * - Memcache / Memcached
15
 * - APC / APCu
16
 * - Xcache
17
 * - Array
18
 * - File / OpCache
19
 */
20
class Cache implements iCache
21
{
22
    /**
23
     * @var array
24
     */
25
    protected static $STATIC_CACHE = [];
26
27
    /**
28
     * @var array
29
     */
30
    protected static $STATIC_CACHE_EXPIRE = [];
31
32
    /**
33
     * @var array
34
     */
35
    protected static $STATIC_CACHE_COUNTER = [];
36
37
    /**
38
     * @var iAdapter|null
39
     */
40
    protected $adapter;
41
42
    /**
43
     * @var iSerializer|null
44
     */
45
    protected $serializer;
46
47
    /**
48
     * @var array
49
     */
50
    protected $unserialize_options = ['allowed_classes' => true];
51
52
    /**
53
     * @var string
54
     */
55
    protected $prefix = '';
56
57
    /**
58
     * @var bool
59
     */
60
    protected $isReady = false;
61
62
    /**
63
     * @var bool
64
     */
65
    protected $isActive = true;
66
67
    /**
68
     * @var bool
69
     */
70
    protected $useCheckForDev;
71
72
    /**
73
     * @var bool
74
     */
75
    protected $useCheckForAdminSession;
76
77
    /**
78
     * @var bool
79
     */
80
    protected $useCheckForServerIpIsClientIp;
81
82
    /**
83
     * @var string
84
     */
85
    protected $disableCacheGetParameter;
86
87
    /**
88
     * @var bool
89
     */
90
    protected $isAdminSession;
91
92
    /**
93
     * @var int
94
     */
95
    protected $staticCacheHitCounter = 10;
96
97
    /**
98
     * __construct
99
     *
100
     * @param iAdapter|null           $adapter
101
     * @param iSerializer|null        $serializer
102
     * @param bool                    $checkForUsage                              <p>check for admin-session && check
103
     *                                                                            for server-ip == client-ip
104
     *                                                                            && check for dev</p>
105
     * @param bool                    $cacheEnabled                               <p>false === disable the cache (use
106
     *                                                                            it
107
     *                                                                            e.g. for global settings)</p>
108
     * @param bool                    $isAdminSession                             <p>true === disable cache for this
109
     *                                                                            user
110
     *                                                                            (use it e.g. for admin user settings)
111
     * @param bool                    $useCheckForAdminSession                    <p>use $isAdminSession flag or
112
     *                                                                            not</p>
113
     * @param bool                    $useCheckForDev                             <p>use checkForDev() or not</p>
114
     * @param bool                    $useCheckForServerIpIsClientIp              <p>use check for server-ip ==
115
     *                                                                            client-ip or not</p>
116
     * @param string                  $disableCacheGetParameter                   <p>set the _GET parameter for
117
     *                                                                            disabling the cache, disable this
118
     *                                                                            check via empty string</p>
119
     * @param CacheAdapterAutoManager $cacheAdapterManagerForAutoConnect          <p>Overwrite some Adapters for the
120
     *                                                                            auto-connect-function.</p>
121
     * @param bool                    $cacheAdapterManagerForAutoConnectOverwrite <p>true === Use only Adapters from
122
     *                                                                            your
123
     *                                                                            "CacheAdapterManager".</p>
124
     */
125 89
    public function __construct(
126
        iAdapter $adapter = null,
127
        iSerializer $serializer = null,
128
        bool $checkForUsage = true,
129
        bool $cacheEnabled = true,
130
        bool $isAdminSession = false,
131
        bool $useCheckForDev = true,
132
        bool $useCheckForAdminSession = true,
133
        bool $useCheckForServerIpIsClientIp = true,
134
        string $disableCacheGetParameter = 'testWithoutCache',
135
        CacheAdapterAutoManager $cacheAdapterManagerForAutoConnect = null,
136
        bool $cacheAdapterManagerForAutoConnectOverwrite = false
137
    ) {
138 89
        $this->isAdminSession = $isAdminSession;
139
140 89
        $this->useCheckForDev = $useCheckForDev;
141 89
        $this->useCheckForAdminSession = $useCheckForAdminSession;
142 89
        $this->useCheckForServerIpIsClientIp = $useCheckForServerIpIsClientIp;
143
144 89
        $this->disableCacheGetParameter = $disableCacheGetParameter;
145
146
        // First check if the cache is active at all.
147 89
        $this->isActive = $cacheEnabled;
148
        if (
149 89
            $this->isActive
150
            &&
151 89
            $checkForUsage
152
        ) {
153
            $this->setActive($this->isCacheActiveForTheCurrentUser());
154
        }
155
156
        // If the cache is active, then try to auto-connect to the best possible cache-system.
157 89
        if ($this->isActive) {
158 89
            $this->setPrefix($this->getTheDefaultPrefix());
159
160 89
            if ($adapter === null) {
161 27
                $adapter = $this->autoConnectToAvailableCacheSystem($cacheAdapterManagerForAutoConnect, $cacheAdapterManagerForAutoConnectOverwrite);
162
            }
163
164
            // INFO: Memcache(d) has his own "serializer", so don't use it twice
165 89
            if (!\is_object($serializer) && $serializer === null) {
166
                if (
167 27
                    $adapter instanceof AdapterMemcached
168
                    ||
169 27
                    $adapter instanceof AdapterMemcache
170
                ) {
171
                    $serializer = new SerializerNo();
172
                } else {
173
                    // set default serializer
174 27
                    $serializer = new SerializerIgbinary();
175
                }
176
            }
177
        }
178
179
        // Final checks ...
180
        if (
181 89
            $serializer !== null
182
            &&
183 89
            $adapter !== null
184
        ) {
185 89
            $this->setCacheIsReady(true);
186
187 89
            $this->adapter = $adapter;
188
189 89
            $this->serializer = $serializer;
190
191 89
            $this->serializer->setUnserializeOptions($this->unserialize_options);
192
        }
193 89
    }
194
195
    /**
196
     * @param array $array
197
     */
198
    public function setUnserializeOptions(array $array = [])
199
    {
200
        $this->unserialize_options = $array;
201
    }
202
203
    /**
204
     * Auto-connect to the available cache-system on the server.
205
     *
206
     * @param CacheAdapterAutoManager $cacheAdapterManagerForAutoConnect          <p>Overwrite some Adapters for the
207
     *                                                                            auto-connect-function.</p>
208
     * @param bool                    $cacheAdapterManagerForAutoConnectOverwrite <p>true === Use only Adapters from
209
     *                                                                            your
210
     *                                                                            "CacheAdapterManager".</p>
211
     *
212
     * @return iAdapter
213
     */
214 27
    protected function autoConnectToAvailableCacheSystem(
215
        CacheAdapterAutoManager $cacheAdapterManagerForAutoConnect = null,
216
        bool $cacheAdapterManagerForAutoConnectOverwrite = false
217
    ): iAdapter {
218 27
        static $AUTO_ADAPTER_STATIC_CACHE = null;
219
220
        if (
221 27
            \is_object($AUTO_ADAPTER_STATIC_CACHE)
222
            &&
223 27
            $AUTO_ADAPTER_STATIC_CACHE instanceof iAdapter
224
        ) {
225 26
            return $AUTO_ADAPTER_STATIC_CACHE;
226
        }
227
228
        // init
229 1
        $adapter = null;
230
231 1
        $cacheAdapterManagerDefault = CacheAdapterAutoManager::getDefaultsForAutoInit();
232
233 1
        if ($cacheAdapterManagerForAutoConnect !== null) {
234 1
            if ($cacheAdapterManagerForAutoConnectOverwrite) {
235 1
                $cacheAdapterManagerDefault = $cacheAdapterManagerForAutoConnect;
236
            } else {
237
                /** @noinspection PhpUnhandledExceptionInspection */
238
                $cacheAdapterManagerDefault->merge($cacheAdapterManagerForAutoConnect);
239
            }
240
        }
241
242 1
        foreach ($cacheAdapterManagerDefault->getAdapters() as $adapterTmp => $callableFunctionTmp) {
243
244
            /** @var iAdapter $adapterTest */
245 1
            if ($callableFunctionTmp !== null) {
246 1
                $adapterTest = new $adapterTmp($callableFunctionTmp);
247
            } else {
248
                $adapterTest = new $adapterTmp();
249
            }
250
251 1
            if ($adapterTest->installed()) {
252 1
                $adapter = $adapterTest;
253
254 1
                break;
255
            }
256
        }
257
258
        // save to static cache
259 1
        $AUTO_ADAPTER_STATIC_CACHE = $adapter;
260
261 1
        return $adapter;
262
    }
263
264
    /**
265
     * Calculate store-key (prefix + $rawKey).
266
     *
267
     * @param string $rawKey
268
     *
269
     * @return string
270
     */
271 73
    protected function calculateStoreKey(string $rawKey): string
272
    {
273 73
        $str = $this->getPrefix() . $rawKey;
274
275 73
        if ($this->adapter instanceof AdapterFileAbstract) {
276 44
            $str = $this->cleanStoreKey($str);
277
        }
278
279 73
        return $str;
280
    }
281
282
    /**
283
     * Check for local developer.
284
     *
285
     * @return bool
286
     */
287
    protected function checkForDev(): bool
288
    {
289
        $return = false;
290
291
        if (\function_exists('checkForDev')) {
292
            $return = checkForDev();
293
        } else {
294
295
            // for testing with dev-address
296
            $noDev = isset($_GET['noDev']) ? (int) $_GET['noDev'] : 0;
297
            $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'NO_REMOTE_ADDR';
298
299
            if (
300
                $noDev !== 1
301
                &&
302
                (
303
                    $remoteAddr === '127.0.0.1'
304
                    ||
305
                    $remoteAddr === '::1'
306
                    ||
307
                    \PHP_SAPI === 'cli'
308
                )
309
            ) {
310
                $return = true;
311
            }
312
        }
313
314
        return $return;
315
    }
316
317
    /**
318
     * @param string $storeKey
319
     *
320
     * @return bool
321
     */
322 44
    protected function checkForStaticCache(string $storeKey): bool
323
    {
324 44
        return !empty(self::$STATIC_CACHE)
325
               &&
326 44
               \array_key_exists($storeKey, self::$STATIC_CACHE)
327
               &&
328 44
               \array_key_exists($storeKey, self::$STATIC_CACHE_EXPIRE)
329
               &&
330 44
               \time() <= self::$STATIC_CACHE_EXPIRE[$storeKey];
331
    }
332
333
    /**
334
     * Clean store-key (required e.g. for the "File"-Adapter).
335
     *
336
     * @param string $str
337
     *
338
     * @return string
339
     */
340 44
    protected function cleanStoreKey(string $str): string
341
    {
342 44
        return \md5($str);
343
    }
344
345
    /**
346
     * Check if cached-item exists.
347
     *
348
     * @param string $key
349
     *
350
     * @return bool
351
     */
352 12
    public function existsItem(string $key): bool
353
    {
354 12
        if (!$this->adapter instanceof iAdapter) {
355
            return false;
356
        }
357
358 12
        $storeKey = $this->calculateStoreKey($key);
359
360
        // check static-cache
361 12
        if ($this->checkForStaticCache($storeKey)) {
362
            return true;
363
        }
364
365 12
        return $this->adapter->exists($storeKey);
366
    }
367
368
    /**
369
     * Get cached-item by key.
370
     *
371
     * @param string $key
372
     * @param int    $forceStaticCacheHitCounter
373
     *
374
     * @return mixed
375
     */
376 38
    public function getItem(string $key, int $forceStaticCacheHitCounter = 0)
377
    {
378 38
        if (!$this->adapter instanceof iAdapter) {
379
            return null;
380
        }
381
382 38
        $storeKey = $this->calculateStoreKey($key);
383
384
        // check if we already using static-cache
385 38
        $useStaticCache = true;
386 38
        if ($this->adapter instanceof AdapterArray) {
387 6
            $useStaticCache = false;
388
        }
389
390 38
        if (!isset(self::$STATIC_CACHE_COUNTER[$storeKey])) {
391 16
            self::$STATIC_CACHE_COUNTER[$storeKey] = 0;
392
        }
393
394
        // get from static-cache
395
        if (
396 38
            $useStaticCache
397
            &&
398 38
            $this->checkForStaticCache($storeKey)
399
        ) {
400 2
            return self::$STATIC_CACHE[$storeKey];
401
        }
402
403 38
        $serialized = $this->adapter->get($storeKey);
404 28
        $value = $serialized && $this->serializer ? $this->serializer->unserialize($serialized) : null;
405
406 28
        self::$STATIC_CACHE_COUNTER[$storeKey]++;
407
408
        // save into static-cache if needed
409
        if (
410 28
            $useStaticCache
411
            &&
412
            (
413
                (
414 22
                    $forceStaticCacheHitCounter !== 0
415
                    &&
416
                    self::$STATIC_CACHE_COUNTER[$storeKey] >= $forceStaticCacheHitCounter
417
                )
418
                ||
419
                (
420 22
                    $this->staticCacheHitCounter !== 0
421
                    &&
422 28
                    self::$STATIC_CACHE_COUNTER[$storeKey] >= $this->staticCacheHitCounter
423
                )
424
            )
425
        ) {
426 3
            self::$STATIC_CACHE[$storeKey] = $value;
427
        }
428
429 28
        return $value;
430
    }
431
432
    /**
433
     * Remove all cached-items.
434
     *
435
     * @return bool
436
     */
437 1
    public function removeAll(): bool
438
    {
439 1
        if (!$this->adapter instanceof iAdapter) {
440
            return false;
441
        }
442
443
        // remove static-cache
444 1
        if (!empty(self::$STATIC_CACHE)) {
445 1
            self::$STATIC_CACHE = [];
446 1
            self::$STATIC_CACHE_COUNTER = [];
447 1
            self::$STATIC_CACHE_EXPIRE = [];
448
        }
449
450 1
        return $this->adapter->removeAll();
451
    }
452
453
    /**
454
     * Remove a cached-item.
455
     *
456
     * @param string $key
457
     *
458
     * @return bool
459
     */
460 6
    public function removeItem(string $key): bool
461
    {
462 6
        if (!$this->adapter instanceof iAdapter) {
463
            return false;
464
        }
465
466 6
        $storeKey = $this->calculateStoreKey($key);
467
468
        // remove static-cache
469
        if (
470 6
            !empty(self::$STATIC_CACHE)
471
            &&
472 6
            \array_key_exists($storeKey, self::$STATIC_CACHE)
473
        ) {
474
            unset(
475
                self::$STATIC_CACHE[$storeKey],
476
                self::$STATIC_CACHE_COUNTER[$storeKey],
477
                self::$STATIC_CACHE_EXPIRE[$storeKey]
478
            );
479
        }
480
481 6
        return $this->adapter->remove($storeKey);
482
    }
483
484
    /**
485
     * Set cache-item by key => value + ttl.
486
     *
487
     * @param string                 $key
488
     * @param mixed                  $value
489
     * @param \DateInterval|int|null $ttl
490
     *
491
     * @throws \InvalidArgumentException
492
     *
493
     * @return bool
494
     */
495 41
    public function setItem(string $key, $value, $ttl = 0): bool
496
    {
497
        if (
498 41
            !$this->adapter instanceof iAdapter
499
            ||
500 41
            !$this->serializer instanceof iSerializer
501
        ) {
502
            return false;
503
        }
504
505 41
        $storeKey = $this->calculateStoreKey($key);
506 41
        $serialized = $this->serializer->serialize($value);
507
508
        // update static-cache, if it's exists
509 41
        if (\array_key_exists($storeKey, self::$STATIC_CACHE)) {
510 1
            self::$STATIC_CACHE[$storeKey] = $value;
511
        }
512
513 41
        if ($ttl) {
514 25
            if ($ttl instanceof \DateInterval) {
515
                // Converting to a TTL in seconds
516
                /** @noinspection PhpUnhandledExceptionInspection */
517 1
                $ttl = (new \DateTimeImmutable('now'))->add($ttl)->getTimestamp() - \time();
518
            }
519
520
            // always cache the TTL time, maybe we need this later ...
521 25
            self::$STATIC_CACHE_EXPIRE[$storeKey] = ($ttl ? (int) $ttl + \time() : 0);
522
523 25
            return $this->adapter->setExpired($storeKey, $serialized, $ttl);
524
        }
525
526 16
        return $this->adapter->set($storeKey, $serialized);
527
    }
528
529
    /**
530
     * Set cache-item by key => value + date.
531
     *
532
     * @param string             $key
533
     * @param mixed              $value
534
     * @param \DateTimeInterface $date <p>If the date is in the past, we will remove the existing cache-item.</p>
535
     *
536
     * @throws InvalidArgumentException
537
     *                                   <p>If the $date is in the past.</p>
538
     *
539
     * @return bool
540
     */
541 19
    public function setItemToDate(string $key, $value, \DateTimeInterface $date): bool
542
    {
543 19
        $ttl = $date->getTimestamp() - \time();
544
545 19
        if ($ttl <= 0) {
546 4
            throw new InvalidArgumentException('Date in the past.');
547
        }
548
549 15
        return $this->setItem($key, $value, $ttl);
550
    }
551
552
    /**
553
     * Get the "isReady" state.
554
     *
555
     * @return bool
556
     */
557 6
    public function getCacheIsReady(): bool
558
    {
559 6
        return $this->isReady;
560
    }
561
562
    /**
563
     * returns the IP address of the client
564
     *
565
     * @param bool $trust_proxy_headers     <p>
566
     *                                      Whether or not to trust the
567
     *                                      proxy headers HTTP_CLIENT_IP
568
     *                                      and HTTP_X_FORWARDED_FOR. ONLY
569
     *                                      use if your $_SERVER is behind a
570
     *                                      proxy that sets these values
571
     *                                      </p>
572
     *
573
     * @return string
574
     */
575
    protected function getClientIp(bool $trust_proxy_headers = false): string
576
    {
577
        $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'NO_REMOTE_ADDR';
578
579
        if ($trust_proxy_headers) {
580
            return $remoteAddr;
581
        }
582
583
        if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
584
            $ip = $_SERVER['HTTP_CLIENT_IP'];
585
        } elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
586
            $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
587
        } else {
588
            $ip = $remoteAddr;
589
        }
590
591
        return $ip;
592
    }
593
594
    /**
595
     * Get the prefix.
596
     *
597
     * @return string
598
     */
599 73
    public function getPrefix(): string
600
    {
601 73
        return $this->prefix;
602
    }
603
604
    /**
605
     * Get the current value, when the static cache is used.
606
     *
607
     * @return int
608
     */
609
    public function getStaticCacheHitCounter(): int
610
    {
611
        return $this->staticCacheHitCounter;
612
    }
613
614
    /**
615
     * Set the default-prefix via "SERVER"-var + "SESSION"-language.
616
     */
617 89
    protected function getTheDefaultPrefix(): string
618
    {
619 89
        return ($_SERVER['SERVER_NAME'] ?? '') . '_' .
620 89
               ($_SERVER['THEME'] ?? '') . '_' .
621 89
               ($_SERVER['STAGE'] ?? '') . '_' .
622 89
               ($_SESSION['language'] ?? '') . '_' .
623 89
               ($_SESSION['language_extra'] ?? '');
624
    }
625
626
    /**
627
     * Get the current adapter class-name.
628
     *
629
     * @return string
630
     */
631 3
    public function getUsedAdapterClassName(): string
632
    {
633 3
        if ($this->adapter) {
634
            /** @noinspection GetClassUsageInspection */
635 3
            return \get_class($this->adapter);
636
        }
637
638
        return '';
639
    }
640
641
    /**
642
     * Get the current serializer class-name.
643
     *
644
     * @return string
645
     */
646 3
    public function getUsedSerializerClassName(): string
647
    {
648 3
        if ($this->serializer) {
649
            /** @noinspection GetClassUsageInspection */
650 3
            return \get_class($this->serializer);
651
        }
652
653
        return '';
654
    }
655
656
    /**
657
     * check if the current use is a admin || dev || server == client
658
     *
659
     * @return bool
660
     */
661
    public function isCacheActiveForTheCurrentUser(): bool
662
    {
663
        $active = true;
664
665
        // test the cache, with this GET-parameter
666
        if ($this->disableCacheGetParameter) {
667
            $testCache = isset($_GET[$this->disableCacheGetParameter]) ? (int) $_GET[$this->disableCacheGetParameter] : 0;
668
        } else {
669
            $testCache = 0;
670
        }
671
672
        if ($testCache !== 1) {
673
            if (
674
                // admin session is active
675
                (
676
                    $this->useCheckForAdminSession
677
                    &&
678
                    $this->isAdminSession
679
                )
680
                ||
681
                // server == client
682
                (
683
                    $this->useCheckForServerIpIsClientIp
684
                    &&
685
                    isset($_SERVER['SERVER_ADDR'])
686
                    &&
687
                    $_SERVER['SERVER_ADDR'] === $this->getClientIp()
688
                )
689
                ||
690
                // user is a dev
691
                (
692
                    $this->useCheckForDev
693
                    &&
694
                    $this->checkForDev()
695
                )
696
            ) {
697
                $active = false;
698
            }
699
        }
700
701
        return $active;
702
    }
703
704
    /**
705
     * enable / disable the cache
706
     *
707
     * @param bool $isActive
708
     */
709
    public function setActive(bool $isActive)
710
    {
711
        $this->isActive = $isActive;
712
    }
713
714
    /**
715
     * Set "isReady" state.
716
     *
717
     * @param bool $isReady
718
     */
719 89
    protected function setCacheIsReady(bool $isReady)
720
    {
721 89
        $this->isReady = $isReady;
722 89
    }
723
724
    /**
725
     * !!! Set the prefix. !!!
726
     *
727
     * WARNING: Do not use if you don't know what you do. Because this will overwrite the default prefix.
728
     *
729
     * @param string $prefix
730
     */
731 89
    public function setPrefix(string $prefix)
732
    {
733 89
        $this->prefix = $prefix;
734 89
    }
735
736
    /**
737
     * Set the static-hit-counter: Who often do we hit the cache, before we use static cache?
738
     *
739
     * @param int $staticCacheHitCounter
740
     */
741
    public function setStaticCacheHitCounter(int $staticCacheHitCounter)
742
    {
743
        $this->staticCacheHitCounter = $staticCacheHitCounter;
744
    }
745
}
746