Completed
Push — master ( 2e23cf...730f80 )
by Lars
03:00
created

Cache::getTheDefaultPrefix()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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