From c83e6d635dce1652b75a3fb3386e7cf9dffa8d8a Mon Sep 17 00:00:00 2001 From: Hasan Deeb Date: Fri, 27 Oct 2023 14:35:25 +0300 Subject: [PATCH] wip: support cache entry binding params --- README.md | 57 ++++++++++++++++++++++++- src/CacheFlusherManager.php | 64 +++++++++++++++++++++-------- src/Facades/CacheFlusher.php | 4 +- src/LaravelCacheFlusherProvider.php | 11 +++-- tests/Unit/CacheFlusherTest.php | 60 +++++++++++++++++++++------ 5 files changed, 156 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 6389beb..98ed831 100644 --- a/README.md +++ b/README.md @@ -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 ``` @@ -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. @@ -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). diff --git a/src/CacheFlusherManager.php b/src/CacheFlusherManager.php index b8244ca..09b5bf0 100644 --- a/src/CacheFlusherManager.php +++ b/src/CacheFlusherManager.php @@ -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 @@ -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 []; } @@ -64,20 +70,40 @@ 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) { @@ -85,31 +111,33 @@ public function handleSingleKey($keys, $patten): void } } - 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; + } } diff --git a/src/Facades/CacheFlusher.php b/src/Facades/CacheFlusher.php index bec6dfa..c1bb521 100644 --- a/src/Facades/CacheFlusher.php +++ b/src/Facades/CacheFlusher.php @@ -3,6 +3,7 @@ namespace Aldeebhasan\LaravelCacheFlusher\Facades; use Aldeebhasan\LaravelCacheFlusher\CacheFlusherManager; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Facade; /** @@ -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 diff --git a/src/LaravelCacheFlusherProvider.php b/src/LaravelCacheFlusherProvider.php index c9b4ce6..94c5421 100644 --- a/src/LaravelCacheFlusherProvider.php +++ b/src/LaravelCacheFlusherProvider.php @@ -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(); @@ -24,11 +24,11 @@ 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 @@ -36,9 +36,8 @@ 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); } } diff --git a/tests/Unit/CacheFlusherTest.php b/tests/Unit/CacheFlusherTest.php index d15ab54..76c7388 100644 --- a/tests/Unit/CacheFlusherTest.php +++ b/tests/Unit/CacheFlusherTest.php @@ -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; @@ -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 @@ -76,32 +78,32 @@ 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); } @@ -109,7 +111,7 @@ 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' ]); @@ -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' ]); @@ -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); } }