Passed
Branch master (6b6e1c)
by Menno
02:03
created

ManagedCache::relationAttached()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 21
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 4
nop 4
dl 0
loc 21
rs 8.7624
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
        $regex = '/^(' . implode('|', $this->getObservableEvents()) . '): ([a-zA-Z0-9\\\\]+)$/';
138
139
        if ( ! preg_match($regex, $eventKey, $matches)) {
140
            return;
141
        }
142
143
        $eventName = $matches[1];
144
        $modelName = $matches[2];
145
146
        //  Ensure $payload is always an array.
147
        if ( ! is_array($payload)) {
148
            $payload = [$payload];
149
        }
150
151
        //  Flush items that are tagged with this event (and no model).
152
        //	i.e. items that should be flushed when this event happens to ANY instance of the model.
153
        $cacheTags = [];
154
155
        //  Create a tag to flush stores tagged with:
156
        //  -   this Eloquent event, AND
157
        //  -   this Model class
158
        $cacheTags[] = new Condition($eventName, $modelName);
159
160
        foreach ($payload as $model) {
161
            if ( ! is_object($model) || ! is_subclass_of($model, Model::class)) {
162
                continue;
163
            }
164
            $modelId = $model->getKey();
165
            if ( ! empty($modelId)) {
166
                //  Create a tag to flush stores tagged with:
167
                //  -   this Eloquent event, AND
168
                //  -   this Model instance
169
                $cacheTags[] = new Condition($eventName, $modelName, $modelId);
170
            }
171
172
            //  @TODO:  Related models.
173
174
            //	Flush cache for related models.
175
            //		E.g.
176
            //			-	A ballot has user_id = 30
177
            //			-	Flush cache tagged "ManagedCache:forget:attach-ballot-user=30"
178
            $modelKeys = $this->extractModelKeys($model->getAttributes());
179
            foreach ($modelKeys as $relatedModelName => $relatedModelId) {
180
                //	Flush cached items that are tagged through a relation
181
                //	with this model.
182
                if ('delete' === $eventName) {
183
                    $relatedEventName = 'detach';
184
                } else {
185
                    $relatedEventName = 'attach.' . $eventName;
186
                }
187
                $cacheTags[] = new Condition($relatedEventName, $modelName, $modelId);
188
                $cacheTags[] = Condition::makeTag($relatedEventName, $modelName, null, $relatedModelName, $relatedModelId);
0 ignored issues
show
Bug introduced by
The method makeTag() does not exist on Codefocus\ManagedCache\Condition. ( Ignorable by Annotation )

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

188
                $cacheTags[] = Condition::/** @scrutinizer ignore-call */ makeTag($relatedEventName, $modelName, null, $relatedModelName, $relatedModelId);

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...
189
            }
190
        }
191
192
        //	Flush all stores with these tags
193
        $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

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