Completed
Push — develop ( dcedcb...6942c0 )
by Abdelrahman
01:01
created

CacheableEloquent::storeCacheKey()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
nc 2
cc 3
eloc 6
nop 2
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 = storage_path('framework/cache/data/rinvex.cacheable.json');
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
            file_put_contents($file, null);
101
        }
102
103
        return json_decode(file_get_contents($file), true) ?: [];
104
    }
105
106
    /**
107
     * Flush cache keys of the given model by mimicking cache tags.
108
     *
109
     * @param string $modelName
110
     *
111
     * @return array
112
     */
113
    protected static function flushCacheKeys(string $modelName): array
114
    {
115
        $flushedKeys = [];
116
        $keysFile = storage_path('framework/cache/data/rinvex.cacheable.json');
117
        $cacheKeys = static::getCacheKeys($keysFile);
118
119
        if (isset($cacheKeys[$modelName])) {
120
            $flushedKeys = $cacheKeys[$modelName];
121
122
            unset($cacheKeys[$modelName]);
123
124
            file_put_contents($keysFile, json_encode($cacheKeys));
125
        }
126
127
        return $flushedKeys;
128
    }
129
130
    /**
131
     * Set the model cache lifetime.
132
     *
133
     * @param float|int $cacheLifetime
134
     *
135
     * @return $this
136
     */
137
    public function setCacheLifetime($cacheLifetime)
138
    {
139
        $this->cacheLifetime = $cacheLifetime;
140
141
        return $this;
142
    }
143
144
    /**
145
     * Get the model cache lifetime.
146
     *
147
     * @return float|int
148
     */
149
    public function getCacheLifetime()
150
    {
151
        return $this->cacheLifetime;
152
    }
153
154
    /**
155
     * Set the model cache driver.
156
     *
157
     * @param string $cacheDriver
158
     *
159
     * @return $this
160
     */
161
    public function setCacheDriver($cacheDriver)
162
    {
163
        $this->cacheDriver = $cacheDriver;
164
165
        return $this;
166
    }
167
168
    /**
169
     * Get the model cache driver.
170
     *
171
     * @return string
172
     */
173
    public function getCacheDriver()
174
    {
175
        return $this->cacheDriver;
176
    }
177
178
    /**
179
     * Determine if model cache clear is enabled.
180
     *
181
     * @return bool
182
     */
183
    public static function isCacheClearEnabled()
184
    {
185
        return static::$cacheClearEnabled;
186
    }
187
188
    /**
189
     * Forget the model cache.
190
     *
191
     * @return void
192
     */
193
    public static function forgetCache()
194
    {
195
        static::fireCacheFlushEvent('cache.flushing');
196
197
        // Flush cache tags
198
        if (method_exists(app('cache')->getStore(), 'tags')) {
199
            app('cache')->tags(static::class)->flush();
200
        } else {
201
            // Flush cache keys, then forget actual cache
202
            foreach (static::flushCacheKeys(static::class) as $cacheKey) {
203
                app('cache')->forget($cacheKey);
204
            }
205
        }
206
207
        static::fireCacheFlushEvent('cache.flushed', false);
208
    }
209
210
    /**
211
     * Fire the given event for the model.
212
     *
213
     * @param string $event
214
     * @param bool   $halt
215
     *
216
     * @return mixed
217
     */
218
    protected static function fireCacheFlushEvent($event, $halt = true)
219
    {
220
        if (! isset(static::$dispatcher)) {
221
            return true;
222
        }
223
224
        // We will append the names of the class to the event to distinguish it from
225
        // other model events that are fired, allowing us to listen on each model
226
        // event set individually instead of catching event for all the models.
227
        $event = "eloquent.{$event}: ".static::class;
228
229
        $method = $halt ? 'until' : 'fire';
230
231
        return static::$dispatcher->$method($event, static::class);
232
    }
233
234
    /**
235
     * Reset cached model to its defaults.
236
     *
237
     * @return $this
238
     */
239
    public function resetCacheConfig()
240
    {
241
        $this->cacheDriver = null;
242
        $this->cacheLifetime = null;
243
244
        return $this;
245
    }
246
247
    /**
248
     * Generate unique cache key.
249
     *
250
     * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
251
     * @param array                                                                    $columns
252
     *
253
     * @return string
254
     */
255
    protected function generateCacheKey($builder, array $columns)
256
    {
257
        $query = $builder instanceof Builder ? $builder->getQuery() : $builder;
258
        $vars = [
259
            'aggregate' => $query->aggregate,
260
            'columns' => $query->columns,
261
            'distinct' => $query->distinct,
262
            'from' => $query->from,
263
            'joins' => $query->joins,
264
            'wheres' => $query->wheres,
265
            'groups' => $query->groups,
266
            'havings' => $query->havings,
267
            'orders' => $query->orders,
268
            'limit' => $query->limit,
269
            'offset' => $query->offset,
270
            'unions' => $query->unions,
271
            'unionLimit' => $query->unionLimit,
272
            'unionOffset' => $query->unionOffset,
273
            'unionOrders' => $query->unionOrders,
274
            'lock' => $query->lock,
275
        ];
276
277
        return md5(json_encode([
278
            $vars,
279
            $columns,
280
            static::class,
281
            $this->getCacheDriver(),
282
            $this->getCacheLifetime(),
283
            $builder instanceof Builder ? $builder->getEagerLoads() : null,
284
            $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...
285
            $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...
286
        ]));
287
    }
288
289
    /**
290
     * Cache given callback.
291
     *
292
     * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
293
     * @param array                                                                    $columns
294
     * @param \Closure                                                                 $closure
295
     *
296
     * @return mixed
297
     */
298
    public function cacheQuery($builder, array $columns, Closure $closure)
299
    {
300
        $modelName = static::class;
301
        $lifetime = $this->getCacheLifetime();
302
        $cacheKey = $this->generateCacheKey($builder, $columns);
303
304
        // Switch cache driver on runtime
305
        if ($driver = $this->getCacheDriver()) {
306
            app('cache')->setDefaultDriver($driver);
307
        }
308
309
        // We need cache tags, check if default driver supports it
310
        if (method_exists(app('cache')->getStore(), 'tags')) {
311
            $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...
312
313
            return $result;
314
        }
315
316
        $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...
317
318
        // Default cache driver doesn't support tags, let's do it manually
319
        static::storeCacheKey($modelName, $cacheKey);
320
321
        // We're done, let's clean up!
322
        $this->resetCacheConfig();
323
324
        return $result;
325
    }
326
327
    /**
328
     * Create a new Eloquent query builder for the model.
329
     *
330
     * @param \Illuminate\Database\Query\Builder $query
331
     *
332
     * @return \Illuminate\Database\Eloquent\Builder|static
333
     */
334
    public function newEloquentBuilder($query)
335
    {
336
        return new EloquentBuilder($query);
337
    }
338
339
    /**
340
     * Attach events to the model.
341
     *
342
     * @return void
343
     */
344
    protected static function attacheEvents()
345
    {
346
        static::updated(function (Model $cachedModel) {
347
            if ($cachedModel::isCacheClearEnabled()) {
348
                $cachedModel::forgetCache();
349
            }
350
        });
351
352
        static::created(function (Model $cachedModel) {
353
            if ($cachedModel::isCacheClearEnabled()) {
354
                $cachedModel::forgetCache();
355
            }
356
        });
357
358
        static::deleted(function (Model $cachedModel) {
359
            if ($cachedModel::isCacheClearEnabled()) {
360
                $cachedModel::forgetCache();
361
            }
362
        });
363
    }
364
}
365