Completed
Push — develop ( 0b6fde...dcedcb )
by Abdelrahman
01:08
created

CacheableEloquent   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 400
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 4

Importance

Changes 0
Metric Value
wmc 41
lcom 2
cbo 4
dl 0
loc 400
rs 8.2769
c 0
b 0
f 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
updated() 0 1 ?
created() 0 1 ?
deleted() 0 1 ?
A bootCacheableEloquent() 0 4 1
A storeCacheKey() 0 10 3
A getCacheKeys() 0 8 3
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 1
B cacheQuery() 0 28 5
A newEloquentBuilder() 0 4 1
A attacheEvents() 0 20 4
B generateCacheKey() 0 33 3
C belongsToMany() 0 32 7

How to fix   Complexity   

Complex Class

Complex classes like CacheableEloquent often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CacheableEloquent, and based on these observations, apply Extract Interface, too.

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
    /**
366
     * Define a many-to-many relationship.
367
     *
368
     * @param  string $related
369
     * @param  string $table
0 ignored issues
show
Documentation introduced by
Should the type for parameter $table not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
370
     * @param  string $foreignPivotKey
0 ignored issues
show
Documentation introduced by
Should the type for parameter $foreignPivotKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
371
     * @param  string $relatedPivotKey
0 ignored issues
show
Documentation introduced by
Should the type for parameter $relatedPivotKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
372
     * @param  string $parentKey
0 ignored issues
show
Documentation introduced by
Should the type for parameter $parentKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
373
     * @param  string $relatedKey
0 ignored issues
show
Documentation introduced by
Should the type for parameter $relatedKey not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
374
     * @param  string $relation
0 ignored issues
show
Documentation introduced by
Should the type for parameter $relation not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
375
     *
376
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use BelongsToMany.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
377
     */
378
    public function belongsToMany($related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null,
379
                                  $parentKey = null, $relatedKey = null, $relation = null)
380
    {
381
        // If no relationship name was passed, we will pull backtraces to get the
382
        // name of the calling function. We will use that function name as the
383
        // title of this relation since that is a great convention to apply.
384
        if (is_null($relation)) {
385
            $relation = $this->guessBelongsToManyRelation();
0 ignored issues
show
Bug introduced by
The method guessBelongsToManyRelation() does not exist on Rinvex\Cacheable\CacheableEloquent. Did you maybe mean belongsToMany()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
386
        }
387
388
        // First, we'll need to determine the foreign key and "other key" for the
389
        // relationship. Once we have determined the keys we'll make the query
390
        // instances as well as the relationship instances we need for this.
391
        $instance = $this->newRelatedInstance($related);
0 ignored issues
show
Bug introduced by
It seems like newRelatedInstance() 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...
392
393
        $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey();
0 ignored issues
show
Bug introduced by
It seems like getForeignKey() 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...
394
395
        $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey();
396
397
        // If no table name was provided, we can guess it by concatenating the two
398
        // models using underscores in alphabetical order. The two model names
399
        // are transformed to snake case from their default CamelCase also.
400
        if (is_null($table)) {
401
            $table = $this->joiningTable($related);
0 ignored issues
show
Bug introduced by
It seems like joiningTable() 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...
402
        }
403
404
        return new BelongsToMany(
405
            $instance->newQuery(), $this, $table, $foreignPivotKey,
406
            $relatedPivotKey, $parentKey ?: $this->getKeyName(),
0 ignored issues
show
Bug introduced by
It seems like getKeyName() 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...
407
            $relatedKey ?: $instance->getKeyName(), $relation
408
        );
409
    }
410
}
411