CacheTrait   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 178
Duplicated Lines 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 21
eloc 54
c 4
b 0
f 0
dl 0
loc 178
rs 10

12 Methods

Rating   Name   Duplication   Size   Complexity  
A getXFetched() 0 11 3
A rnd() 0 5 1
A getDeltaKey() 0 3 1
A getStdCached() 0 14 3
A getCached() 0 7 3
A getSource() 0 8 2
A xFetch() 0 9 1
A recompute() 0 14 2
A set() 0 7 2
A getKeyHash() 0 4 1
A unser() 0 6 1
A ser() 0 5 1
1
<?php
2
3
namespace SoliDry\Extension;
4
5
use Illuminate\Http\Request;
6
use Illuminate\Support\Facades\Redis;
7
use SoliDry\Helpers\ConfigOptions;
8
use SoliDry\Helpers\Metrics;
9
use SoliDry\Helpers\SqlOptions;
10
use SoliDry\Types\PhpInterface;
11
use SoliDry\Types\RedisInterface;
12
13
/**
14
 * Trait CacheTrait
15
 *
16
 * @package SoliDry\Extension
17
 *
18
 * @property ConfigOptions configOptions
19
 */
20
trait CacheTrait
21
{
22
23
    /**
24
     * @param string $key
25
     * @param \League\Fractal\Resource\Collection | \League\Fractal\Resource\Item | int $val
26
     * @param int $ttl
27
     * @return mixed
28
     */
29
    private function set(string $key, $val, int $ttl = 0)
30
    {
31
        if ($ttl > 0) {
32
            return Redis::set($key, $this->ser($val), 'EX', $ttl);
0 ignored issues
show
Bug introduced by
It seems like $val can also be of type integer; however, parameter $data of SoliDry\Extension\CacheTrait::ser() does only seem to accept League\Fractal\Resource\...e\Fractal\Resource\Item, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

32
            return Redis::set($key, $this->ser(/** @scrutinizer ignore-type */ $val), 'EX', $ttl);
Loading history...
33
        }
34
35
        return Redis::set($key, $this->ser($val));
36
    }
37
38
    /**
39
     * @param string $key
40
     * @return mixed
41
     */
42
    private function getSource(string $key)
43
    {
44
        $data = Redis::get($key);
45
        if ($data === NULL) {
46
            return NULL;
47
        }
48
49
        return $this->unser($data);
50
    }
51
52
    /**
53
     * @param Request $request
54
     * @param SqlOptions $sqlOptions
55
     * @return mixed
56
     */
57
    private function getCached(Request $request, SqlOptions $sqlOptions)
58
    {
59
        if ($this->configOptions->isXFetch() && $this->configOptions->getCacheTtl() > 0) {
60
            return $this->getXFetched($request, $sqlOptions);
61
        }
62
63
        return $this->getStdCached($request, $sqlOptions);
64
    }
65
66
    /**
67
     * @param Request $request
68
     * @param SqlOptions $sqlOptions
69
     * @return mixed
70
     */
71
    private function getStdCached(Request $request, SqlOptions $sqlOptions)
72
    {
73
        $hashKey = $this->getKeyHash($request);
74
        $items   = $this->getSource($hashKey);
75
        if ($items === null) {
76
            if ($sqlOptions->getId() > 0) {
77
                $items = $this->getEntity($sqlOptions->getId(), $sqlOptions->getData());
0 ignored issues
show
Bug introduced by
It seems like getEntity() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

77
                /** @scrutinizer ignore-call */ 
78
                $items = $this->getEntity($sqlOptions->getId(), $sqlOptions->getData());
Loading history...
78
            } else {
79
                $items = $this->getEntities($sqlOptions);
0 ignored issues
show
Bug introduced by
It seems like getEntities() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

79
                /** @scrutinizer ignore-call */ 
80
                $items = $this->getEntities($sqlOptions);
Loading history...
80
            }
81
            $this->set($hashKey, $items);
82
        }
83
84
        return $items;
85
    }
86
87
    /**
88
     * @param Request $request
89
     * @param SqlOptions $sqlOptions
90
     * @return mixed
91
     */
92
    private function getXFetched(Request $request, SqlOptions $sqlOptions)
93
    {
94
        $hashKey   = $this->getKeyHash($request);
95
        $delta     = Redis::get($this->getDeltaKey($hashKey));
96
        $ttl       = $this->configOptions->getCacheTtl();
97
        $recompute = $this->xFetch((float)$delta, Redis::ttl($hashKey));
98
        if ($delta === null || $recompute === true) {
99
            return $this->recompute($sqlOptions, $hashKey, $ttl);
100
        }
101
102
        return $this->getSource($hashKey);
103
    }
104
105
    /**
106
     * @param string $hashKey
107
     * @return string
108
     */
109
    private function getDeltaKey(string $hashKey) : string
110
    {
111
        return $hashKey . PhpInterface::COLON . RedisInterface::REDIS_DELTA;
112
    }
113
114
    /**
115
     * @param SqlOptions $sqlOptions
116
     * @param string $hashKey
117
     * @param int $ttl
118
     * @return mixed
119
     */
120
    private function recompute(SqlOptions $sqlOptions, string $hashKey, int $ttl)
121
    {
122
        $start = Metrics::millitime();
123
        if ($sqlOptions->getId() > 0) {
124
            $items = $this->getEntity($sqlOptions->getId(), $sqlOptions->getData());
125
        } else {
126
            $items = $this->getEntities($sqlOptions);
127
        }
128
129
        $delta = (Metrics::millitime() - $start) / 1000;
130
        $this->set($hashKey, $items, $ttl);
131
132
        Redis::set($this->getDeltaKey($hashKey), $delta);
133
        return $items;
134
    }
135
136
    /**
137
     * @param Request $request
138
     * @return string
139
     */
140
    private function getKeyHash(Request $request) : string
141
    {
142
        $qStr = $request->getQueryString() ?? '';
143
        return $this->configOptions->getCalledMethod() . PhpInterface::COLON . md5($request->getRequestUri() . $qStr);
144
    }
145
146
    /**
147
     * @param \League\Fractal\Resource\Collection | \League\Fractal\Resource\Item $data
148
     * @return string
149
     */
150
    protected function ser($data) : string
151
    {
152
        return str_replace(
153
            PhpInterface::DOUBLE_QUOTES, PhpInterface::DOUBLE_QUOTES_ESC,
154
            serialize($data)
155
        );
156
    }
157
158
    /**
159
     * @param string $data
160
     * @return \League\Fractal\Resource\Collection | \League\Fractal\Resource\Item
161
     */
162
    protected function unser(string $data)
163
    {
164
        return unserialize(
165
            str_replace(
166
                PhpInterface::DOUBLE_QUOTES_ESC, PhpInterface::DOUBLE_QUOTES,
167
                $data), ['allowed_classes' => true]
168
        );
169
    }
170
171
    /**
172
     * @return float
173
     * @throws \Exception
174
     */
175
    public static function rnd() : float
176
    {
177
        $max = mt_getrandmax();
178
179
        return random_int(1, $max) / $max;
180
    }
181
182
    /**
183
     * @param float $delta Amount of time it takes to recompute the value in secs ex.: 0.3 - 300ms
184
     * @param int $ttl Time to live in cache
185
     * @return bool
186
     * @internal double $beta   > 1.0 schedule a recompute earlier, < 1.0 schedule a recompute later (0.5-2.0 best
187
     *           practice)
188
     */
189
    private function xFetch(float $delta, int $ttl) : bool
190
    {
191
        $beta   = $this->configOptions->getCacheBeta();
192
        $now    = time();
193
        $rnd    = static::rnd();
194
        $logrnd = log($rnd);
195
        $xfetch = $delta * $beta * $logrnd;
196
197
        return ($now - $xfetch) >= ($now + $ttl);
198
    }
199
}