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

Cache::__construct()   C

Complexity

Conditions 12
Paths 36

Size

Total Lines 72

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 12.1769

Importance

Changes 0
Metric Value
dl 0
loc 72
ccs 25
cts 28
cp 0.8929
rs 6.1842
c 0
b 0
f 0
cc 12
nc 36
nop 11
crap 12.1769

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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