Passed
Branch master (fd157b)
by Menno
02:03
created

ManagedCache::isModel()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 1
nc 2
nop 1
dl 0
loc 3
rs 10
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
class ManagedCache
15
{
16
    //  Eloquent events.
17
    const EVENT_ELOQUENT_CREATED = 'eloquent.created';
18
    const EVENT_ELOQUENT_UPDATED = 'eloquent.updated';
19
    const EVENT_ELOQUENT_SAVED = 'eloquent.saved';
20
    const EVENT_ELOQUENT_DELETED = 'eloquent.deleted';
21
    const EVENT_ELOQUENT_RESTORED = 'eloquent.restored';
22
    //  Relation events.
23
    const EVENT_ELOQUENT_ATTACHED = 'eloquent.attached';
24
    const EVENT_ELOQUENT_DETACHED = 'eloquent.detached';
25
26
    //  ..
27
    const TAG_MAP_CACHE_KEY = 'ManagedCache_TagMap';
28
29
    /**
30
     * @var Dispatcher
31
     */
32
    protected $dispatcher;
33
34
    /**
35
     * @var StoreContract
36
     */
37
    protected $store;
38
39
    private $isDebugModeEnabled = false;
40
41
    protected $tagMap = [];
42
43
    /**
44
     * Constructor.
45
     *
46
     * @param Application $application
47
     */
48
    public function __construct(Application $application)
49
    {
50
        $this->store = $application['cache.store'];
0 ignored issues
show
Documentation Bug introduced by
It seems like $application['cache.store'] of type Illuminate\Cache\Repository is incompatible with the declared type Illuminate\Contracts\Cache\Store of property $store.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
51
        if ( ! ($this->store->getStore() instanceof MemcachedStore)) {
52
            throw new Exception('Memcached not configured. Cache store is "' . class_basename($this->store) . '"');
53
        }
54
        $this->dispatcher = $application['events'];
55
        $this->registerEventListener();
56
    }
57
58
    public function enableDebugMode()
59
    {
60
        $this->isDebugModeEnabled = true;
61
62
        return $this;
63
    }
64
65
    public function isDebugModeEnabled()
66
    {
67
        return $this->isDebugModeEnabled;
68
    }
69
70
    public function getTagMap(): array
71
    {
72
        if (empty($this->tagMap)) {
73
            $this->tagMap = $this->store->get(self::TAG_MAP_CACHE_KEY, []);
74
            if ( ! is_array($this->tagMap)) {
75
                $this->tagMap = [];
76
            }
77
        }
78
79
        return $this->tagMap;
80
    }
81
82
    public function getTagsForKey(string $key): array
83
    {
84
        $tagMap = $this->getTagMap();
85
        if ( ! isset($tagMap[$key])) {
86
            return [];
87
        }
88
89
        return $tagMap[$key];
90
    }
91
92
    public function setTagsForKey(string $key, array $tags): void
93
    {
94
        $this->getTagMap();
95
        $this->tagMap[$key] = $tags;
96
        $this->store->forever(self::TAG_MAP_CACHE_KEY, $this->tagMap);
97
    }
98
99
    public function deleteTagsForKey(string $key)
100
    {
101
        $this->getTagMap();
102
        if (isset($this->tagMap[$key])) {
103
            unset($this->tagMap[$key]);
104
            $this->store->forever(self::TAG_MAP_CACHE_KEY, $this->tagMap);
105
        }
106
    }
107
108
    /**
109
     * Returns the Cache store instance.
110
     *
111
     * @return CacheRepository
112
     */
113
    public function getStore(): CacheRepository
114
    {
115
        return $this->store;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->store returns the type Illuminate\Contracts\Cache\Store which is incompatible with the type-hinted return Illuminate\Cache\Repository.
Loading history...
116
    }
117
118
    /**
119
     * Register event listeners.
120
     */
121
    protected function registerEventListener(): void
122
    {
123
        //  Register Eloquent event listeners.
124
        foreach ($this->getObservableEvents() as $eventKey) {
125
            $this->dispatcher->listen($eventKey . ':*', [$this, 'handleEloquentEvent']);
126
        }
127
    }
128
129
    /**
130
     * Handle an Eloquent event.
131
     *
132
     * @param string $eventKey
133
     * @param mixed $payload
134
     */
135
    public function handleEloquentEvent($eventKey, $payload): void
136
    {
137
        //  Extract the basic event name and the model name from the event key.
138
        $regex = '/^(' . implode('|', $this->getObservableEvents()) . '): ([a-zA-Z0-9\\\\]+)$/';
139
        if ( ! preg_match($regex, $eventKey, $matches)) {
140
            return;
141
        }
142
        $eventName = $matches[1];
143
        $modelName = $matches[2];
144
        //  Ensure $payload is always an array.
145
        if ( ! is_array($payload)) {
146
            $payload = [$payload];
147
        }
148
        //  Flush items that are tagged with this event (and no model).
149
        //	i.e. items that should be flushed when this event happens to ANY instance of the model.
150
        $cacheTags = [];
151
        //  Create a tag to flush stores tagged with:
152
        //  -   this Eloquent event, AND
153
        //  -   this Model class
154
        $cacheTags[] = new Condition($eventName, $modelName);
155
        foreach ($payload as $model) {
156
            if ( ! $this->isModel($model)) {
157
                continue;
158
            }
159
            $modelId = $model->getKey();
160
            if ( ! empty($modelId)) {
161
                //  Create a tag to flush stores tagged with:
162
                //  -   this Eloquent event, AND
163
                //  -   this Model instance
164
                $cacheTags[] = new Condition($eventName, $modelName, $modelId);
165
            }
166
            //	Create tags for related models.
167
            foreach ($this->extractModelKeys($model->getAttributes()) as $relatedModelName => $relatedModelId) {
168
                //	Flush cached items that are tagged through a relation
169
                //	with this model.
170
                if ('delete' === $eventName) {
171
                    $relatedEventName = 'detach';
172
                } else {
173
                    $relatedEventName = 'attach';
174
                }
175
                $cacheTags[] = new Condition($relatedEventName, $modelName, $modelId, $relatedModelName, $relatedModelId);
176
            }
177
        }
178
        //	Flush all stores with these tags
179
        $this->forgetWhen($cacheTags)->flush();
0 ignored issues
show
Bug introduced by
The method forgetWhen() does not exist on Codefocus\ManagedCache\ManagedCache. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

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