1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Rinvex\Repository\Traits; |
6
|
|
|
|
7
|
|
|
use Closure; |
8
|
|
|
|
9
|
|
|
trait Cacheable |
10
|
|
|
{ |
11
|
|
|
/** |
12
|
|
|
* The repository cache lifetime. |
13
|
|
|
* |
14
|
|
|
* @var int |
15
|
|
|
*/ |
16
|
|
|
protected $cacheLifetime; |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* The repository cache driver. |
20
|
|
|
* |
21
|
|
|
* @var string |
22
|
|
|
*/ |
23
|
|
|
protected $cacheDriver; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Indicate if the repository cache clear is enabled. |
27
|
|
|
* |
28
|
|
|
* @var bool |
29
|
|
|
*/ |
30
|
|
|
protected $cacheClearEnabled = true; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Generate unique query hash. |
34
|
|
|
* |
35
|
|
|
* @param $args |
36
|
|
|
* |
37
|
|
|
* @return string |
38
|
|
|
*/ |
39
|
|
|
protected function generateCacheHash($args): string |
40
|
|
|
{ |
41
|
|
|
return md5(json_encode($args + [ |
42
|
|
|
$this->getRepositoryId(), |
|
|
|
|
43
|
|
|
$this->getModel(), |
|
|
|
|
44
|
|
|
$this->getCacheDriver(), |
45
|
|
|
$this->getCacheLifetime(), |
46
|
|
|
$this->relations, |
|
|
|
|
47
|
|
|
$this->where, |
|
|
|
|
48
|
|
|
$this->whereIn, |
|
|
|
|
49
|
|
|
$this->whereNotIn, |
|
|
|
|
50
|
|
|
$this->offset, |
|
|
|
|
51
|
|
|
$this->limit, |
|
|
|
|
52
|
|
|
$this->orderBy, |
|
|
|
|
53
|
|
|
])); |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Store cache keys by mimicking cache tags. |
58
|
|
|
* |
59
|
|
|
* @param string $class |
60
|
|
|
* @param string $method |
61
|
|
|
* @param string $hash |
62
|
|
|
* |
63
|
|
|
* @return void |
64
|
|
|
*/ |
65
|
|
|
protected function storeCacheKeys($class, $method, $hash): void |
66
|
|
|
{ |
67
|
|
|
$keysFile = $this->getContainer('config')->get('rinvex.repository.cache.keys_file'); |
|
|
|
|
68
|
|
|
$cacheKeys = $this->getCacheKeys($keysFile); |
69
|
|
|
|
70
|
|
|
if (! isset($cacheKeys[$class]) || ! in_array($method.'.'.$hash, $cacheKeys[$class])) { |
71
|
|
|
$cacheKeys[$class][] = $method.'.'.$hash; |
72
|
|
|
file_put_contents($keysFile, json_encode($cacheKeys)); |
73
|
|
|
} |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Get cache keys. |
78
|
|
|
* |
79
|
|
|
* @param string $file |
80
|
|
|
* |
81
|
|
|
* @return array |
82
|
|
|
*/ |
83
|
|
|
protected function getCacheKeys($file): array |
84
|
|
|
{ |
85
|
|
|
if (! file_exists($file)) { |
86
|
|
|
file_put_contents($file, null); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
return json_decode(file_get_contents($file), true) ?: []; |
90
|
|
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* Flush cache keys by mimicking cache tags. |
94
|
|
|
* |
95
|
|
|
* @return array |
96
|
|
|
*/ |
97
|
|
|
protected function flushCacheKeys(): array |
98
|
|
|
{ |
99
|
|
|
$flushedKeys = []; |
100
|
|
|
$calledClass = static::class; |
101
|
|
|
$config = $this->getContainer('config')->get('rinvex.repository.cache'); |
|
|
|
|
102
|
|
|
$cacheKeys = $this->getCacheKeys($config['keys_file']); |
103
|
|
|
|
104
|
|
|
if (isset($cacheKeys[$calledClass]) && is_array($cacheKeys[$calledClass])) { |
105
|
|
|
foreach ($cacheKeys[$calledClass] as $cacheKey) { |
106
|
|
|
$flushedKeys[] = $calledClass.'@'.$cacheKey; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
unset($cacheKeys[$calledClass]); |
110
|
|
|
file_put_contents($config['keys_file'], json_encode($cacheKeys)); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
return $flushedKeys; |
114
|
|
|
} |
115
|
|
|
|
116
|
|
|
/** |
117
|
|
|
* {@inheritdoc} |
118
|
|
|
*/ |
119
|
|
|
public function setCacheLifetime($cacheLifetime) |
120
|
|
|
{ |
121
|
|
|
$this->cacheLifetime = $cacheLifetime; |
122
|
|
|
|
123
|
|
|
return $this; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* {@inheritdoc} |
128
|
|
|
*/ |
129
|
|
|
public function getCacheLifetime(): int |
130
|
|
|
{ |
131
|
|
|
// Return value even if it's zero "0" (which means cache is disabled) |
132
|
|
|
return $this->cacheLifetime ?? $this->getContainer('config')->get('rinvex.repository.cache.lifetime'); |
|
|
|
|
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* {@inheritdoc} |
137
|
|
|
*/ |
138
|
|
|
public function setCacheDriver($cacheDriver) |
139
|
|
|
{ |
140
|
|
|
$this->cacheDriver = $cacheDriver; |
141
|
|
|
|
142
|
|
|
return $this; |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* {@inheritdoc} |
147
|
|
|
*/ |
148
|
|
|
public function getCacheDriver(): ?string |
|
|
|
|
149
|
|
|
{ |
150
|
|
|
return $this->cacheDriver; |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* {@inheritdoc} |
155
|
|
|
*/ |
156
|
|
|
public function enableCacheClear($status = true) |
157
|
|
|
{ |
158
|
|
|
$this->cacheClearEnabled = $status; |
159
|
|
|
|
160
|
|
|
return $this; |
161
|
|
|
} |
162
|
|
|
|
163
|
|
|
/** |
164
|
|
|
* {@inheritdoc} |
165
|
|
|
*/ |
166
|
|
|
public function isCacheClearEnabled(): bool |
167
|
|
|
{ |
168
|
|
|
return $this->cacheClearEnabled; |
169
|
|
|
} |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* {@inheritdoc} |
173
|
|
|
*/ |
174
|
|
|
public function forgetCache() |
175
|
|
|
{ |
176
|
|
|
if ($this->getCacheLifetime()) { |
177
|
|
|
// Flush cache tags |
178
|
|
|
if (method_exists($this->getContainer('cache')->getStore(), 'tags')) { |
|
|
|
|
179
|
|
|
$this->getContainer('cache')->tags($this->getRepositoryId())->flush(); |
|
|
|
|
180
|
|
|
} else { |
181
|
|
|
// Flush cache keys, then forget actual cache |
182
|
|
|
foreach ($this->flushCacheKeys() as $cacheKey) { |
183
|
|
|
$this->getContainer('cache')->forget($cacheKey); |
|
|
|
|
184
|
|
|
} |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$this->getContainer('events')->dispatch($this->getRepositoryId().'.entity.cache.flushed', [$this]); |
|
|
|
|
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
return $this; |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Cache given callback. |
195
|
|
|
* |
196
|
|
|
* @param string $class |
197
|
|
|
* @param string $method |
198
|
|
|
* @param array $args |
199
|
|
|
* @param \Closure $closure |
200
|
|
|
* |
201
|
|
|
* @return mixed |
202
|
|
|
*/ |
203
|
|
|
protected function cacheCallback($class, $method, $args, Closure $closure) |
204
|
|
|
{ |
205
|
|
|
$repositoryId = $this->getRepositoryId(); |
|
|
|
|
206
|
|
|
$lifetime = $this->getCacheLifetime(); |
207
|
|
|
$hash = $this->generateCacheHash($args); |
208
|
|
|
$cacheKey = $class.'@'.$method.'.'.$hash; |
209
|
|
|
|
210
|
|
|
// Switch cache driver on runtime |
211
|
|
|
if ($driver = $this->getCacheDriver()) { |
212
|
|
|
$this->getContainer('cache')->setDefaultDriver($driver); |
|
|
|
|
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
// We need cache tags, check if default driver supports it |
216
|
|
|
if (method_exists($this->getContainer('cache')->getStore(), 'tags')) { |
|
|
|
|
217
|
|
|
$result = $lifetime === -1 |
218
|
|
|
? $this->getContainer('cache')->tags($repositoryId)->rememberForever($cacheKey, $closure) |
|
|
|
|
219
|
|
|
: $this->getContainer('cache')->tags($repositoryId)->remember($cacheKey, $lifetime, $closure); |
|
|
|
|
220
|
|
|
|
221
|
|
|
// We're done, let's clean up! |
222
|
|
|
$this->resetRepository(); |
|
|
|
|
223
|
|
|
|
224
|
|
|
return $result; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
// Default cache driver doesn't support tags, let's do it manually |
228
|
|
|
$this->storeCacheKeys($class, $method, $hash); |
229
|
|
|
|
230
|
|
|
$result = $lifetime === -1 |
231
|
|
|
? $this->getContainer('cache')->rememberForever($cacheKey, $closure) |
|
|
|
|
232
|
|
|
: $this->getContainer('cache')->remember($cacheKey, $lifetime, $closure); |
|
|
|
|
233
|
|
|
|
234
|
|
|
// We're done, let's clean up! |
235
|
|
|
$this->resetCachedRepository(); |
236
|
|
|
|
237
|
|
|
return $result; |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* Reset cached repository to its defaults. |
242
|
|
|
* |
243
|
|
|
* @return $this |
244
|
|
|
*/ |
245
|
|
|
protected function resetCachedRepository() |
246
|
|
|
{ |
247
|
|
|
$this->resetRepository(); |
|
|
|
|
248
|
|
|
|
249
|
|
|
$this->cacheLifetime = null; |
250
|
|
|
$this->cacheDriver = null; |
251
|
|
|
|
252
|
|
|
return $this; |
253
|
|
|
} |
254
|
|
|
} |
255
|
|
|
|
This check looks for methods that are used by a trait but not required by it.
To illustrate, let’s look at the following code example
The trait
Idable
provides a methodequalsId
that in turn relies on the methodgetId()
. If this method does not exist on a class mixing in this trait, the method will fail.Adding the
getId()
as an abstract method to the trait will make sure it is available.