Completed
Push — master ( 67b201...64a3e6 )
by Lars
02:31 queued 11s
created

Cache::autoConnectToAvailableCacheSystem()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 50

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

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