Completed
Push — master ( 0e0464...f097a8 )
by Lars
01:40
created

Cache::getItem()   C

Complexity

Conditions 15
Paths 45

Size

Total Lines 59

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 15.13

Importance

Changes 0
Metric Value
dl 0
loc 59
ccs 22
cts 24
cp 0.9167
rs 5.9166
c 0
b 0
f 0
cc 15
nc 45
nop 2
crap 15.13

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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