Skip to content

Commit

Permalink
Merge pull request #22 from renoki-co/feature/invalidate-cache-on-mod…
Browse files Browse the repository at this point in the history
…el-events

[feature] Automatic cache flushing
  • Loading branch information
rennokki authored Apr 27, 2020
2 parents 6f7d5ed + 9e28bea commit 569831f
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 0 deletions.
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,80 @@ class Kid extends Model
Kid::flushQueryCache();
```

## Full Automatic Invalidation

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.

```php
class Page extends Model
{
use QueryCacheable;

/**
* Invalidate the cache automatically
* upon update in the database.
*/
protected static $flushCacheOnUpdate = true;
}
```

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.

> 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.
## Partial Automatic Invalidation

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.

To do this, overwrite your `getCacheTagsToInvalidateOnUpdate()` method in your model:

```php
class Page extends Model
{
use QueryCacheable;

/**
* Invalidate the cache automatically
* upon update in the database.
*/
protected static $flushCacheOnUpdate = true;

/**
* When invalidating automatically on update, you can specify
* which tags to invalidate.
*
* @return array
*/
public function getCacheTagsToInvalidateOnUpdate(): array
{
return [
'query1',
];
}
}

$query1 = Page::cacheFor(60)
->cacheTags(['query1'])
->get();

$query2 = Page::cacheFor(60)
->cacheTags(['query2'])
->get();

// The $query1 gets invalidated
// but $query2 will still hit from cache if re-called.

$page = Page::first();

$page->update([
'name' => 'Reddit',
]);
```

**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.**

**Not specifying a tag to invalidate fallbacks to the set of base tags, thus leading to Full Automatic Invalidation.**

## Relationship Caching

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.
Expand Down
19 changes: 19 additions & 0 deletions database/factories/PageFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php
/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| This directory should contain each of the model factory definitions for
| your application. Factories provide a convenient way to generate new
| model instances for testing / seeding your application's database.
|
*/

use Illuminate\Support\Str;

$factory->define(\Rennokki\QueryCache\Test\Models\Page::class, function () {
return [
'name' => 'Page'.Str::random(5),
];
});
83 changes: 83 additions & 0 deletions src/FlushQueryCacheObserver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace Rennokki\QueryCache;

use Illuminate\Database\Eloquent\Model;

class FlushQueryCacheObserver
{
/**
* Handle the Model "created" event.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function created(Model $model)
{
$this->invalidateCache($model);
}

/**
* Handle the Model "updated" event.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function updated(Model $model)
{
$this->invalidateCache($model);
}

/**
* Handle the Model "deleted" event.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function deleted(Model $model)
{
$this->invalidateCache($model);
}

/**
* Handle the Model "forceDeleted" event.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function forceDeleted(Model $model)
{
$this->invalidateCache($model);
}

/**
* Handle the Model "restored" event.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
*/
public function restored(Model $model)
{
$this->invalidateCache($model);
}

/**
* Invalidate the cache for a model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return void
* @throws Exception
*/
protected function invalidateCache(Model $model): void
{
if (! $model->getCacheTagsToInvalidateOnUpdate()) {
throw new Exception('Automatic invalidation for '.$class.' works only if at least one tag to be invalidated is specified.');
}

$class = get_class($model);

$class::flushQueryCache(
$model->getCacheTagsToInvalidateOnUpdate()
);
}
}
38 changes: 38 additions & 0 deletions src/Traits/QueryCacheable.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,48 @@

namespace Rennokki\QueryCache\Traits;

use Rennokki\QueryCache\FlushQueryCacheObserver;
use Rennokki\QueryCache\Query\Builder;

trait QueryCacheable
{
/**
* Get the observer class name that will
* observe the changes and will invalidate the cache
* upon database change.
*
* @return string
*/
protected static function getFlushQueryCacheObserver()
{
return FlushQueryCacheObserver::class;
}

/**
* When invalidating automatically on update, you can specify
* which tags to invalidate.
*
* @return array
*/
public function getCacheTagsToInvalidateOnUpdate(): array
{
return $this->getCacheBaseTags();
}

/**
* {@inheritdoc}
*/
public static function boot()
{
parent::boot();

if (isset(static::$flushCacheOnUpdate) && static::$flushCacheOnUpdate) {
static::observe(
static::getFlushQueryCacheObserver()
);
}
}

/**
* {@inheritdoc}
*/
Expand Down
93 changes: 93 additions & 0 deletions tests/FlushCacheOnUpdateTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace Rennokki\QueryCache\Test;

use Cache;
use Rennokki\QueryCache\Test\Models\Page;

class FlushCacheOnUpdateTest extends TestCase
{
public function test_flush_cache_on_create()
{
$page = factory(Page::class)->create();
$storedPage = Page::cacheFor(now()->addHours(1))->first();
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');

$this->assertNotNull($cache);

$this->assertEquals(
$cache->first()->id,
$storedPage->id
);

Page::create([
'name' => '9GAG',
]);

$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');

$this->assertNull($cache);
}

public function test_flush_cache_on_update()
{
$page = factory(Page::class)->create();
$storedPage = Page::cacheFor(now()->addHours(1))->first();
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');

$this->assertNotNull($cache);

$this->assertEquals(
$cache->first()->id,
$storedPage->id
);

$page->update([
'name' => '9GAG',
]);

$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');

$this->assertNull($cache);
}

public function test_flush_cache_on_delete()
{
$page = factory(Page::class)->create();
$storedPage = Page::cacheFor(now()->addHours(1))->first();
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');

$this->assertNotNull($cache);

$this->assertEquals(
$cache->first()->id,
$storedPage->id
);

$page->delete();

$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');

$this->assertNull($cache);
}

public function test_flush_cache_on_force_deletion()
{
$page = factory(Page::class)->create();
$storedPage = Page::cacheFor(now()->addHours(1))->first();
$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');

$this->assertNotNull($cache);

$this->assertEquals(
$cache->first()->id,
$storedPage->id
);

$page->forceDelete();

$cache = Cache::tags(['test'])->get('leqc:sqlitegetselect * from "pages" limit 1a:0:{}');

$this->assertNull($cache);
}
}
26 changes: 26 additions & 0 deletions tests/Models/Page.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Rennokki\QueryCache\Test\Models;

use Illuminate\Database\Eloquent\Model;
use Rennokki\QueryCache\Traits\QueryCacheable;

class Page extends Model
{
use QueryCacheable;

protected static $flushCacheOnUpdate = true;

protected $cacheUsePlainKey = true;

protected $fillable = [
'name',
];

protected function getCacheBaseTags(): array
{
return [
'test',
];
}
}
1 change: 1 addition & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public function getEnvironmentSetUp($app)
$app['config']->set('auth.providers.posts.model', Post::class);
$app['config']->set('auth.providers.kids.model', Kid::class);
$app['config']->set('auth.providers.books.model', Book::class);
$app['config']->set('auth.providers.pages.model', Page::class);
$app['config']->set('app.key', 'wslxrEFGWY6GfGhvN9L3wH3KSRJQQpBD');
}

Expand Down
32 changes: 32 additions & 0 deletions tests/database/migrations/2018_07_14_183253_pages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class Pages extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('pages', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('pages');
}
}

0 comments on commit 569831f

Please sign in to comment.