Completed
Pull Request — develop (#13)
by
unknown
03:11
created

CacheableEloquent::getCacheKeysFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Rinvex\Cacheable;
6
7
use Closure;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Database\Eloquent\Builder;
10
11
trait CacheableEloquent
12
{
13
    /**
14
     * Indicate if the model cache clear is enabled.
15
     *
16
     * @var bool
17
     */
18
    protected static $cacheClearEnabled = true;
19
20
    /**
21
     * The model cache driver.
22
     *
23
     * @var string
24
     */
25
    protected $cacheDriver;
26
27
    /**
28
     * The model cache lifetime.
29
     *
30
     * @var float|int
31
     */
32
    protected $cacheLifetime = -1;
33
34
    /**
35
     * Register an updated model event with the dispatcher.
36
     *
37
     * @param \Closure|string $callback
38
     *
39
     * @return void
40
     */
41
    abstract public static function updated($callback);
42
43
    /**
44
     * Register a created model event with the dispatcher.
45
     *
46
     * @param \Closure|string $callback
47
     *
48
     * @return void
49
     */
50
    abstract public static function created($callback);
51
52
    /**
53
     * Register a deleted model event with the dispatcher.
54
     *
55
     * @param \Closure|string $callback
56
     *
57
     * @return void
58
     */
59
    abstract public static function deleted($callback);
60
61
    /**
62
     * Boot the cacheable eloquent trait for a model.
63
     *
64
     * @return void
65
     */
66
    public static function bootCacheableEloquent()
67
    {
68
        static::attacheEvents();
69
    }
70
71
    /**
72
     * Store the given cache key for the given model by mimicking cache tags.
73
     *
74
     * @param string $modelName
75
     * @param string $cacheKey
76
     *
77
     * @return void
78
     */
79
    protected static function storeCacheKey(string $modelName, string $cacheKey)
80
    {
81
        $keysFile = self::getCacheKeysFile();
82
        $cacheKeys = static::getCacheKeys($keysFile);
83
84
        if (! isset($cacheKeys[$modelName]) || ! in_array($cacheKey, $cacheKeys[$modelName])) {
85
            $cacheKeys[$modelName][] = $cacheKey;
86
            file_put_contents($keysFile, json_encode($cacheKeys));
87
        }
88
    }
89
90
    /**
91
     * Get cache keys from the given file.
92
     *
93
     * @param string $file
94
     *
95
     * @return array
96
     */
97
    protected static function getCacheKeys($file)
98
    {
99
        if (! file_exists($file)) {
100
            $cacheFolder = storage_path('framework/cache/data/');
101
            if (! file_exists($cacheFolder)) {
102
                mkdir($cacheFolder);
103
            }
104
105
            touch($file);
106
        }
107
108
        return json_decode(file_get_contents($file), true) ?: [];
109
    }
110
111
    /**
112
     * Flush cache keys of the given model by mimicking cache tags.
113
     *
114
     * @param string $modelName
115
     *
116
     * @return array
117
     */
118
    protected static function flushCacheKeys(string $modelName): array
119
    {
120
        $flushedKeys = [];
121
        $keysFile = self::getCacheKeysFile();
122
        $cacheKeys = static::getCacheKeys($keysFile);
123
124
        if (isset($cacheKeys[$modelName])) {
125
            $flushedKeys = $cacheKeys[$modelName];
126
127
            unset($cacheKeys[$modelName]);
128
129
            file_put_contents($keysFile, json_encode($cacheKeys));
130
        }
131
132
        return $flushedKeys;
133
    }
134
135
    /**
136
     * @return string
137
     */
138
    protected static function getCacheKeysFile()
139
    {
140
        return storage_path('framework/cache/data/' . config('app.eav_cacheable_prefix') . '/rinvex.cacheable.json');
141
    }
142
143
    /**
144
     * Set the model cache lifetime.
145
     *
146
     * @param float|int $cacheLifetime
147
     *
148
     * @return $this
149
     */
150
    public function setCacheLifetime($cacheLifetime)
151
    {
152
        $this->cacheLifetime = $cacheLifetime;
153
154
        return $this;
155
    }
156
157
    /**
158
     * Get the model cache lifetime.
159
     *
160
     * @return float|int
161
     */
162
    public function getCacheLifetime()
163
    {
164
        return $this->cacheLifetime;
165
    }
166
167
    /**
168
     * Set the model cache driver.
169
     *
170
     * @param string $cacheDriver
171
     *
172
     * @return $this
173
     */
174
    public function setCacheDriver($cacheDriver)
175
    {
176
        $this->cacheDriver = $cacheDriver;
177
178
        return $this;
179
    }
180
181
    /**
182
     * Get the model cache driver.
183
     *
184
     * @return string
185
     */
186
    public function getCacheDriver()
187
    {
188
        return $this->cacheDriver;
189
    }
190
191
    /**
192
     * Determine if model cache clear is enabled.
193
     *
194
     * @return bool
195
     */
196
    public static function isCacheClearEnabled()
197
    {
198
        return static::$cacheClearEnabled;
199
    }
200
201
    /**
202
     * Forget the model cache.
203
     *
204
     * @return void
205
     */
206
    public static function forgetCache()
207
    {
208
        static::fireCacheFlushEvent('cache.flushing');
209
210
        // Flush cache tags
211
        if (method_exists(app('cache')->getStore(), 'tags')) {
212
            app('cache')->tags(static::class)->flush();
213
        } else {
214
            // Flush cache keys, then forget actual cache
215
            foreach (static::flushCacheKeys(static::class) as $cacheKey) {
216
                app('cache')->forget($cacheKey);
217
            }
218
        }
219
220
        static::fireCacheFlushEvent('cache.flushed', false);
221
    }
222
223
    /**
224
     * Fire the given event for the model.
225
     *
226
     * @param string $event
227
     * @param bool   $halt
228
     *
229
     * @return mixed
230
     */
231
    protected static function fireCacheFlushEvent($event, $halt = true)
232
    {
233
        if (! isset(static::$dispatcher)) {
234
            return true;
235
        }
236
237
        // We will append the names of the class to the event to distinguish it from
238
        // other model events that are fired, allowing us to listen on each model
239
        // event set individually instead of catching event for all the models.
240
        $event = "eloquent.{$event}: ".static::class;
241
242
        $method = $halt ? 'until' : 'fire';
243
244
        return static::$dispatcher->$method($event, static::class);
245
    }
246
247
    /**
248
     * Reset cached model to its defaults.
249
     *
250
     * @return $this
251
     */
252
    public function resetCacheConfig()
253
    {
254
        $this->cacheDriver = null;
255
        $this->cacheLifetime = null;
256
257
        return $this;
258
    }
259
260
    /**
261
     * Generate unique cache key.
262
     *
263
     * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
264
     * @param array                                                                    $columns
265
     *
266
     * @return string
267
     */
268
    protected function generateCacheKey($builder, array $columns)
269
    {
270
        $query = $builder instanceof Builder ? $builder->getQuery() : $builder;
271
        $vars = [
272
            'aggregate' => $query->aggregate,
273
            'columns' => $query->columns,
274
            'distinct' => $query->distinct,
275
            'from' => $query->from,
276
            'joins' => $query->joins,
277
            'wheres' => $query->wheres,
278
            'groups' => $query->groups,
279
            'havings' => $query->havings,
280
            'orders' => $query->orders,
281
            'limit' => $query->limit,
282
            'offset' => $query->offset,
283
            'unions' => $query->unions,
284
            'unionLimit' => $query->unionLimit,
285
            'unionOffset' => $query->unionOffset,
286
            'unionOrders' => $query->unionOrders,
287
            'lock' => $query->lock,
288
        ];
289
290
        return md5(json_encode([
291
            $vars,
292
            $columns,
293
            static::class,
294
            $this->getCacheDriver(),
295
            $this->getCacheLifetime(),
296
            $builder instanceof Builder ? $builder->getEagerLoads() : null,
297
            $builder->getBindings(),
0 ignored issues
show
Bug introduced by
The method getBindings does only exist in Illuminate\Database\Query\Builder, but not in Illuminate\Database\Eloquent\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
298
            $builder->toSql(),
0 ignored issues
show
Bug introduced by
The method toSql does only exist in Illuminate\Database\Query\Builder, but not in Illuminate\Database\Eloquent\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
299
        ]));
300
    }
301
302
    /**
303
     * Cache given callback.
304
     *
305
     * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
306
     * @param array                                                                    $columns
307
     * @param \Closure                                                                 $closure
308
     *
309
     * @return mixed
310
     */
311
    public function cacheQuery($builder, array $columns, Closure $closure)
312
    {
313
        $modelName = static::class;
314
        $lifetime = $this->getCacheLifetime();
315
        $cacheKey = $this->generateCacheKey($builder, $columns);
316
317
        // Switch cache driver on runtime
318
        if ($driver = $this->getCacheDriver()) {
319
            app('cache')->setDefaultDriver($driver);
320
        }
321
322
        // We need cache tags, check if default driver supports it
323
        if (method_exists(app('cache')->getStore(), 'tags')) {
324
            $result = $lifetime === -1 ? app('cache')->tags($modelName)->rememberForever($cacheKey, $closure) : app('cache')->tags($modelName)->remember($cacheKey, $lifetime, $closure);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 185 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
325
326
            return $result;
327
        }
328
329
        $result = $lifetime === -1 ? app('cache')->rememberForever($cacheKey, $closure) : app('cache')->remember($cacheKey, $lifetime, $closure);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 145 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
330
331
        // Default cache driver doesn't support tags, let's do it manually
332
        static::storeCacheKey($modelName, $cacheKey);
333
334
        // We're done, let's clean up!
335
        $this->resetCacheConfig();
336
337
        return $result;
338
    }
339
340
    /**
341
     * Create a new Eloquent query builder for the model.
342
     *
343
     * @param \Illuminate\Database\Query\Builder $query
344
     *
345
     * @return \Illuminate\Database\Eloquent\Builder|static
346
     */
347
    public function newEloquentBuilder($query)
348
    {
349
        return new EloquentBuilder($query);
350
    }
351
352
    /**
353
     * Attach events to the model.
354
     *
355
     * @return void
356
     */
357
    protected static function attacheEvents()
358
    {
359
        static::updated(function (Model $cachedModel) {
360
            if ($cachedModel::isCacheClearEnabled()) {
361
                $cachedModel::forgetCache();
362
            }
363
        });
364
365
        static::created(function (Model $cachedModel) {
366
            if ($cachedModel::isCacheClearEnabled()) {
367
                $cachedModel::forgetCache();
368
            }
369
        });
370
371
        static::deleted(function (Model $cachedModel) {
372
            if ($cachedModel::isCacheClearEnabled()) {
373
                $cachedModel::forgetCache();
374
            }
375
        });
376
    }
377
}
378