Skip to content

Commit

Permalink
wip: support cache entry binding params
Browse files Browse the repository at this point in the history
  • Loading branch information
Hasan Deeb committed Oct 27, 2023
1 parent 3703821 commit c83e6d6
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 40 deletions.
57 changes: 55 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ Install using composer:
composer require aldeebhasan/laravel-cache-flusher
```

After installation run the following code to publish the config:
After installation run the following code to publish the config:

```
php artisan vendor:publish --tag=cache-flusher
```
Expand All @@ -43,7 +44,7 @@ in your .env file.
### 2) driver (default : your default cache driver)

It will be used to specify the cache driver the package will work with,
it is preferable to use the cache driver similar to the one you used in your project
it is preferable to use the cache driver similar to the one you used in your project
(to make sure that all the operations done on the same cache driver).
you can control its value using the entry `CACHE_FLUSHER_DRIVER`
in your .env file.
Expand Down Expand Up @@ -97,6 +98,58 @@ if Product, Category, or Attribute changed (create|update|delete)
]
```

## Advanced Usage

Some time you may want to provide some condition when invalidating the cache keys.
One examples: you have a multi tenant project and you want to invalidate the cache entries of a specific company.

The package can help you with that by providing a binding resolving within the defined `mapping` config param.
One last thing, you have to provide a binding function (in your service provider) to extract the related values
from the model that trigger the invalidation operation.

Example:

```php
// define the binding mapping function
class AppServiceProvider extends ServiceProvider
{

public function boot()
{
//.... other boot methods

CacheFlusher::setBindingFunction(
function (string $bindingKey, Model $model): ?string {
switch ($bindingKey) {
case "company_id":
return '1'; //$model->company_id
case "user_id":
return "2"; // $model->user_id;
}
return null;
});
}
}
```

The binding function accept two params: `$bindingKey` which represent the matched binding key.
`$model` is the model who triggered the invalidation operation. The defined function will be called
for each matched param in the defined cache entry patterns (`mapping` in config file)

Now, according to the following mapping configuration :

```php
'mapping' => [
'^companies\.{company_id}\.stores' => [User::class],
'^companies\.{company_id}\.mobiles\.{user_id}' => [User::class],
]
```

When the `User` model changed, all the entries that start with
`companies.1.stores` and `companies.1.mobiles.2` will be invalidated.

NOTE: {company_id} is fixed to 1 in the binding function, {user_id} is fixed to 2 in the binding function.

## License

Laravel Cache Flusher package is licensed under [The MIT License (MIT)](LICENSE).
Expand Down
64 changes: 46 additions & 18 deletions src/CacheFlusherManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
namespace Aldeebhasan\LaravelCacheFlusher;

use Illuminate\Contracts\Cache\Repository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;

class CacheFlusherManager
{
private array $mapping = [];

private $bindingFn = null;

private Repository $cacheManager;

private string $file = 'cache-flusher/cache.json';

public function initialize(): void
Expand All @@ -24,13 +28,15 @@ public function enabled(): bool
return config('cache-flusher.enabled', false);
}

public function getKeys(): array
private function getKeys(): array
{
$storage = Storage::disk('local');
if ($storage->exists($this->file)) {
$data = $storage->get($this->file);

return json_decode($data) ?? [];
}

return [];
}

Expand Down Expand Up @@ -64,52 +70,74 @@ public function flush(): void
$this->saveKeys([]);
}

public function process(string $model): void
public function process(Model $model): void
{
if ($this->needToCoolDown($model)) return;
$modelClassName = strtolower(class_basename($model));
$modelClass = get_class($model);
if ($this->needToCoolDown($modelClassName)) return;

$keys = $this->getKeys();
foreach ($this->mapping as $key => $map) {
if (in_array($model, $map)) {
if (in_array($modelClass, $map)) {
$key = $this->handleWithBinding($key, $model);
$this->handleSingleKey($keys, $key);
$this->configureCoolDown($model);
$this->configureCoolDown($modelClassName);
}
}
}

public function handleSingleKey($keys, $patten): void
private function handleWithBinding(string $patten, Model $model): string
{
if (!$this->bindingFn || !is_callable($this->bindingFn)) return $patten;

$matches = [];
preg_match_all("/{\w+}/", $patten, $matches);
foreach (array_shift($matches) as $bindKey) {
$key = str_replace(['{', '}'], '', $bindKey);
$bindingValue = call_user_func($this->bindingFn, $key, $model);
if ($bindingValue)
$patten = str_replace($bindKey, $bindingValue, $patten);
}

return $patten;

}

public function handleSingleKey(array $keys, string $patten): void
{
$matches = preg_grep("/^$patten/i", $keys);
foreach ($matches as $key) {
$this->cacheManager->forget($key);
}
}

private function needToCoolDown($model): bool
private function needToCoolDown(string $modelClassName): bool
{
$modelClassName = last(explode('\\', $model));
$modelClassName = strtolower($modelClassName);
$cacheCooldown = config('cache-flusher.cool-down');
$cacheCoolDown = config('cache-flusher.cool-down');

if (!$cacheCooldown) return false;
if (!$cacheCoolDown) return false;
$invalidatedAt = $this->cacheManager->get("$modelClassName-cooldown");

if (!$invalidatedAt) return false;
return now()->diffInSeconds($invalidatedAt) < $cacheCooldown;

return now()->diffInSeconds($invalidatedAt) < $cacheCoolDown;

}

private function configureCoolDown($model): void
private function configureCoolDown(string $modelClassName): void
{
$modelClassName = last(explode('\\', $model));
$modelClassName = strtolower($modelClassName);
$cacheCooldown = config('cache-flusher.cool-down');
if (!$cacheCooldown) return;
$cacheCoolDown = config('cache-flusher.cool-down');
if (!$cacheCoolDown) return;

$this->cacheManager->put(
"$modelClassName-cooldown",
now()->addSeconds($cacheCooldown)->toDateTimeString()
now()->addSeconds($cacheCoolDown)->toDateTimeString()
);

}

public function setBindingFunction(\Closure $closure): void
{
$this->bindingFn = $closure;
}
}
4 changes: 3 additions & 1 deletion src/Facades/CacheFlusher.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Aldeebhasan\LaravelCacheFlusher\Facades;

use Aldeebhasan\LaravelCacheFlusher\CacheFlusherManager;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Facade;

/**
Expand All @@ -11,7 +12,8 @@
* @method static void put(string $key)
* @method static void forget(string|int|array $forgetKey)
* @method static void flush()
* @method static void process(string $model)
* @method static void process(Model $model)
* @method static void setBindingFunction(\Closure $closure)
* @see CacheFlusherManager
*/
class CacheFlusher extends Facade
Expand Down
11 changes: 5 additions & 6 deletions src/LaravelCacheFlusherProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class LaravelCacheFlusherProvider extends ServiceProvider
public function boot()
{
$this->publishes([
__DIR__ . '/../config/cache-flusher.php' => config_path('cache-flusher.php'),
__DIR__.'/../config/cache-flusher.php' => config_path('cache-flusher.php'),
], 'cache-flusher');

$this->registerCacheFlusher();
Expand All @@ -24,21 +24,20 @@ public function register()
{

$this->mergeConfigFrom(
__DIR__ . '/../config/cache-flusher.php',
__DIR__.'/../config/cache-flusher.php',
'cache-flusher-config'
);

$this->app->singleton('cache-flusher', CacheFlusherManager::class,);
$this->app->singleton('cache-flusher', CacheFlusherManager::class, );
}

private function registerCacheFlusher(): void
{
if (CacheFlusher::enabled()) {
Event::listen(
['eloquent.updated: *', 'eloquent.created: *', 'eloquent.deleted: *', 'eloquent.saved: *'],
function (string $event,$model) {
$model = last(explode(': ', $event));
if (class_exists($model)) {
function (string $event, $models) {
if ($model = last($models)) {
CacheFlusher::process($model);
}
}
Expand Down
60 changes: 47 additions & 13 deletions tests/Unit/CacheFlusherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Aldeebhasan\LaravelCacheFlusher\Test\TestCase;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Foundation\Auth\User;
use Illuminate\Foundation\Testing\WithFaker;
use Mockery\MockInterface;

Expand All @@ -28,6 +29,7 @@ private function setCustomConfig(array $config): void
config()->set('cache-flusher', $config);
CacheFlusher::initialize();
CacheFlusher::flush();
$this->cacheManager->clear();
}

private function initKeys(array $keys): void
Expand Down Expand Up @@ -76,40 +78,40 @@ public function test_flush_keys_after_model_create()
{
$this->setCustomConfig([
'mapping' => [
'(test\..+|test_2\.v1\.*)' => [Model::class],
'(test\..+|test_2\.v1\.*)' => [User::class],
]
]);

$keys = ['test', 'test.1', 'test.2', 'test_2.v1.1', 'test_2.v2.1'];
$this->initKeys($keys);

event('eloquent.created: ' . Model::class, Model::class);
event('eloquent.created: ' . User::class, new User);

$storeKeys = $this->getStoredKeys();
self::assertEquals(['test', 'test_2.v2.1'], $storeKeys);
}

public function test_check_processes_of__multi_model_create()
public function test_check_processes_of_multi_model_create()
{
$this->setCustomConfig([
'mapping' => [
'(test\..+|test_2\.v1\.*)' => [Model::class],
'(test\..+|test_2\.v1\.*)' => [User::class],
],
'cool-down' => '5'
]);


CacheFlusher::shouldReceive('process')->twice();
event('eloquent.created: ' . Model::class, Model::class);
event('eloquent.created: ' . Model::class, Model::class);
event('eloquent.created: ' . User::class, new User);
event('eloquent.created: ' . User::class, new User);

}

public function test_cool_down_after_multi_model_create()
{
$this->setCustomConfig([
'mapping' => [
'(test\..+|test_2\.v1\.*)' => [Model::class],
'(test\..+|test_2\.v1\.*)' => [User::class],
],
'cool-down' => '5'
]);
Expand All @@ -120,15 +122,15 @@ function (MockInterface $mock) {
$mock->shouldReceive('handleSingleKey')->once();
});
$mock->initialize();
$mock->process(Model::class);
$mock->process(Model::class);
$mock->process(new User);
$mock->process(new User);
}

public function test_cool_down_flush()
{
$this->setCustomConfig([
'mapping' => [
'(test\..+|test_2\.v1\.*)' => [Model::class],
'(test\..+|test_2\.v1\.*)' => [User::class],
],
'cool-down' => '1'
]);
Expand All @@ -139,11 +141,43 @@ function (MockInterface $mock) {
$mock->shouldReceive('handleSingleKey')->twice();
});
$mock->initialize();
$mock->process(Model::class);
$mock->process(Model::class);
$mock->process(new User);
$mock->process(new User);

sleep(2);
$mock->process(Model::class);
$mock->process(new User);
}

public function test_auto_binding()
{

$this->setCustomConfig([
'mapping' => [
'^companies\.{company_id}\.stores' => [User::class],
'^companies\.{company_id}\.mobiles\.{user_id}' => [User::class],
]
]);
CacheFlusher::setBindingFunction(
function (string $bindingKey, Model $model): ?string {
switch ($bindingKey) {
case "company_id":
return '1'; //$model->company_id
case "user_id":
if ($model instanceof User)
return "2"; // $model->user_id;
break;
}
return null;
});

$keys = ['companies.1.stores', 'companies.2.stores', 'companies.1.mobiles', 'companies.1.mobiles.2.products'];
$this->initKeys($keys);

$user = tap(new User(), fn($user) => $user->company_id = 1);
event('eloquent.created: ' . User::class, $user);

$storeKeys = $this->getStoredKeys();
self::assertEquals(['companies.2.stores', 'companies.1.mobiles'], $storeKeys);
}

}

0 comments on commit c83e6d6

Please sign in to comment.