CacheableEloquent   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 319
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 3

Importance

Changes 0
Metric Value
wmc 36
lcom 3
cbo 3
dl 0
loc 319
rs 9.52
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
updated() 0 1 ?
created() 0 1 ?
deleted() 0 1 ?
A bootCacheableEloquent() 0 14 4
A storeCacheKey() 0 10 3
A getCacheKeys() 0 10 4
A flushCacheKeys() 0 16 2
A setCacheLifetime() 0 6 1
A getCacheLifetime() 0 4 1
A setCacheDriver() 0 6 1
A getCacheDriver() 0 4 1
A isCacheClearEnabled() 0 4 1
A forgetCache() 0 16 3
A fireCacheFlushEvent() 0 15 3
A resetCacheConfig() 0 7 3
A generateCacheKey() 0 33 3
A cacheQuery() 0 28 5
A newEloquentBuilder() 0 4 1
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
     * Register an updated model event with the dispatcher.
15
     *
16
     * @param \Closure|string $callback
17
     *
18
     * @return void
19
     */
20
    abstract public static function updated($callback);
21
22
    /**
23
     * Register a created model event with the dispatcher.
24
     *
25
     * @param \Closure|string $callback
26
     *
27
     * @return void
28
     */
29
    abstract public static function created($callback);
30
31
    /**
32
     * Register a deleted model event with the dispatcher.
33
     *
34
     * @param \Closure|string $callback
35
     *
36
     * @return void
37
     */
38
    abstract public static function deleted($callback);
39
40
    /**
41
     * Boot the cacheable eloquent trait for a model.
42
     *
43
     * @return void
44
     */
45
    public static function bootCacheableEloquent(): void
46
    {
47
        static::updated(function (Model $cachedModel) {
48
            ! $cachedModel->isCacheClearEnabled() || $cachedModel::forgetCache();
49
        });
50
51
        static::created(function (Model $cachedModel) {
52
            ! $cachedModel->isCacheClearEnabled() || $cachedModel::forgetCache();
53
        });
54
55
        static::deleted(function (Model $cachedModel) {
56
            ! $cachedModel->isCacheClearEnabled() || $cachedModel::forgetCache();
57
        });
58
    }
59
60
    /**
61
     * Store the given cache key for the given model by mimicking cache tags.
62
     *
63
     * @param string $modelName
64
     * @param string $cacheKey
65
     *
66
     * @return void
67
     */
68
    protected static function storeCacheKey(string $modelName, string $cacheKey): void
69
    {
70
        $keysFile = storage_path('framework/cache/data/rinvex.cacheable.json');
71
        $cacheKeys = static::getCacheKeys($keysFile);
72
73
        if (! isset($cacheKeys[$modelName]) || ! in_array($cacheKey, $cacheKeys[$modelName])) {
74
            $cacheKeys[$modelName][] = $cacheKey;
75
            file_put_contents($keysFile, json_encode($cacheKeys));
76
        }
77
    }
78
79
    /**
80
     * Get cache keys from the given file.
81
     *
82
     * @param string $file
83
     *
84
     * @return array
85
     */
86
    protected static function getCacheKeys($file): array
87
    {
88
        if (! file_exists($file)) {
89
            $dir = dirname($file);
90
            is_dir($dir) || mkdir($dir);
91
            file_put_contents($file, null);
92
        }
93
94
        return json_decode(file_get_contents($file), true) ?: [];
95
    }
96
97
    /**
98
     * Flush cache keys of the given model by mimicking cache tags.
99
     *
100
     * @param string $modelName
101
     *
102
     * @return array
103
     */
104
    protected static function flushCacheKeys(string $modelName): array
105
    {
106
        $flushedKeys = [];
107
        $keysFile = storage_path('framework/cache/data/rinvex.cacheable.json');
108
        $cacheKeys = static::getCacheKeys($keysFile);
109
110
        if (isset($cacheKeys[$modelName])) {
111
            $flushedKeys = $cacheKeys[$modelName];
112
113
            unset($cacheKeys[$modelName]);
114
115
            file_put_contents($keysFile, json_encode($cacheKeys));
116
        }
117
118
        return $flushedKeys;
119
    }
120
121
    /**
122
     * Set the model cache lifetime.
123
     *
124
     * @param int $cacheLifetime
125
     *
126
     * @return $this
127
     */
128
    public function setCacheLifetime(int $cacheLifetime)
129
    {
130
        $this->cacheLifetime = $cacheLifetime;
0 ignored issues
show
Bug introduced by
The property cacheLifetime does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
131
132
        return $this;
133
    }
134
135
    /**
136
     * Get the model cache lifetime.
137
     *
138
     * @return int
139
     */
140
    public function getCacheLifetime(): int
141
    {
142
        return $this->cacheLifetime ?? -1;
143
    }
144
145
    /**
146
     * Set the model cache driver.
147
     *
148
     * @param string $cacheDriver
149
     *
150
     * @return $this
151
     */
152
    public function setCacheDriver($cacheDriver)
153
    {
154
        $this->cacheDriver = $cacheDriver;
0 ignored issues
show
Bug introduced by
The property cacheDriver does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
155
156
        return $this;
157
    }
158
159
    /**
160
     * Get the model cache driver.
161
     *
162
     * @return string|null
163
     */
164
    public function getCacheDriver(): ?string
165
    {
166
        return $this->cacheDriver ?? null;
167
    }
168
169
    /**
170
     * Determine if model cache clear is enabled.
171
     *
172
     * @return bool
173
     */
174
    public function isCacheClearEnabled(): bool
175
    {
176
        return $this->cacheClearEnabled ?? true;
0 ignored issues
show
Bug introduced by
The property cacheClearEnabled does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
177
    }
178
179
    /**
180
     * Forget the model cache.
181
     *
182
     * @return void
183
     */
184
    public static function forgetCache()
185
    {
186
        static::fireCacheFlushEvent('cache.flushing');
187
188
        // Flush cache tags
189
        if (method_exists(app('cache')->getStore(), 'tags')) {
190
            app('cache')->tags(static::class)->flush();
191
        } else {
192
            // Flush cache keys, then forget actual cache
193
            foreach (static::flushCacheKeys(static::class) as $cacheKey) {
194
                app('cache')->forget($cacheKey);
195
            }
196
        }
197
198
        static::fireCacheFlushEvent('cache.flushed', false);
199
    }
200
201
    /**
202
     * Fire the given event for the model.
203
     *
204
     * @param string $event
205
     * @param bool   $halt
206
     *
207
     * @return mixed
208
     */
209
    protected static function fireCacheFlushEvent($event, $halt = true)
210
    {
211
        if (! isset(static::$dispatcher)) {
212
            return true;
213
        }
214
215
        // We will append the names of the class to the event to distinguish it from
216
        // other model events that are fired, allowing us to listen on each model
217
        // event set individually instead of catching event for all the models.
218
        $event = "eloquent.{$event}: ".static::class;
219
220
        $method = $halt ? 'until' : 'dispatch';
221
222
        return static::$dispatcher->{$method}($event, static::class);
223
    }
224
225
    /**
226
     * Reset cached model to its defaults.
227
     *
228
     * @return $this
229
     */
230
    public function resetCacheConfig()
231
    {
232
        ! $this->cacheDriver || $this->cacheDriver = null;
233
        ! $this->cacheLifetime || $this->cacheLifetime = -1;
234
235
        return $this;
236
    }
237
238
    /**
239
     * Generate unique cache key.
240
     *
241
     * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
242
     * @param array                                                                    $columns
243
     *
244
     * @return string
245
     */
246
    protected function generateCacheKey($builder, array $columns): string
247
    {
248
        $query = $builder instanceof Builder ? $builder->getQuery() : $builder;
249
        $vars = [
250
            'aggregate' => $query->aggregate,
251
            'columns' => $query->columns,
252
            'distinct' => $query->distinct,
253
            'from' => $query->from,
254
            'joins' => $query->joins,
255
            'wheres' => $query->wheres,
256
            'groups' => $query->groups,
257
            'havings' => $query->havings,
258
            'orders' => $query->orders,
259
            'limit' => $query->limit,
260
            'offset' => $query->offset,
261
            'unions' => $query->unions,
262
            'unionLimit' => $query->unionLimit,
263
            'unionOffset' => $query->unionOffset,
264
            'unionOrders' => $query->unionOrders,
265
            'lock' => $query->lock,
266
        ];
267
268
        return md5(json_encode([
269
            $vars,
270
            $columns,
271
            static::class,
272
            $this->getCacheDriver(),
273
            $this->getCacheLifetime(),
274
            $builder instanceof Builder ? $builder->getEagerLoads() : null,
275
            $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...
276
            $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...
277
        ]));
278
    }
279
280
    /**
281
     * Cache given callback.
282
     *
283
     * @param \Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder $builder
284
     * @param array                                                                    $columns
285
     * @param \Closure                                                                 $closure
286
     *
287
     * @return mixed
288
     */
289
    public function cacheQuery($builder, array $columns, Closure $closure)
290
    {
291
        $modelName = $this->getMorphClass();
0 ignored issues
show
Bug introduced by
It seems like getMorphClass() must be provided by classes using this trait. How about adding it as abstract method to this trait?

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

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). 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.

Loading history...
292
        $lifetime = $this->getCacheLifetime();
293
        $cacheKey = $this->generateCacheKey($builder, $columns);
294
295
        // Switch cache driver on runtime
296
        if ($driver = $this->getCacheDriver()) {
297
            app('cache')->setDefaultDriver($driver);
298
        }
299
300
        // We need cache tags, check if default driver supports it
301
        if (method_exists(app('cache')->getStore(), 'tags')) {
302
            $result = $lifetime === -1 ? app('cache')->tags($modelName)->rememberForever($cacheKey, $closure) : app('cache')->tags($modelName)->remember($cacheKey, $lifetime, $closure);
303
304
            return $result;
305
        }
306
307
        $result = $lifetime === -1 ? app('cache')->rememberForever($cacheKey, $closure) : app('cache')->remember($cacheKey, $lifetime, $closure);
308
309
        // Default cache driver doesn't support tags, let's do it manually
310
        static::storeCacheKey($modelName, $cacheKey);
311
312
        // We're done, let's clean up!
313
        $this->resetCacheConfig();
314
315
        return $result;
316
    }
317
318
    /**
319
     * Create a new Eloquent query builder for the model.
320
     *
321
     * @param \Illuminate\Database\Query\Builder $query
322
     *
323
     * @return \Illuminate\Database\Eloquent\Builder|static
324
     */
325
    public function newEloquentBuilder($query)
326
    {
327
        return new EloquentBuilder($query);
328
    }
329
}
330