Passed
Branch master (3ccf2a)
by Menno
01:54
created

ManagedCache::enableDebugMode()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 5
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
        if ( ! is_array($payload)) {
155
            $payload = [$payload];
156
        }
157
        //  Flush items that are tagged with this event (and no model).
158
        //	i.e. items that should be flushed when this event happens to ANY instance of the model.
159
        $cacheTags = [];
160
        //  Create a tag to flush stores tagged with:
161
        //  -   this Eloquent event, AND
162
        //  -   this Model class
163
        $cacheTags[] = new Condition($eventName, $modelName);
164
        foreach ($payload as $model) {
165
            if ( ! $this->isModel($model)) {
166
                continue;
167
            }
168
            $modelId = $model->getKey();
169
            if ( ! empty($modelId)) {
170
                //  Create a tag to flush stores tagged with:
171
                //  -   this Eloquent event, AND
172
                //  -   this Model instance
173
                $cacheTags[] = new Condition($eventName, $modelName, $modelId);
174
            }
175
            //	Create tags for related models.
176
            foreach ($this->extractModelKeys($model->getAttributes()) as $relatedModelName => $relatedModelId) {
177
                //	Flush cached items that are tagged through a relation
178
                //	with this model.
179
                if ('delete' === $eventName) {
180
                    $relatedEventName = 'detach';
181
                } else {
182
                    $relatedEventName = 'attach';
183
                }
184
                $cacheTags[] = new Condition($relatedEventName, $modelName, $modelId, $relatedModelName, $relatedModelId);
185
            }
186
        }
187
        //	Flush all stores with these tags
188
        $this->forgetWhen($cacheTags)->flush();
189
    }
190
191
    /**
192
     * Get the observable event names.
193
     *
194
     * @return array
195
     */
196
    protected function getObservableEvents(): array
197
    {
198
        return [
199
            static::EVENT_ELOQUENT_CREATED,
200
            static::EVENT_ELOQUENT_UPDATED,
201
            static::EVENT_ELOQUENT_SAVED,
202
            static::EVENT_ELOQUENT_DELETED,
203
            static::EVENT_ELOQUENT_RESTORED,
204
        ];
205
    }
206
207
    /**
208
     * extractModelKeys function.
209
     *
210
     * @param array $attributeNames
211
     *
212
     * @return array
213
     */
214
    protected function extractModelKeys(array $attributeNames)
215
    {
216
        $modelKeys = [];
217
        foreach ($attributeNames as $attributeName => $value) {
218
            if (preg_match('/([^_]+)_id/', $attributeName, $matches)) {
219
                //	This field is a key
220
                $modelKeys[strtolower($matches[1])] = $value;
221
            }
222
        }
223
        //	Ensure our model keys are always in the same order.
224
        ksort($modelKeys);
225
226
        return $modelKeys;
227
    }
228
229
    /**
230
     * Returns a Condition instance that tags a cache to get invalidated
231
     * when a new Model of the specified class is created.
232
     *
233
     * @param string $modelClassName model class name
234
     *
235
     * @return Condition
236
     */
237
    public function created(string $modelClassName): Condition
238
    {
239
        return new Condition(
240
            self::EVENT_ELOQUENT_CREATED,
241
            $modelClassName
242
        );
243
    }
244
245
    /**
246
     * Returns a Condition instance that tags a cache to get invalidated
247
     * when the specified Model instance, or any Model of the specified class
248
     * is updated.
249
     *
250
     * @param mixed $model model instance or class name
251
     * @param int|null $modelId (default: null) The Model id
252
     *
253
     * @return Condition
254
     */
255
    public function updated($model, ?int $modelId = null): Condition
256
    {
257
        if ($this->isModel($model)) {
258
            $modelClassName = get_class($model);
259
            $modelId = $model->getKey();
260
        } else {
261
            $modelClassName = $model;
262
        }
263
264
        return new Condition(
265
            self::EVENT_ELOQUENT_UPDATED,
266
            $modelClassName,
267
            $modelId
268
        );
269
    }
270
271
    /**
272
     * Returns a Condition instance that tags a cache to get invalidated
273
     * when the specified Model instance, or any Model of the specified class
274
     * is saved.
275
     *
276
     * @param mixed $model model instance or class name
277
     * @param int|null $modelId (default: null) The Model id
278
     *
279
     * @return Condition
280
     */
281
    public function saved($model, ?int $modelId = null): Condition
282
    {
283
        if ($this->isModel($model)) {
284
            $modelClassName = get_class($model);
285
            $modelId = $model->getKey();
286
        } else {
287
            $modelClassName = $model;
288
        }
289
290
        return new Condition(
291
            self::EVENT_ELOQUENT_SAVED,
292
            $modelClassName,
293
            $modelId
294
        );
295
    }
296
297
    /**
298
     * Returns a Condition instance that tags a cache to get invalidated
299
     * when the specified Model instance, or any Model of the specified class
300
     * is deleted.
301
     *
302
     * @param mixed $model model instance or class name
303
     * @param int|null $modelId (default: null) The Model id
304
     *
305
     * @return Condition
306
     */
307
    public function deleted($model, ?int $modelId = null): Condition
308
    {
309
        if ($this->isModel($model)) {
310
            $modelClassName = get_class($model);
311
            $modelId = $model->getKey();
312
        } else {
313
            $modelClassName = $model;
314
        }
315
316
        return new Condition(
317
            self::EVENT_ELOQUENT_DELETED,
318
            $modelClassName,
319
            $modelId
320
        );
321
    }
322
323
    /**
324
     * Returns a Condition instance that tags a cache to get invalidated
325
     * when the specified Model instance, or any Model of the specified class
326
     * is restored.
327
     *
328
     * @param mixed $model model instance or class name
329
     * @param int|null $modelId (default: null) The Model id
330
     *
331
     * @return Condition
332
     */
333
    public function restored($model, ?int $modelId = null): Condition
334
    {
335
        if ($this->isModel($model)) {
336
            $modelClassName = get_class($model);
337
            $modelId = $model->getKey();
338
        } else {
339
            $modelClassName = $model;
340
        }
341
342
        return new Condition(
343
            self::EVENT_ELOQUENT_RESTORED,
344
            $modelClassName,
345
            $modelId
346
        );
347
    }
348
349
    /**
350
     * Returns a Condition instance that tags a cache to get invalidated when
351
     * a related Model of the specified class is attached.
352
     *
353
     * @param mixed $model model instance or class name
354
     * @param int|null $modelId (default: null) the Model id, if $model is a class name
355
     * @param mixed|null $relatedModel (default: null) the related Model instance or class name
356
     * @param int|null $relatedModelId (default: null) the related Model id
357
     *
358
     * @return Condition
359
     */
360
    public function relationAttached($model, ?int $modelId = null, $relatedModel = null, ?int $relatedModelId = null): Condition
361
    {
362
        if ($this->isModel($model)) {
363
            $modelClassName = get_class($model);
364
            $modelId = $model->getKey();
365
        } else {
366
            $modelClassName = $model;
367
        }
368
        if ($this->isModel($relatedModel)) {
369
            $relatedModelClassName = get_class($relatedModel);
370
            $relatedModelId = $relatedModel->getKey();
0 ignored issues
show
Bug introduced by
The method getKey() does not exist on null. ( Ignorable by Annotation )

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

370
            $relatedModelId = $relatedModel->/** @scrutinizer ignore-call */ getKey();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
371
        } else {
372
            $relatedModelClassName = $relatedModel;
373
        }
374
375
        return new Condition(
376
            self::EVENT_ELOQUENT_ATTACHED,
377
            $modelClassName,
378
            $modelId,
379
            $relatedModelClassName,
380
            $relatedModelId
381
        );
382
    }
383
384
    /**
385
     * Returns a Condition instance that tags a cache to get invalidated when
386
     * a related Model of the specified class is detached.
387
     *
388
     * @param mixed $model model instance or class name
389
     * @param int|null $modelId (default: null) the Model id, if $model is a class name
390
     * @param mixed|null $relatedModel (default: null) the related Model instance or class name
391
     * @param int|null $relatedModelId (default: null) the related Model id
392
     *
393
     * @return Condition
394
     */
395
    public function relationDetached($model, ?int $modelId = null, $relatedModel = null, ?int $relatedModelId = null): Condition
396
    {
397
        if ($this->isModel($model)) {
398
            $modelClassName = get_class($model);
399
            $modelId = $model->getKey();
400
        } else {
401
            $modelClassName = $model;
402
        }
403
        if ($this->isModel($relatedModel)) {
404
            $relatedModelClassName = get_class($relatedModel);
405
            $relatedModelId = $relatedModel->getKey();
406
        } else {
407
            $relatedModelClassName = $relatedModel;
408
        }
409
410
        return new Condition(
411
            self::EVENT_ELOQUENT_DETACHED,
412
            $modelClassName,
413
            $modelId,
414
            $relatedModelClassName,
415
            $relatedModelId
416
        );
417
    }
418
419
    /**
420
     * Returns a Condition instance that tags a cache to get invalidated when
421
     * a related Model of the specified class is updated.
422
     *
423
     * @param mixed $model model instance or class name
424
     * @param int|null $modelId (default: null) the Model id, if $model is a class name
425
     * @param mixed|null $relatedModel (default: null) the related Model instance or class name
426
     * @param int|null $relatedModelId (default: null) the related Model id
427
     *
428
     * @return Condition
429
     */
430
    public function relationUpdated($model, ?int $modelId = null, $relatedModel = null, ?int $relatedModelId = null): Condition
431
    {
432
        if ($this->isModel($model)) {
433
            $modelClassName = get_class($model);
434
            $modelId = $model->getKey();
435
        } else {
436
            $modelClassName = $model;
437
        }
438
        if ($this->isModel($relatedModel)) {
439
            $relatedModelClassName = get_class($relatedModel);
440
            $relatedModelId = $relatedModel->getKey();
441
        } else {
442
            $relatedModelClassName = $relatedModel;
443
        }
444
445
        return new Condition(
446
            self::EVENT_ELOQUENT_UPDATED,
447
            $modelClassName,
448
            $modelId,
449
            $relatedModelClassName,
450
            $relatedModelId
451
        );
452
    }
453
454
    /**
455
     * Return whether the specified class name is an Eloquent Model.
456
     *
457
     * @param mixed $value
458
     *
459
     * @return bool
460
     */
461
    protected function isModel($value): bool
462
    {
463
        return (is_object($value) && is_subclass_of($value, Model::class));
464
    }
465
466
    /**
467
     * Route function calls to a new DefinitionChain.
468
     *
469
     * @param string $name
470
     * @param array $arguments
471
     *
472
     * @throws BadFunctionCallException
473
     */
474
    public function __call(string $name, array $arguments)
475
    {
476
        $definitionChain = new DefinitionChain($this);
477
        if ( ! method_exists($definitionChain, $name)) {
478
            throw new BadFunctionCallException('Function ' . $name . ' does not exist.');
479
        }
480
481
        return call_user_func_array([$definitionChain, $name], $arguments);
482
    }
483
}
484