From a0bfb7d299e1fb5899c9489b0dd4632f61d84561 Mon Sep 17 00:00:00 2001 From: abdeltif Date: Mon, 6 Oct 2025 03:08:57 +0100 Subject: [PATCH 1/2] - Optimize settings retrieval with runtime and per-class config caching. - Add `clearSettingsCache` method to handle clearing runtime and Redis cache. - Update README with Redis request counting instructions. --- README.md | 12 ++++ src/Traits/HasSettingsTable.php | 98 ++++++++++++++++++++++++++------- 2 files changed, 91 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4957fa0..fbc9e1d 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,18 @@ class User extends Model } ``` +## Counting redis requests +``` +docker compose exec redis redis-cli monitor | grep -E '"(GET|SET|DEL)"' | grep "model_settings" | awk '{ + if ($0 ~ /"GET"/) get++; + else if ($0 ~ /"SET"/) set++; + else if ($0 ~ /"DEL"/) del++; + total++; + printf "\rGET: %d, SET: %d, DEL: %d, TOTAL: %d", get, set, del, total; +}' +``` + + ## Changelog Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. diff --git a/src/Traits/HasSettingsTable.php b/src/Traits/HasSettingsTable.php index 8cbeab6..d686e73 100644 --- a/src/Traits/HasSettingsTable.php +++ b/src/Traits/HasSettingsTable.php @@ -8,61 +8,121 @@ use Illuminate\Database\Eloquent\Relations\MorphOne; use Illuminate\Support\Facades\Cache; -/** - * Trait HasSettingsTable - * @package Glorand\Model\Settings\Traits - * @property ModelSettings $modelSettings - * @property array $settings - * @method morphOne($model, $name) - */ trait HasSettingsTable { use HasSettings; + protected $settingsRuntimeCache = null; + protected $settingsManagerInstance = null; + protected static $configCache = []; + /** - * @return \Glorand\Model\Settings\Contracts\SettingsManagerContract + * Returns a settings manager instance for the model, creating one if necessary. + * @return SettingsManagerContract * @throws \Glorand\Model\Settings\Exceptions\ModelSettingsException */ public function settings(): SettingsManagerContract { - return new TableSettingsManager($this); + if ($this->settingsManagerInstance === null) { + $this->settingsManagerInstance = new TableSettingsManager($this); + } + + return $this->settingsManagerInstance; } /** + * Retrieves the settings value for the model, optionally using a cache. * @return array */ public function getSettingsValue(): array { - if (config('model_settings.settings_table_use_cache')) { - return Cache::rememberForever($this->getSettingsCacheKey(), function () { - return $this->__getSettingsValue(); - }); + // Runtime cache = ZERO Redis calls after first access + if ($this->settingsRuntimeCache !== null) { + return $this->settingsRuntimeCache; + } + + // Cache config lookup once per class (not per instance) + if (!isset(static::$configCache['use_cache'])) { + static::$configCache['use_cache'] = config('model_settings.settings_table_use_cache'); + } + + if (static::$configCache['use_cache']) { + // Only 1 Redis call per request + $this->settingsRuntimeCache = Cache::rememberForever( + $this->getSettingsCacheKey(), + fn() => $this->__getSettingsValue() + ); + } else { + $this->settingsRuntimeCache = $this->__getSettingsValue(); } - return $this->__getSettingsValue(); + return $this->settingsRuntimeCache; } + /** + * Retrieves the settings value for the model. + * @return array + */ private function __getSettingsValue(): array { - if ($modelSettings = $this->modelSettings()->first()) { - return $modelSettings->settings; + // Eager loading support (prevents N+1) + if ($this->relationLoaded('modelSettings')) { + $modelSettings = $this->getRelation('modelSettings'); + return $modelSettings ? $modelSettings->settings : []; } - return []; + $modelSettings = $this->modelSettings() + ->select('settings', 'model_id', 'model_type') + ->first(); + + return $modelSettings ? $modelSettings->settings : []; } /** - * @return \Illuminate\Database\Eloquent\Relations\MorphOne + * Define an inverse one-to-one or many relationship. + * @return MorphOne */ public function modelSettings(): MorphOne { return $this->morphOne(ModelSettings::class, 'model'); } + /** + * Retrieves the cache key for settings associated with the model. + * + * This method ensures that a consistent cache key is generated for + * the model's settings by utilizing a shared static configuration + * for the cache prefix and combining it with the table name and + * primary key of the current model instance. + * + * @return string The generated cache key for the model's settings. + */ public function getSettingsCacheKey(): string { - return config('model_settings.settings_table_cache_prefix') . $this->getTable() . '::' . $this->getKey(); + // Static config cache shared across all instances + if (!isset(static::$configCache['cache_prefix'])) { + static::$configCache['cache_prefix'] = config('model_settings.settings_table_cache_prefix'); + } + + return static::$configCache['cache_prefix'] . $this->getTable() . '::' . $this->getKey(); + } + + /** + * Clears the runtime cache and optionally the cache for the model's settings.' + * @return void + */ + public function clearSettingsCache(): void + { + $this->settingsRuntimeCache = null; + + if (static::$configCache['use_cache'] ?? config('model_settings.settings_table_use_cache')) { + Cache::forget($this->getSettingsCacheKey()); + } } + /** + * Retrieves the name of the database table associated with the model. + * @return string + */ abstract public function getTable(); } From a7719868673a1ae2a0e47fa676b6d163f7d47146 Mon Sep 17 00:00:00 2001 From: abdeltif Date: Tue, 7 Oct 2025 03:24:37 +0100 Subject: [PATCH 2/2] - Optimize settings retrieval with runtime and per-class config caching. - Add `clearSettingsCache` method to handle clearing runtime and Redis cache. - Update README with Redis request counting instructions. --- src/Managers/TableSettingsManager.php | 41 ++++++- src/Traits/HasSettingsTable.php | 153 +++++++++++++------------- 2 files changed, 113 insertions(+), 81 deletions(-) diff --git a/src/Managers/TableSettingsManager.php b/src/Managers/TableSettingsManager.php index a036f41..4f3be30 100644 --- a/src/Managers/TableSettingsManager.php +++ b/src/Managers/TableSettingsManager.php @@ -4,6 +4,7 @@ use Glorand\Model\Settings\Contracts\SettingsManagerContract; use Glorand\Model\Settings\Models\ModelSettings; +use Illuminate\Support\Facades\Redis; /** * Class TableSettingsManager @@ -16,29 +17,57 @@ class TableSettingsManager extends AbstractSettingsManager * @param array $settings * @return \Glorand\Model\Settings\Contracts\SettingsManagerContract * @throws \Exception - * @SuppressWarnings(PHPMD.ElseExpression) */ public function apply(array $settings = []): SettingsManagerContract { $this->validate($settings); $modelSettings = $this->model->modelSettings()->first(); - if (!count($settings)) { + + if (! count($settings)) { if ($modelSettings) { - $modelSettings->delete(); + $modelSettings->delete(); // fires deleted event → cache flushed } } else { - if (!$modelSettings) { + if (! $modelSettings) { $modelSettings = new ModelSettings(); $modelSettings->setConnection($this->model->getConnectionName() ?? config('database.default')); $modelSettings->model()->associate($this->model); } + $modelSettings->settings = $settings; - $modelSettings->save(); + $modelSettings->save(); // fires saved event → cache flushed } + // Optional: warm Redis cache instantly via pipeline + $this->warmCache($settings); + + $this->model->flushSettingsCache(); + cache()->forget($this->model->getSettingsCacheKey()); return $this; } -} + + /* ----------------------------------------------------------------- + | Internal helpers + | ----------------------------------------------------------------- + */ + private function warmCache(array $settings): void + { + $cacheKey = $this->model->getSettingsCacheKey(); + $payload = $this->compress(json_encode($settings)); + + Redis::connection()->pipeline(function ($pipe) use ($cacheKey, $payload, $settings) { + $pipe->del($cacheKey); + if (count($settings)) { + $pipe->set($cacheKey, $payload); + } + }); + } + + private function compress(string $data): string + { + return gzencode($data, 6); + } +} \ No newline at end of file diff --git a/src/Traits/HasSettingsTable.php b/src/Traits/HasSettingsTable.php index d686e73..f49d479 100644 --- a/src/Traits/HasSettingsTable.php +++ b/src/Traits/HasSettingsTable.php @@ -8,121 +8,124 @@ use Illuminate\Database\Eloquent\Relations\MorphOne; use Illuminate\Support\Facades\Cache; +/** + * Trait HasSettingsTable + * @package Glorand\Model\Settings\Traits + * + * @property ModelSettings $modelSettings + * @property array $settings + * @method morphOne(string $related, string $name, string $type = null, string $id = null, string $localKey = null) + */ trait HasSettingsTable { use HasSettings; - protected $settingsRuntimeCache = null; - protected $settingsManagerInstance = null; - protected static $configCache = []; + /** @var SettingsManagerContract|null */ + private ?SettingsManagerContract $_settingsManager = null; - /** - * Returns a settings manager instance for the model, creating one if necessary. - * @return SettingsManagerContract - * @throws \Glorand\Model\Settings\Exceptions\ModelSettingsException + /** @var array|null In-memory copy */ + private ?array $_settingsCache = null; + + /* ----------------------------------------------------------------- + | Boot – register model event listeners + | ----------------------------------------------------------------- */ - public function settings(): SettingsManagerContract + public static function bootHasSettingsTable(): void { - if ($this->settingsManagerInstance === null) { - $this->settingsManagerInstance = new TableSettingsManager($this); - } + // When the polymorphic row changes, flush our caches + static::saved(function ($model) { + $model->flushSettingsCache(); + Cache::forget($model->getSettingsCacheKey()); + }); + + static::deleted(function ($model) { + $model->flushSettingsCache(); + Cache::forget($model->getSettingsCacheKey()); + }); + } - return $this->settingsManagerInstance; + /* ----------------------------------------------------------------- + | Settings Manager + | ----------------------------------------------------------------- + */ + public function settings(): SettingsManagerContract + { + return $this->_settingsManager ??= new TableSettingsManager($this); } - /** - * Retrieves the settings value for the model, optionally using a cache. - * @return array + /* ----------------------------------------------------------------- + | Settings Value (lazy + in-memory) + | ----------------------------------------------------------------- */ public function getSettingsValue(): array { - // Runtime cache = ZERO Redis calls after first access - if ($this->settingsRuntimeCache !== null) { - return $this->settingsRuntimeCache; - } - - // Cache config lookup once per class (not per instance) - if (!isset(static::$configCache['use_cache'])) { - static::$configCache['use_cache'] = config('model_settings.settings_table_use_cache'); + if ($this->_settingsCache !== null) { + return $this->_settingsCache; } - if (static::$configCache['use_cache']) { - // Only 1 Redis call per request - $this->settingsRuntimeCache = Cache::rememberForever( - $this->getSettingsCacheKey(), - fn() => $this->__getSettingsValue() - ); - } else { - $this->settingsRuntimeCache = $this->__getSettingsValue(); + if (! config('model_settings.settings_table_use_cache')) { + return $this->_settingsCache = $this->loadSettingsFromDatabase(); } - return $this->settingsRuntimeCache; + return $this->_settingsCache = Cache::rememberForever( + $this->getSettingsCacheKey(), + fn () => $this->loadSettingsFromDatabase() + ); } /** - * Retrieves the settings value for the model. - * @return array + * Reload settings and refresh all caches. */ - private function __getSettingsValue(): array + public function refreshSettings(): array { - // Eager loading support (prevents N+1) - if ($this->relationLoaded('modelSettings')) { - $modelSettings = $this->getRelation('modelSettings'); - return $modelSettings ? $modelSettings->settings : []; - } - - $modelSettings = $this->modelSettings() - ->select('settings', 'model_id', 'model_type') - ->first(); + $this->flushSettingsCache(); + Cache::forget($this->getSettingsCacheKey()); - return $modelSettings ? $modelSettings->settings : []; + return $this->getSettingsValue(); } /** - * Define an inverse one-to-one or many relationship. - * @return MorphOne + * Flush only the in-RAM copy. */ - public function modelSettings(): MorphOne + public function flushSettingsCache(): void { - return $this->morphOne(ModelSettings::class, 'model'); + $this->_settingsCache = null; } /** - * Retrieves the cache key for settings associated with the model. - * - * This method ensures that a consistent cache key is generated for - * the model's settings by utilizing a shared static configuration - * for the cache prefix and combining it with the table name and - * primary key of the current model instance. - * - * @return string The generated cache key for the model's settings. + * Raw DB hit (no caching layer). */ - public function getSettingsCacheKey(): string + private function loadSettingsFromDatabase(): array { - // Static config cache shared across all instances - if (!isset(static::$configCache['cache_prefix'])) { - static::$configCache['cache_prefix'] = config('model_settings.settings_table_cache_prefix'); - } + /** @var ModelSettings|null $row */ + $row = $this->modelSettings()->first(['settings']); - return static::$configCache['cache_prefix'] . $this->getTable() . '::' . $this->getKey(); + return $row?->settings ?? []; } - /** - * Clears the runtime cache and optionally the cache for the model's settings.' - * @return void + /* ----------------------------------------------------------------- + | Relations + | ----------------------------------------------------------------- */ - public function clearSettingsCache(): void + public function modelSettings(): MorphOne { - $this->settingsRuntimeCache = null; + return $this->morphOne(ModelSettings::class, 'model'); + } - if (static::$configCache['use_cache'] ?? config('model_settings.settings_table_use_cache')) { - Cache::forget($this->getSettingsCacheKey()); - } + + /* ----------------------------------------------------------------- + | Helpers + | ----------------------------------------------------------------- + */ + public function getSettingsCacheKey(): string + { + return config('model_settings.settings_table_cache_prefix') + . $this->getTable() . '::' . $this->getKey(); } - /** - * Retrieves the name of the database table associated with the model. - * @return string + /* ----------------------------------------------------------------- + | Abstract requirements + | ----------------------------------------------------------------- */ abstract public function getTable(); -} +} \ No newline at end of file