Completed
Push — master ( 94f5e8...a88b9e )
by Lars
01:57 queued 11s
created

Cache::getCacheIsReady()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
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 107
    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 107
        $this->isAdminSession = $isAdminSession;
139
140 107
        $this->useCheckForDev = $useCheckForDev;
141 107
        $this->useCheckForAdminSession = $useCheckForAdminSession;
142 107
        $this->useCheckForServerIpIsClientIp = $useCheckForServerIpIsClientIp;
143
144 107
        $this->disableCacheGetParameter = $disableCacheGetParameter;
145
146
        // First check if the cache is active at all.
147 107
        $this->isActive = $cacheEnabled;
148
        if (
149 107
            $this->isActive
150
            &&
151 107
            $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 107
        if ($this->isActive) {
158 107
            $this->setPrefix($this->getTheDefaultPrefix());
159
160 107
            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 107
            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 107
            $serializer !== null
182
            &&
183 107
            $adapter !== null
184
        ) {
185 107
            $this->setCacheIsReady(true);
186
187 107
            $this->adapter = $adapter;
188
189 107
            $this->serializer = $serializer;
190
191 107
            $this->serializer->setUnserializeOptions($this->unserialize_options);
192
        }
193 107
    }
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 90
    protected function calculateStoreKey(string $rawKey): string
272
    {
273 90
        $str = $this->getPrefix() . $rawKey;
274
275 90
        if ($this->adapter instanceof AdapterFileAbstract) {
276 48
            $str = $this->cleanStoreKey($str);
277
        }
278
279 90
        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 59
    protected function checkForStaticCache(string $storeKey): bool
323
    {
324 59
        return !empty(self::$STATIC_CACHE)
325
               &&
326 59
               \array_key_exists($storeKey, self::$STATIC_CACHE)
327
               &&
328 59
               \array_key_exists($storeKey, self::$STATIC_CACHE_EXPIRE)
329
               &&
330 59
               \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 48
    protected function cleanStoreKey(string $str): string
341
    {
342 48
        return \md5($str);
343
    }
344
345
    /**
346
     * Check if cached-item exists.
347
     *
348
     * @param string $key
349
     *
350
     * @return bool
351
     */
352 23
    public function existsItem(string $key): bool
353
    {
354 23
        if (!$this->adapter instanceof iAdapter) {
355
            return false;
356
        }
357
358 23
        $storeKey = $this->calculateStoreKey($key);
359
360
        // check static-cache
361 23
        if ($this->checkForStaticCache($storeKey)) {
362
            return true;
363
        }
364
365 23
        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 49
    public function getItem(string $key, int $forceStaticCacheHitCounter = 0)
377
    {
378 49
        if (!$this->adapter instanceof iAdapter) {
379
            return null;
380
        }
381
382 49
        $storeKey = $this->calculateStoreKey($key);
383
384
        // check if we already using static-cache
385 49
        $useStaticCache = true;
386 49
        if ($this->adapter instanceof AdapterArray) {
387 12
            $useStaticCache = false;
388
        }
389
390 49
        if (!isset(self::$STATIC_CACHE_COUNTER[$storeKey])) {
391 29
            self::$STATIC_CACHE_COUNTER[$storeKey] = 0;
392
        }
393
394
        // get from static-cache
395
        if (
396 49
            $useStaticCache
397
            &&
398 49
            $this->checkForStaticCache($storeKey)
399
        ) {
400 8
            return self::$STATIC_CACHE[$storeKey];
401
        }
402
403 47
        $serialized = $this->adapter->get($storeKey);
404 47
        $value = $serialized && $this->serializer ? $this->serializer->unserialize($serialized) : null;
405
406 47
        self::$STATIC_CACHE_COUNTER[$storeKey]++;
407
408
        // save into static-cache if needed
409
        if (
410 47
            $useStaticCache
411
            &&
412
            (
413
                (
414 35
                    $forceStaticCacheHitCounter !== 0
415
                    &&
416 1
                    self::$STATIC_CACHE_COUNTER[$storeKey] >= $forceStaticCacheHitCounter
417
                )
418
                ||
419
                (
420 35
                    $this->staticCacheHitCounter !== 0
421
                    &&
422 47
                    self::$STATIC_CACHE_COUNTER[$storeKey] >= $this->staticCacheHitCounter
423
                )
424
            )
425
        ) {
426 10
            self::$STATIC_CACHE[$storeKey] = $value;
427
        }
428
429 47
        return $value;
430
    }
431
432
    /**
433
     * Remove all cached-items.
434
     *
435
     * @return bool
436
     */
437 4
    public function removeAll(): bool
438
    {
439 4
        if (!$this->adapter instanceof iAdapter) {
440
            return false;
441
        }
442
443
        // remove static-cache
444 4
        if (!empty(self::$STATIC_CACHE)) {
445 4
            self::$STATIC_CACHE = [];
446 4
            self::$STATIC_CACHE_COUNTER = [];
447 4
            self::$STATIC_CACHE_EXPIRE = [];
448
        }
449
450 4
        return $this->adapter->removeAll();
451
    }
452
453
    /**
454
     * Remove a cached-item.
455
     *
456
     * @param string $key
457
     *
458
     * @return bool
459
     */
460 9
    public function removeItem(string $key): bool
461
    {
462 9
        if (!$this->adapter instanceof iAdapter) {
463
            return false;
464
        }
465
466 9
        $storeKey = $this->calculateStoreKey($key);
467
468
        // remove static-cache
469
        if (
470 9
            !empty(self::$STATIC_CACHE)
471
            &&
472 9
            \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 9
        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 50
    public function setItem(string $key, $value, $ttl = 0): bool
496
    {
497
        if (
498 50
            !$this->adapter instanceof iAdapter
499
            ||
500 50
            !$this->serializer instanceof iSerializer
501
        ) {
502
            return false;
503
        }
504
505 50
        $storeKey = $this->calculateStoreKey($key);
506 50
        $serialized = $this->serializer->serialize($value);
507
508
        // update static-cache, if it's exists
509 50
        if (\array_key_exists($storeKey, self::$STATIC_CACHE)) {
510 6
            self::$STATIC_CACHE[$storeKey] = $value;
511
        }
512
513 50
        if ($ttl) {
514 29
            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 29
            self::$STATIC_CACHE_EXPIRE[$storeKey] = ($ttl ? (int) $ttl + \time() : 0);
522
523 29
            return $this->adapter->setExpired($storeKey, $serialized, $ttl);
524
        }
525
526 21
        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 23
    public function setItemToDate(string $key, $value, \DateTimeInterface $date): bool
542
    {
543 23
        $ttl = $date->getTimestamp() - \time();
544
545 23
        if ($ttl <= 0) {
546 4
            throw new InvalidArgumentException('Date in the past.');
547
        }
548
549 19
        return $this->setItem($key, $value, $ttl);
550
    }
551
552
    /**
553
     * Get the "isReady" state.
554
     *
555
     * @return bool
556
     */
557 7
    public function getCacheIsReady(): bool
558
    {
559 7
        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 90
    public function getPrefix(): string
600
    {
601 90
        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 107
    protected function getTheDefaultPrefix(): string
618
    {
619 107
        return ($_SERVER['SERVER_NAME'] ?? '') . '_' .
620 107
               ($_SERVER['THEME'] ?? '') . '_' .
621 107
               ($_SERVER['STAGE'] ?? '') . '_' .
622 107
               ($_SESSION['language'] ?? '') . '_' .
623 107
               ($_SESSION['language_extra'] ?? '') . '_' .
624 107
               \PHP_VERSION_ID . '_' .
625 107
               ($this->serializer ? $this->serializer->getName() : '');
626
    }
627
628
    /**
629
     * Get the current adapter class-name.
630
     *
631
     * @return string
632
     */
633 3
    public function getUsedAdapterClassName(): string
634
    {
635 3
        if ($this->adapter) {
636
            /** @noinspection GetClassUsageInspection */
637 3
            return \get_class($this->adapter);
638
        }
639
640
        return '';
641
    }
642
643
    /**
644
     * Get the current serializer class-name.
645
     *
646
     * @return string
647
     */
648 3
    public function getUsedSerializerClassName(): string
649
    {
650 3
        if ($this->serializer) {
651
            /** @noinspection GetClassUsageInspection */
652 3
            return \get_class($this->serializer);
653
        }
654
655
        return '';
656
    }
657
658
    /**
659
     * check if the current use is a admin || dev || server == client
660
     *
661
     * @return bool
662
     */
663
    public function isCacheActiveForTheCurrentUser(): bool
664
    {
665
        $active = true;
666
667
        // test the cache, with this GET-parameter
668
        if ($this->disableCacheGetParameter) {
669
            $testCache = isset($_GET[$this->disableCacheGetParameter]) ? (int) $_GET[$this->disableCacheGetParameter] : 0;
670
        } else {
671
            $testCache = 0;
672
        }
673
674
        if ($testCache !== 1) {
675
            if (
676
                // admin session is active
677
                (
678
                    $this->useCheckForAdminSession
679
                    &&
680
                    $this->isAdminSession
681
                )
682
                ||
683
                // server == client
684
                (
685
                    $this->useCheckForServerIpIsClientIp
686
                    &&
687
                    isset($_SERVER['SERVER_ADDR'])
688
                    &&
689
                    $_SERVER['SERVER_ADDR'] === $this->getClientIp()
690
                )
691
                ||
692
                // user is a dev
693
                (
694
                    $this->useCheckForDev
695
                    &&
696
                    $this->checkForDev()
697
                )
698
            ) {
699
                $active = false;
700
            }
701
        }
702
703
        return $active;
704
    }
705
706
    /**
707
     * enable / disable the cache
708
     *
709
     * @param bool $isActive
710
     */
711
    public function setActive(bool $isActive)
712
    {
713
        $this->isActive = $isActive;
714
    }
715
716
    /**
717
     * Set "isReady" state.
718
     *
719
     * @param bool $isReady
720
     */
721 107
    protected function setCacheIsReady(bool $isReady)
722
    {
723 107
        $this->isReady = $isReady;
724 107
    }
725
726
    /**
727
     * !!! Set the prefix. !!!
728
     *
729
     * WARNING: Do not use if you don't know what you do. Because this will overwrite the default prefix.
730
     *
731
     * @param string $prefix
732
     */
733 107
    public function setPrefix(string $prefix)
734
    {
735 107
        $this->prefix = $prefix;
736 107
    }
737
738
    /**
739
     * Set the static-hit-counter: Who often do we hit the cache, before we use static cache?
740
     *
741
     * @param int $staticCacheHitCounter
742
     */
743
    public function setStaticCacheHitCounter(int $staticCacheHitCounter)
744
    {
745
        $this->staticCacheHitCounter = $staticCacheHitCounter;
746
    }
747
}
748