Skip to content

Commit 5e4b961

Browse files
committed
Added automatic cache invalidation
1 parent 6f7d5ed commit 5e4b961

File tree

8 files changed

+369
-0
lines changed

8 files changed

+369
-0
lines changed

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,80 @@ $user = User::with(['orders' => function ($query) {
150150
$orders = $user->orders;
151151
```
152152

153+
## Full Automatic Invalidation
154+
155+
To speed up the scaffolding of invalidation within your app, you can specify the model to auto-flush the cache upon any model gets created, updated or deleted.
156+
157+
```php
158+
class Page extends Model
159+
{
160+
use QueryCacheable;
161+
162+
/**
163+
* Invalidate the cache automatically
164+
* upon update in the database.
165+
*/
166+
protected static $flushCacheOnUpdate = true;
167+
}
168+
```
169+
170+
When you set up the `$flushCacheOnUpdate` variable, the package attaches an observer to your model, and any `created`, `updated`, `deleted`, `forceDeleted` or `restored` event will trigger the cache invalidation.
171+
172+
> In order for auto-flush to work, you will need at least one **base tag**. Out-of-the-box, the model has a base tag set. In some cases, if you have overwritten the `getCacheBaseTags()` with an empty array, it might not work.
173+
174+
## Partial Automatic Invalidation
175+
176+
In some cases, you might not want to invalidate the whole cache of a specific model. Perhaps you got two queries that run individually and want to invalidate the cache only for one of them.
177+
178+
To do this, overwrite your `getCacheTagsToInvalidateOnUpdate()` method in your model:
179+
180+
```php
181+
class Page extends Model
182+
{
183+
use QueryCacheable;
184+
185+
/**
186+
* Invalidate the cache automatically
187+
* upon update in the database.
188+
*/
189+
protected static $flushCacheOnUpdate = true;
190+
191+
/**
192+
* When invalidating automatically on update, you can specify
193+
* which tags to invalidate.
194+
*
195+
* @return array
196+
*/
197+
public function getCacheTagsToInvalidateOnUpdate(): array
198+
{
199+
return [
200+
'query1',
201+
];
202+
}
203+
}
204+
205+
$query1 = Page::cacheFor(60)
206+
->cacheTags(['query1'])
207+
->get();
208+
209+
$query2 = Page::cacheFor(60)
210+
->cacheTags(['query2'])
211+
->get();
212+
213+
// The $query1 gets invalidated
214+
// but $query2 will still hit from cache if re-called.
215+
216+
$page = Page::first();
217+
218+
$page->update([
219+
'name' => 'Reddit',
220+
]);
221+
```
222+
223+
**Please keep in mind: Setting `$flushCacheOnUpdate` to `true` and not specifying individual tags to invalidate will lead to [Full Automatic Invalidation](#full-automatic-invalidation) since the default tags to invalidate are the base tags and you need at least one tag to invalidate.**
224+
225+
**Not specifying a tag to invalidate fallbacks to the set of base tags, thus leading to Full Automatic Invalidation.**
226+
153227
## Cache Keys
154228

155229
The package automatically generate the keys needed to store the data in the cache store. However, prefixing them might be useful if the cache store is used by other applications and/or models and you want to manage the keys better to avoid collisions.

database/factories/PageFactory.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
/*
3+
|--------------------------------------------------------------------------
4+
| Model Factories
5+
|--------------------------------------------------------------------------
6+
|
7+
| This directory should contain each of the model factory definitions for
8+
| your application. Factories provide a convenient way to generate new
9+
| model instances for testing / seeding your application's database.
10+
|
11+
*/
12+
13+
use Illuminate\Support\Str;
14+
15+
$factory->define(\Rennokki\QueryCache\Test\Models\Page::class, function () {
16+
return [
17+
'name' => 'Page'.Str::random(5),
18+
];
19+
});

src/FlushQueryCacheObserver.php

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
namespace Rennokki\QueryCache;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class FlushQueryCacheObserver
8+
{
9+
/**
10+
* Handle the Model "created" event.
11+
*
12+
* @param \Illuminate\Database\Eloquent\Model $model
13+
* @return void
14+
*/
15+
public function created(Model $model)
16+
{
17+
$this->invalidateCache($model);
18+
}
19+
20+
/**
21+
* Handle the Model "updated" event.
22+
*
23+
* @param \Illuminate\Database\Eloquent\Model $model
24+
* @return void
25+
*/
26+
public function updated(Model $model)
27+
{
28+
$this->invalidateCache($model);
29+
}
30+
31+
/**
32+
* Handle the Model "deleted" event.
33+
*
34+
* @param \Illuminate\Database\Eloquent\Model $model
35+
* @return void
36+
*/
37+
public function deleted(Model $model)
38+
{
39+
$this->invalidateCache($model);
40+
}
41+
42+
/**
43+
* Handle the Model "forceDeleted" event.
44+
*
45+
* @param \Illuminate\Database\Eloquent\Model $model
46+
* @return void
47+
*/
48+
public function forceDeleted(Model $model)
49+
{
50+
$this->invalidateCache($model);
51+
}
52+
53+
/**
54+
* Handle the Model "restored" event.
55+
*
56+
* @param \Illuminate\Database\Eloquent\Model $model
57+
* @return void
58+
*/
59+
public function restored(Model $model)
60+
{
61+
$this->invalidateCache($model);
62+
}
63+
64+
/**
65+
* Invalidate the cache for a model.
66+
*
67+
* @param \Illuminate\Database\Eloquent\Model $model
68+
* @return void
69+
* @throws Exception
70+
*/
71+
protected function invalidateCache(Model $model): void
72+
{
73+
if (! $model->getCacheTagsToInvalidateOnUpdate()) {
74+
throw new Exception('Automatic invalidation for '.$class.' works only if at least one tag to be invalidated is specified.');
75+
}
76+
77+
$class = get_class($model);
78+
79+
$class::flushQueryCache(
80+
$model->getCacheTagsToInvalidateOnUpdate()
81+
);
82+
}
83+
}

src/Traits/QueryCacheable.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,50 @@
22

33
namespace Rennokki\QueryCache\Traits;
44

5+
use Illuminate\Database\Eloquent\Model;
6+
use Rennokki\QueryCache\FlushQueryCacheObserver;
57
use Rennokki\QueryCache\Query\Builder;
68

79
trait QueryCacheable
810
{
11+
/**
12+
* Get the observer class name that will
13+
* observe the changes and will invalidate the cache
14+
* upon database change.
15+
*
16+
* @return string
17+
*/
18+
protected static function getFlushQueryCacheObserver()
19+
{
20+
return FlushQueryCacheObserver::class;
21+
}
22+
23+
/**
24+
* When invalidating automatically on update, you can specify
25+
* which tags to invalidate.
26+
*
27+
* @return array
28+
*/
29+
public function getCacheTagsToInvalidateOnUpdate(): array
30+
{
31+
return $this->getCacheBaseTags();
32+
}
33+
34+
/**
35+
* {@inheritdoc}
36+
*
37+
*/
38+
public static function boot()
39+
{
40+
parent::boot();
41+
42+
if (isset(static::$flushCacheOnUpdate) && static::$flushCacheOnUpdate) {
43+
static::observe(
44+
static::getFlushQueryCacheObserver()
45+
);
46+
}
47+
}
48+
949
/**
1050
* {@inheritdoc}
1151
*/

tests/FlushCacheOnUpdateTest.php

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace Rennokki\QueryCache\Test;
4+
5+
use Cache;
6+
use Rennokki\QueryCache\Test\Models\Page;
7+
use Rennokki\QueryCache\Test\Models\Post;
8+
9+
class FlushCacheOnUpdateTest extends TestCase
10+
{
11+
public function test_flush_cache_on_create()
12+
{
13+
$page = factory(Page::class)->create();
14+
$storedPage = Page::cacheFor(now()->addHours(1))->first();
15+
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');
16+
17+
$this->assertNotNull($cache);
18+
19+
$this->assertEquals(
20+
$cache->first()->id,
21+
$storedPage->id
22+
);
23+
24+
Page::create([
25+
'name' => '9GAG',
26+
]);
27+
28+
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');
29+
30+
$this->assertNull($cache);
31+
}
32+
33+
public function test_flush_cache_on_update()
34+
{
35+
$page = factory(Page::class)->create();
36+
$storedPage = Page::cacheFor(now()->addHours(1))->first();
37+
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');
38+
39+
$this->assertNotNull($cache);
40+
41+
$this->assertEquals(
42+
$cache->first()->id,
43+
$storedPage->id
44+
);
45+
46+
$page->update([
47+
'name' => '9GAG',
48+
]);
49+
50+
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');
51+
52+
$this->assertNull($cache);
53+
}
54+
55+
public function test_flush_cache_on_delete()
56+
{
57+
$page = factory(Page::class)->create();
58+
$storedPage = Page::cacheFor(now()->addHours(1))->first();
59+
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');
60+
61+
$this->assertNotNull($cache);
62+
63+
$this->assertEquals(
64+
$cache->first()->id,
65+
$storedPage->id
66+
);
67+
68+
$page->delete();
69+
70+
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');
71+
72+
$this->assertNull($cache);
73+
}
74+
75+
public function test_flush_cache_on_force_deletion()
76+
{
77+
$page = factory(Page::class)->create();
78+
$storedPage = Page::cacheFor(now()->addHours(1))->first();
79+
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');
80+
81+
$this->assertNotNull($cache);
82+
83+
$this->assertEquals(
84+
$cache->first()->id,
85+
$storedPage->id
86+
);
87+
88+
$page->forceDelete();
89+
90+
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');
91+
92+
$this->assertNull($cache);
93+
}
94+
}

tests/Models/Page.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace Rennokki\QueryCache\Test\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Rennokki\QueryCache\Traits\QueryCacheable;
7+
8+
class Page extends Model
9+
{
10+
use QueryCacheable;
11+
12+
protected static $flushCacheOnUpdate = true;
13+
14+
protected $cacheUsePlainKey = true;
15+
16+
protected $fillable = [
17+
'name',
18+
];
19+
20+
protected function getCacheBaseTags(): array
21+
{
22+
return [
23+
'test',
24+
];
25+
}
26+
}

tests/TestCase.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ public function getEnvironmentSetUp($app)
5757
$app['config']->set('auth.providers.posts.model', Post::class);
5858
$app['config']->set('auth.providers.kids.model', Kid::class);
5959
$app['config']->set('auth.providers.books.model', Book::class);
60+
$app['config']->set('auth.providers.pages.model', Page::class);
6061
$app['config']->set('app.key', 'wslxrEFGWY6GfGhvN9L3wH3KSRJQQpBD');
6162
}
6263

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
class Pages extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
Schema::create('pages', function (Blueprint $table) {
17+
$table->increments('id');
18+
$table->string('name');
19+
$table->timestamps();
20+
});
21+
}
22+
23+
/**
24+
* Reverse the migrations.
25+
*
26+
* @return void
27+
*/
28+
public function down()
29+
{
30+
Schema::dropIfExists('pages');
31+
}
32+
}

0 commit comments

Comments
 (0)