diff --git a/database/factories/SettingFactory.php b/database/factories/SettingFactory.php new file mode 100644 index 00000000..69db050a --- /dev/null +++ b/database/factories/SettingFactory.php @@ -0,0 +1,27 @@ + + */ + protected $model = Setting::class; + + /** + * Define the model's default state. + */ + public function definition(): array + { + return [ + 'key' => $this->faker->slug(1), + 'value' => mt_rand(10, 1000), + ]; + } +} diff --git a/database/migrations/2024_09_02_111321_create_root_settings_table.php b/database/migrations/2024_09_02_111321_create_root_settings_table.php new file mode 100644 index 00000000..407de160 --- /dev/null +++ b/database/migrations/2024_09_02_111321_create_root_settings_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('key')->unique(); + $table->text('value')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('root_settings'); + } +}; diff --git a/src/Interfaces/Models/Setting.php b/src/Interfaces/Models/Setting.php new file mode 100644 index 00000000..32b2fc80 --- /dev/null +++ b/src/Interfaces/Models/Setting.php @@ -0,0 +1,8 @@ + + */ + protected $fillable = [ + 'key', + 'value', + ]; + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'root_settings'; + + /** + * Get the proxied interface. + */ + public static function getProxiedInterface(): string + { + return Contract::class; + } + + /** + * Create a new factory instance for the model. + */ + protected static function newFactory(): SettingFactory + { + return SettingFactory::new(); + } + + /** + * Cast the value attribute to the given type. + */ + public function castValue(?string $type = null): static + { + if (! is_null($type)) { + $this->casts['value'] = $type; + } else { + unset($this->casts['value']); + } + + return $this; + } +} diff --git a/src/Root.php b/src/Root.php index ec78beb6..289ad3af 100644 --- a/src/Root.php +++ b/src/Root.php @@ -5,6 +5,7 @@ use Closure; use Cone\Root\Interfaces\Breadcrumbs\Registry as Breadcrumbs; use Cone\Root\Interfaces\Navigation\Registry as Navigation; +use Cone\Root\Interfaces\Settings\Registry as Settings; use Cone\Root\Models\User; use Cone\Root\Resources\Resources; use Cone\Root\Widgets\Widgets; @@ -55,6 +56,11 @@ class Root */ public readonly Breadcrumbs $breadcrumbs; + /** + * The settings instance. + */ + public readonly Settings $settings; + /** * The auth resolver. */ @@ -75,6 +81,7 @@ public function __construct(Application $app) $this->widgets = new Widgets; $this->navigation = $app->make(Navigation::class); $this->breadcrumbs = $app->make(Breadcrumbs::class); + $this->settings = $app->make(Settings::class); $this->timezone = $app['config']->get('app.timezone'); } diff --git a/src/RootServiceProvider.php b/src/RootServiceProvider.php index 3c32b339..0b6e4009 100644 --- a/src/RootServiceProvider.php +++ b/src/RootServiceProvider.php @@ -39,8 +39,11 @@ class RootServiceProvider extends ServiceProvider Interfaces\Models\Medium::class => Models\Medium::class, Interfaces\Models\Meta::class => Models\Meta::class, Interfaces\Models\Notification::class => Models\Notification::class, + Interfaces\Models\Setting::class => Models\Setting::class, Interfaces\Models\User::class => Models\User::class, Interfaces\Navigation\Registry::class => Navigation\Registry::class, + Interfaces\Settings\Registry::class => Settings\Registry::class, + Interfaces\Settings\Repository::class => Settings\Repository::class, ]; /** diff --git a/src/Settings/Registry.php b/src/Settings/Registry.php new file mode 100644 index 00000000..39db241a --- /dev/null +++ b/src/Settings/Registry.php @@ -0,0 +1,38 @@ +repository = $repository; + } + + /** + * Get the repository instance. + */ + public function getRepository(): Repository + { + return $this->repository; + } + + /** + * Dynamically call the given method. + */ + public function __call(string $name, array $arguments): mixed + { + return call_user_func_array([$this->repository, $name], $arguments); + } +} diff --git a/src/Settings/Repository.php b/src/Settings/Repository.php new file mode 100644 index 00000000..018011b8 --- /dev/null +++ b/src/Settings/Repository.php @@ -0,0 +1,207 @@ +model()->newQuery(); + } + + /** + * Set the value cast. + */ + public function cast(string $key, string $type): void + { + $this->casts[$key] = $type; + } + + /** + * Merge the casts. + */ + public function mergeCasts(array $casts): void + { + $this->casts = array_merge($this->casts, $casts); + } + + /** + * Remove the given casts. + */ + public function removeCasts(string|array $keys): void + { + foreach ((array) $keys as $key) { + unset($this->casts[$key]); + } + } + + /** + * Remove the given casts. + */ + public function clearCasts(): void + { + $this->casts = []; + } + + /** + * Get the value casts. + */ + public function getCasts(): array + { + return $this->casts; + } + + /** + * Get the value for the given key. + */ + public function get(string $key, mixed $default = null, bool $fresh = false): mixed + { + if ($this->offsetExists($key) && ! $fresh) { + return $this->offsetGet($key); + } + + return $this->refresh($key, $default); + } + + /** + * Refresh the given key. + */ + public function refresh(string $key, mixed $default = null): mixed + { + $model = $this->query()->firstWhere('key', '=', $key); + + $value = is_null($model) + ? $default + : $model->castValue($this->casts[$key] ?? null)->value; + + $this->offsetSet($key, $value); + + return $value; + } + + /** + * Set the value for the given key. + */ + public function set(string $key, mixed $value): mixed + { + $model = $this->query()->firstOrNew(['key' => $key]); + + $model->castValue($this->casts[$key] ?? null); + + $model->fill(['value' => $value]); + + $model->save(); + + $this->offsetSet($key, $model->value); + + return $this->offsetGet($key); + } + + /** + * Delete the given keys. + */ + public function delete(string|array $keys): void + { + foreach ((array) $keys as $key) { + $this->offsetUnset($key); + } + + $this->query()->whereIn('key', (array) $keys)->delete(); + } + + /** + * Flush the cache. + */ + public function flush(): void + { + $this->cache = []; + } + + /** + * Get all the settings. + */ + public function all(): array + { + return $this->toArray(); + } + + /** + * Convert the repository to an array. + */ + public function toArray(): array + { + return $this->cache; + } + + /** + * Determine if an item exists at an offset. + * + * @param TKey $key + */ + public function offsetExists($key): bool + { + return isset($this->cache[$key]); + } + + /** + * Get an item at a given offset. + * + * @param TKey $key + */ + public function offsetGet($key): mixed + { + return $this->cache[$key]; + } + + /** + * Set the item at a given offset. + * + * @param TKey|null $key + * @param TValue $value + */ + public function offsetSet($key, $value): void + { + if (is_null($key)) { + $this->cache[] = $value; + } else { + $this->cache[$key] = $value; + } + } + + /** + * Unset the item at a given offset. + * + * @param TKey $key + */ + public function offsetUnset($key): void + { + unset($this->cache[$key]); + } +} diff --git a/tests/Settings/SettingsTest.php b/tests/Settings/SettingsTest.php new file mode 100644 index 00000000..03f03673 --- /dev/null +++ b/tests/Settings/SettingsTest.php @@ -0,0 +1,70 @@ +registry = new Registry(new Repository); + } + + public function test_setting_can_be_set(): void + { + $value = $this->registry->set('foo', 'bar'); + $this->assertSame('bar', $value); + + $this->assertDatabaseHas('root_settings', ['key' => 'foo', 'value' => 'bar']); + } + + public function test_setting_value_with_cast(): void + { + $this->registry->cast('ran_at', 'datetime'); + + $value = $this->registry->get('ran_at'); + $this->assertNull($value); + + $value = $this->registry->set('ran_at', $now = Date::now()); + $this->assertSame( + $now->__toString(), + $this->registry->query()->firstWhere('key', 'ran_at')->value + ); + + $this->assertSame( + $now->__toString(), + $this->registry->get('ran_at')->__toString() + ); + } + + public function test_setting_can_be_get(): void + { + $value = $this->registry->get('foo'); + $this->assertNull($value); + + $value = $this->registry->get('foo', 'bar'); + $this->assertSame('bar', $value); + } + + public function test_setting_can_be_deleted(): void + { + $value = $this->registry->set('foo', 'bar'); + $this->assertSame('bar', $value); + + $value = $this->registry->get('foo'); + $this->assertSame('bar', $value); + + $this->registry->delete('foo'); + $this->assertNull($this->registry->get('foo')); + + $this->assertDatabaseMissing('root_settings', ['key' => 'foo']); + } +}