Completed
Push — master ( af64de...0bab8c )
by Abdelrahman
02:32 queued 01:21
created

CacheableEloquent::attacheEvents()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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