Passed
Push — master ( 5f058e...885914 )
by Menno
01:56
created

ManagedCache::getObservableEvents()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace Codefocus\ManagedCache;
4
5
use BadFunctionCallException;
6
use Exception;
7
use Illuminate\Cache\MemcachedStore;
8
use Illuminate\Cache\Repository as CacheRepository;
9
use Illuminate\Contracts\Cache\Store as StoreContract;
10
use Illuminate\Contracts\Events\Dispatcher;
11
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Foundation\Application;
13
14
/**
15
 * ManagedCache
16
 *
17
 * @method DefinitionChain forgetWhen(array $conditions)
18
 */
19
class ManagedCache
20
{
21
    //  Eloquent events.
22
    const EVENT_ELOQUENT_CREATED = 'eloquent.created';
23
    const EVENT_ELOQUENT_UPDATED = 'eloquent.updated';
24
    const EVENT_ELOQUENT_SAVED = 'eloquent.saved';
25
    const EVENT_ELOQUENT_DELETED = 'eloquent.deleted';
26
    const EVENT_ELOQUENT_RESTORED = 'eloquent.restored';
27
    //  Relation events.
28
    const EVENT_ELOQUENT_ATTACHED = 'eloquent.attached';
29
    const EVENT_ELOQUENT_DETACHED = 'eloquent.detached';
30
    //  Cache keys.
31
    const TAG_MAP_CACHE_KEY = 'ManagedCache_TagMap';
32
33
    /**
34
     * @var Dispatcher
35
     */
36
    protected $dispatcher;
37
38
    /**
39
     * @var CacheRepository
40
     */
41
    protected $store;
42
43
    private $isDebugModeEnabled = false;
44
45
    /**
46
     * Maps cache keys to their tags.
47
     *
48
     * @var array
49
     */
50
    protected $tagMap = [];
51
52
    /**
53
     * Constructor.
54
     *
55
     * @param Application $application
56
     */
57
    public function __construct(Application $application)
58
    {
59
        $this->store = $application['cache.store'];
60
        if ( ! ($this->store->getStore() instanceof MemcachedStore)) {
61
            throw new Exception('Memcached not configured. Cache store is "' . class_basename($this->store) . '"');
62
        }
63
        $this->dispatcher = $application['events'];
64
        $this->registerEventListener();
65
    }
66
67
    public function enableDebugMode()
68
    {
69
        $this->isDebugModeEnabled = true;
70
71
        return $this;
72
    }
73
74
    public function isDebugModeEnabled()
75
    {
76
        return $this->isDebugModeEnabled;
77
    }
78
79
    public function getTagMap(): array
80
    {
81
        if (empty($this->tagMap)) {
82
            $this->tagMap = $this->store->get(self::TAG_MAP_CACHE_KEY, []);
83
            if ( ! is_array($this->tagMap)) {
84
                $this->tagMap = [];
85
            }
86
        }
87
88
        return $this->tagMap;
89
    }
90
91
    public function getTagsForKey(string $key): array
92
    {
93
        $tagMap = $this->getTagMap();
94
        if ( ! isset($tagMap[$key])) {
95
            return [];
96
        }
97
98
        return $tagMap[$key];
99
    }
100
101
    public function setTagsForKey(string $key, array $tags): void
102
    {
103
        $this->getTagMap();
104
        $this->tagMap[$key] = $tags;
105
        $this->store->forever(self::TAG_MAP_CACHE_KEY, $this->tagMap);
106
    }
107
108
    public function deleteTagsForKey(string $key)
109
    {
110
        $this->getTagMap();
111
        if (isset($this->tagMap[$key])) {
112
            unset($this->tagMap[$key]);
113
            $this->store->forever(self::TAG_MAP_CACHE_KEY, $this->tagMap);
114
        }
115
    }
116
117
    /**
118
     * Returns the Cache store instance.
119
     *
120
     * @return CacheRepository
121
     */
122
    public function getStore(): CacheRepository
123
    {
124
        return $this->store;
125
    }
126
127
    /**
128
     * Register event listeners.
129
     */
130
    protected function registerEventListener(): void
131
    {
132
        //  Register Eloquent event listeners.
133
        foreach ($this->getObservableEvents() as $eventKey) {
134
            $this->dispatcher->listen($eventKey . ':*', [$this, 'handleEloquentEvent']);
135
        }
136
    }
137
138
    /**
139
     * Handle an Eloquent event.
140
     *
141
     * @param string $eventKey
142
     * @param mixed $payload
143
     */
144
    public function handleEloquentEvent($eventKey, $payload): void
145
    {
146
        //  Extract the basic event name and the model name from the event key.
147
        $regex = '/^(' . implode('|', $this->getObservableEvents()) . '): ([a-zA-Z0-9\\\\]+)$/';
148
        if ( ! preg_match($regex, $eventKey, $matches)) {
149
            return;
150
        }
151
        $eventName = $matches[1];
152
        $modelName = $matches[2];
153
        //  Ensure $payload is always an array.
154
        $payload = (is_array($payload)) ? $payload : [$payload];
155
        //  Create a tag to flush stores tagged with:
156
        //  -   this Eloquent event, AND
157
        //  -   this Model class
158
        $cacheTags = [
159
            new Condition($eventName, $modelName)
160
        ];
161
        foreach ($payload as $model) {
162
            if ( ! $this->isModel($model)) {
163
                continue;
164
            }
165
            $cacheTags += $this->getModelEventTags($model, $eventName);
166
        }
167
        //	Flush all stores with these tags
168
        $this->forgetWhen($cacheTags)->flush();
169
    }
170
171
    private function getModelEventTags(Model $model, string $eventName)
172
    {
173
        $modelId = $model->getKey();
174
        if (empty($modelId)) {
175
            return [];
176
        }
177
        $modelName = get_class($model);
178
        //  Create a tag to flush stores tagged with:
179
        //  -   this Eloquent event, AND
180
        //  -   this Model instance
181
        $cacheTags = [
182
            new Condition($eventName, $modelName, $modelId)
0 ignored issues
show
Bug introduced by
It seems like $modelId can also be of type Carbon\Carbon and string and boolean and double and Illuminate\Support\Collection; however, parameter $modelId of Codefocus\ManagedCache\Condition::__construct() does only seem to accept null|integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

182
            new Condition($eventName, $modelName, /** @scrutinizer ignore-type */ $modelId)
Loading history...
183
        ];
184
        //	Create tags for related models.
185
        foreach ($this->extractModelKeys($model) as $relatedModelName => $relatedModelId) {
186
            //	Flush cached items that are tagged through a relation
187
            //	with this model.
188
            $cacheTags[] = new Condition(
189
                (self::EVENT_ELOQUENT_DELETED === $eventName) ? self::EVENT_ELOQUENT_DETACHED : self::EVENT_ELOQUENT_ATTACHED,
190
                $modelName,
191
                $modelId,
192
                $relatedModelName,
193
                $relatedModelId
194
            );
195
        }
196
197
        return $cacheTags;
198
    }
199
200
    /**
201
     * Get the observable event names.
202
     *
203
     * @return array
204
     */
205
    protected function getObservableEvents(): array
206
    {
207
        return [
208
            static::EVENT_ELOQUENT_CREATED,
209
            static::EVENT_ELOQUENT_UPDATED,
210
            static::EVENT_ELOQUENT_SAVED,
211
            static::EVENT_ELOQUENT_DELETED,
212
            static::EVENT_ELOQUENT_RESTORED,
213
        ];
214
    }
215
216
    /**
217
     * Extract attributes that act as foreign keys.
218
     *
219
     * @param Model $model An Eloquent Model instance
220
     *
221
     * @return array
222
     */
223
    protected function extractModelKeys(Model $model)
224
    {
225
        $modelKeys = [];
226
        foreach ($model->getAttributes() as $attributeName => $value) {
227
            if (preg_match('/([^_]+)_id/', $attributeName, $matches)) {
228
                //	This field is a key
229
                $modelKeys[strtolower($matches[1])] = $value;
230
            }
231
        }
232
        //	Ensure our model keys are always in the same order.
233
        ksort($modelKeys);
234
235
        return $modelKeys;
236
    }
237
238
    /**
239
     * Returns a Condition instance that tags a cache to get invalidated
240
     * when a new Model of the specified class is created.
241
     *
242
     * @param string $modelClassName model class name
243
     *
244
     * @return Condition
245
     */
246
    public function created(string $modelClassName): Condition
247
    {
248
        return new Condition(
249
            self::EVENT_ELOQUENT_CREATED,
250
            $modelClassName
251
        );
252
    }
253
254
    /**
255
     * Returns a Condition instance that tags a cache to get invalidated
256
     * when the specified Model instance, or any Model of the specified class
257
     * is updated.
258
     *
259
     * @param mixed $model model instance or class name
260
     * @param int|null $modelId (default: null) The Model id
261
     *
262
     * @return Condition
263
     */
264
    public function updated($model, ?int $modelId = null): Condition
265
    {
266
        if ($this->isModel($model)) {
267
            $modelClassName = get_class($model);
268
            $modelId = $model->getKey();
269
        } else {
270
            $modelClassName = $model;
271
        }
272
273
        return new Condition(
274
            self::EVENT_ELOQUENT_UPDATED,
275
            $modelClassName,
276
            $modelId
277
        );
278
    }
279
280
    /**
281
     * Returns a Condition instance that tags a cache to get invalidated
282
     * when the specified Model instance, or any Model of the specified class
283
     * is saved.
284
     *
285
     * @param mixed $model model instance or class name
286
     * @param int|null $modelId (default: null) The Model id
287
     *
288
     * @return Condition
289
     */
290
    public function saved($model, ?int $modelId = null): Condition
291
    {
292
        if ($this->isModel($model)) {
293
            $modelClassName = get_class($model);
294
            $modelId = $model->getKey();
295
        } else {
296
            $modelClassName = $model;
297
        }
298
299
        return new Condition(
300
            self::EVENT_ELOQUENT_SAVED,
301
            $modelClassName,
302
            $modelId
303
        );
304
    }
305
306
    /**
307
     * Returns a Condition instance that tags a cache to get invalidated
308
     * when the specified Model instance, or any Model of the specified class
309
     * is deleted.
310
     *
311
     * @param mixed $model model instance or class name
312
     * @param int|null $modelId (default: null) The Model id
313
     *
314
     * @return Condition
315
     */
316
    public function deleted($model, ?int $modelId = null): Condition
317
    {
318
        if ($this->isModel($model)) {
319
            $modelClassName = get_class($model);
320
            $modelId = $model->getKey();
321
        } else {
322
            $modelClassName = $model;
323
        }
324
325
        return new Condition(
326
            self::EVENT_ELOQUENT_DELETED,
327
            $modelClassName,
328
            $modelId
329
        );
330
    }
331
332
    /**
333
     * Returns a Condition instance that tags a cache to get invalidated
334
     * when the specified Model instance, or any Model of the specified class
335
     * is restored.
336
     *
337
     * @param mixed $model model instance or class name
338
     * @param int|null $modelId (default: null) The Model id
339
     *
340
     * @return Condition
341
     */
342
    public function restored($model, ?int $modelId = null): Condition
343
    {
344
        if ($this->isModel($model)) {
345
            $modelClassName = get_class($model);
346
            $modelId = $model->getKey();
347
        } else {
348
            $modelClassName = $model;
349
        }
350
351
        return new Condition(
352
            self::EVENT_ELOQUENT_RESTORED,
353
            $modelClassName,
354
            $modelId
355
        );
356
    }
357
358
    /**
359
     * Returns a Condition instance that tags a cache to get invalidated when
360
     * a related Model of the specified class is attached.
361
     *
362
     * @param mixed $model model instance or class name
363
     * @param int|null $modelId (default: null) the Model id, if $model is a class name
364
     * @param mixed|null $relatedModel (default: null) the related Model instance or class name
365
     * @param int|null $relatedModelId (default: null) the related Model id
366
     *
367
     * @return Condition
368
     */
369
    public function relationAttached($model, ?int $modelId = null, $relatedModel = null, ?int $relatedModelId = null): Condition
370
    {
371
        if ($this->isModel($model)) {
372
            $modelClassName = get_class($model);
373
            $modelId = $model->getKey();
374
        } else {
375
            $modelClassName = $model;
376
        }
377
        if ($this->isModel($relatedModel)) {
378
            $relatedModelClassName = get_class($relatedModel);
379
            $relatedModelId = $relatedModel->/** @scrutinizer ignore-call */getKey();
380
        } else {
381
            $relatedModelClassName = $relatedModel;
382
        }
383
384
        return new Condition(
385
            self::EVENT_ELOQUENT_ATTACHED,
386
            $modelClassName,
387
            $modelId,
388
            $relatedModelClassName,
389
            $relatedModelId
390
        );
391
    }
392
393
    /**
394
     * Returns a Condition instance that tags a cache to get invalidated when
395
     * a related Model of the specified class is detached.
396
     *
397
     * @param mixed $model model instance or class name
398
     * @param int|null $modelId (default: null) the Model id, if $model is a class name
399
     * @param mixed|null $relatedModel (default: null) the related Model instance or class name
400
     * @param int|null $relatedModelId (default: null) the related Model id
401
     *
402
     * @return Condition
403
     */
404
    public function relationDetached($model, ?int $modelId = null, $relatedModel = null, ?int $relatedModelId = null): Condition
405
    {
406
        if ($this->isModel($model)) {
407
            $modelClassName = get_class($model);
408
            $modelId = $model->getKey();
409
        } else {
410
            $modelClassName = $model;
411
        }
412
        if ($this->isModel($relatedModel)) {
413
            $relatedModelClassName = get_class($relatedModel);
414
            $relatedModelId = $relatedModel->/** @scrutinizer ignore-call */getKey();
415
        } else {
416
            $relatedModelClassName = $relatedModel;
417
        }
418
419
        return new Condition(
420
            self::EVENT_ELOQUENT_DETACHED,
421
            $modelClassName,
422
            $modelId,
423
            $relatedModelClassName,
424
            $relatedModelId
425
        );
426
    }
427
428
    /**
429
     * Returns a Condition instance that tags a cache to get invalidated when
430
     * a related Model of the specified class is updated.
431
     *
432
     * @param mixed $model model instance or class name
433
     * @param int|null $modelId (default: null) the Model id, if $model is a class name
434
     * @param mixed|null $relatedModel (default: null) the related Model instance or class name
435
     * @param int|null $relatedModelId (default: null) the related Model id
436
     *
437
     * @return Condition
438
     */
439
    public function relationUpdated($model, ?int $modelId = null, $relatedModel = null, ?int $relatedModelId = null): Condition
440
    {
441
        if ($this->isModel($model)) {
442
            $modelClassName = get_class($model);
443
            $modelId = $model->getKey();
444
        } else {
445
            $modelClassName = $model;
446
        }
447
        if ($this->isModel($relatedModel)) {
448
            $relatedModelClassName = get_class($relatedModel);
449
            $relatedModelId = $relatedModel->/** @scrutinizer ignore-call */getKey();
450
        } else {
451
            $relatedModelClassName = $relatedModel;
452
        }
453
454
        return new Condition(
455
            self::EVENT_ELOQUENT_UPDATED,
456
            $modelClassName,
457
            $modelId,
458
            $relatedModelClassName,
459
            $relatedModelId
460
        );
461
    }
462
463
    /**
464
     * Return whether the specified class name is an Eloquent Model.
465
     *
466
     * @param mixed $value
467
     *
468
     * @return bool
469
     */
470
    protected function isModel($value): bool
471
    {
472
        return (is_object($value) && is_subclass_of($value, Model::class));
473
    }
474
475
    /**
476
     * Route function calls to a new DefinitionChain.
477
     *
478
     * @param string $name
479
     * @param array $arguments
480
     *
481
     * @throws BadFunctionCallException
482
     */
483
    public function __call(string $name, array $arguments)
484
    {
485
        $definitionChain = new DefinitionChain($this);
486
        if ( ! method_exists($definitionChain, $name)) {
487
            throw new BadFunctionCallException('Function ' . $name . ' does not exist.');
488
        }
489
490
        return call_user_func_array([$definitionChain, $name], $arguments);
491
    }
492
}
493