Skip to content

Commit 569831f

Browse files
authored
Merge pull request #22 from renoki-co/feature/invalidate-cache-on-model-events
[feature] Automatic cache flushing
2 parents 6f7d5ed + 9e28bea commit 569831f

File tree

8 files changed

+366
-0
lines changed

8 files changed

+366
-0
lines changed

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,80 @@ class Kid extends Model
135135
Kid::flushQueryCache();
136136
```
137137

138+
## Full Automatic Invalidation
139+
140+
To speed up the scaffolding of invalidation within your app, you can specify the model to auto-flush the cache upon any time records gets created, updated or deleted.
141+
142+
```php
143+
class Page extends Model
144+
{
145+
use QueryCacheable;
146+
147+
/**
148+
* Invalidate the cache automatically
149+
* upon update in the database.
150+
*/
151+
protected static $flushCacheOnUpdate = true;
152+
}
153+
```
154+
155+
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.
156+
157+
> 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.
158+
159+
## Partial Automatic Invalidation
160+
161+
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.
162+
163+
To do this, overwrite your `getCacheTagsToInvalidateOnUpdate()` method in your model:
164+
165+
```php
166+
class Page extends Model
167+
{
168+
use QueryCacheable;
169+
170+
/**
171+
* Invalidate the cache automatically
172+
* upon update in the database.
173+
*/
174+
protected static $flushCacheOnUpdate = true;
175+
176+
/**
177+
* When invalidating automatically on update, you can specify
178+
* which tags to invalidate.
179+
*
180+
* @return array
181+
*/
182+
public function getCacheTagsToInvalidateOnUpdate(): array
183+
{
184+
return [
185+
'query1',
186+
];
187+
}
188+
}
189+
190+
$query1 = Page::cacheFor(60)
191+
->cacheTags(['query1'])
192+
->get();
193+
194+
$query2 = Page::cacheFor(60)
195+
->cacheTags(['query2'])
196+
->get();
197+
198+
// The $query1 gets invalidated
199+
// but $query2 will still hit from cache if re-called.
200+
201+
$page = Page::first();
202+
203+
$page->update([
204+
'name' => 'Reddit',
205+
]);
206+
```
207+
208+
**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.**
209+
210+
**Not specifying a tag to invalidate fallbacks to the set of base tags, thus leading to Full Automatic Invalidation.**
211+
138212
## Relationship Caching
139213

140214
Relationships are just another queries. They can be intercepted and modified before the database is hit with the query. The following example needs the `Order` model (or the model associated with the `orders` relationship) to include the `QueryCacheable` trait.

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: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,48 @@
22

33
namespace Rennokki\QueryCache\Traits;
44

5+
use Rennokki\QueryCache\FlushQueryCacheObserver;
56
use Rennokki\QueryCache\Query\Builder;
67

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

tests/FlushCacheOnUpdateTest.php

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

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)