diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be58ed..a5d4fb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Removed + +- **Breaking, for 0.4.0: the runtime settings store.** `Toolkit::settings()`, `Laranail::settings()`, + `Services\SettingsStore`, `Services\Contracts\SettingsStoreInterface`, their container bindings, + the `laranail.toolkit.settings` config block and its env keys `LARANAIL_SETTINGS_DISK` and + `LARANAIL_SETTINGS_PATH` are removed, with no forwarder. Runtime settings now live in + `laranail/settings`, added to `suggest`. Move the JSON file's data with + `php artisan laranail::settings.import --from=toolkit --apply` (a dry run without + `--apply`); see [UPGRADING.md](UPGRADING.md). `isValidDatabaseConnection('settings')`, + `RuntimeConfigurator` and the feature toggles are unaffected. + ## [0.3.2] - 2026-10-20 ### Changed diff --git a/UPGRADING.md b/UPGRADING.md index adf9616..64ba386 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,43 @@ # Upgrade guide +## The settings store moved to `laranail/settings` (0.4.0) + +Toolkit's JSON-file settings store is gone. Runtime settings now live in +[`laranail/settings`](https://opensource.simtabi.com/documentation/laranail/settings/), which stores +them in the database with types, scopes and encryption. This is a breaking change, shipping in 0.4.0. + +Removed, with no forwarder: + +- `Toolkit::settings()` and `Laranail::settings()`; +- `Simtabi\Laranail\Toolkit\Services\SettingsStore` and + `Services\Contracts\SettingsStoreInterface`, and their container bindings; +- the `laranail.toolkit.settings` config block (`disk`, `path`) and its env keys + `LARANAIL_SETTINGS_DISK` and `LARANAIL_SETTINGS_PATH`. Delete them from your `.env` and from a + published `config/laranail/toolkit.php`. + +Move the data before you upgrade. The file defaulted to `laranail/settings.json` on the `local` +disk, so it is usually `storage/app/laranail/settings.json`. The importer reads a local absolute +path; if the file sat on another disk (for example `s3`), copy it down first. + +```bash +composer require laranail/settings +php artisan laranail::settings.import /var/www/storage/app/laranail/settings.json --from=toolkit # a dry run +php artisan laranail::settings.import /var/www/storage/app/laranail/settings.json --from=toolkit --apply +``` + +The importer flattens nested objects to dot keys (`{"mail": {"host": …}}` becomes `mail.host`) and +keeps a list as one value. Then replace the calls: + +```diff +- Toolkit::settings()->get('mail.host', 'localhost'); +- Toolkit::settings()->set('mail.host', $host); ++ Settings::get('mail.host', 'localhost'); ++ Settings::set('mail.host', $host); +``` + +See the settings package's importer and usage docs for keys that must be defined before they are +set, and for scoped values. + ## Atlas, Avatar and Gravatar moved out Three modules left this package. `Modules\Atlas` became diff --git a/composer.json b/composer.json index b3a9f97..4fbd4eb 100644 --- a/composer.json +++ b/composer.json @@ -120,7 +120,8 @@ "livewire/livewire": "^3.0 \u2014 for the Livewire component-registration module", "laranail/db-tools": "for UUID/ULID model traits, schema macros, soft-archive, backup/restore and the database CLI (relocated out of this package)", "laranail/package-tools": "for the fluent runtime ConfigManager, relocated out of this package.", - "laranail/python": "Bidirectional Python bridge \u2014 HTTP services, local process execution and signed callbacks (supersedes PythonApiService, relocated out of this package)." + "laranail/python": "Bidirectional Python bridge \u2014 HTTP services, local process execution and signed callbacks (supersedes PythonApiService, relocated out of this package).", + "laranail/settings": "Typed, scoped, database-backed runtime settings (replaces the removed SettingsStore; import its JSON file with laranail::settings.import --from=toolkit)." }, "repositories": [ { diff --git a/config/toolkit.php b/config/toolkit.php index 6dcf1e4..4f05d8e 100644 --- a/config/toolkit.php +++ b/config/toolkit.php @@ -118,15 +118,6 @@ ], ], - /* - * Runtime settings store (Services\SettingsStore) — a JSON file of dynamic, - * persisted-at-runtime values, kept separate from this static config. - */ - 'settings' => [ - 'disk' => env('LARANAIL_SETTINGS_DISK', 'local'), - 'path' => env('LARANAIL_SETTINGS_PATH', 'laranail/settings.json'), - ], - /* * PHP runtime / INI settings applied by Support\RuntimeConfigurator via * ->usingConfig()/::fromConfig(). Everything here is data — override any diff --git a/docs/architecture.md b/docs/architecture.md index 0471b75..8a841ee 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -22,7 +22,7 @@ src/ ├── Commands/MakeCrud.php # make-crud (extends the laranail/console base) ├── Http/Controllers/CrudController.php # abstract base controller ├── Services/ # injectable, interface-backed services (File, System, -│ # Cache, Log, SettingsStore, RateLimiter, Scheduler, ...) +│ # Cache, Log, RateLimiter, Scheduler, ...) ├── Macros/ # grouped macro providers + MacroServiceProvider ├── Traits/ # ApiResponse, Auditable, FilePathGuard, ... ├── Support/ # pure/static helpers: ApiResponder, Cast, Config, AuthHelper, @@ -86,14 +86,13 @@ or the unified `Toolkit` facade (`Toolkit::archiver()`, …). injectable core services with typed `@method` accessors. Alongside the module accessors (`archiver()`, `cache()`, `livewire()`) and the security generators (`token()`, `password()`, -`passphrase()`), it exposes the core services — including the **five service +`passphrase()`), it exposes the core services — including the **four service accessors**: | Accessor | Returns (contract) | |---|---| | `Toolkit::cache()` | `Services\Contracts\CacheRepositoryInterface` | | `Toolkit::log()` | `Services\Contracts\LoggerServiceInterface` | -| `Toolkit::settings()` | `Services\Contracts\SettingsStoreInterface` | | `Toolkit::rateLimiter()` | `Services\Contracts\RateLimiterServiceInterface` | | `Toolkit::scheduler()` | `Services\Contracts\SchedulerServiceInterface` | diff --git a/docs/configuration.md b/docs/configuration.md index c072ee0..e107860 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -77,16 +77,6 @@ Named profiles live under `defaults.`, each a `{max_attempts, decay_minute | `defaults.auth` | `5` | `15` | | `defaults.download` | `3` | `1` | -## `laranail.toolkit.settings` - -The runtime settings store (`SettingsStore`) — a JSON file of dynamic, -persisted-at-runtime values, kept separate from this static config. - -| Key | Default | Notes | -|-----|---------|-------| -| `disk` | `local` | Filesystem disk for the settings file. Env: `LARANAIL_SETTINGS_DISK`. | -| `path` | `laranail/settings.json` | Path on the disk. Env: `LARANAIL_SETTINGS_PATH`. | - ## `laranail.toolkit.runtime` PHP runtime / INI settings applied by diff --git a/docs/installation.md b/docs/installation.md index 1d81843..917a210 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -59,8 +59,8 @@ php artisan migrate These are resolved from the package and need **no** `vendor:publish`: -- **Services** — `Services\{CacheService, SettingsStore, SchedulerService, - RateLimiterService, LogService}` (container-bound by their contracts + concrete +- **Services** — `Services\{CacheService, SchedulerService, RateLimiterService, + LogService}` (container-bound by their contracts + concrete class; inject or `app(...)` them). - **Support helpers** — `Support\{QueryParameters, CollectionFilter, Environment, AuthHelper}` (static utilities). diff --git a/docs/utilities.md b/docs/utilities.md index 67215d4..53c5ad8 100644 --- a/docs/utilities.md +++ b/docs/utilities.md @@ -69,21 +69,6 @@ Level methods: `debug()`, `info()`, `warning()`, `error()`, `critical()` (each `(string $message, array $context = [], ?string $channel = null)`), plus the enum-keyed `log(LogLevel $level, ...)` and `exception(Throwable $e, ...)`. -### SettingsStore - -Read/write dynamic settings persisted as a JSON file on a configured disk — a -runtime store, deliberately separate from Laravel's static `config()`. Keys use -dot notation. Bound to `Services\Contracts\SettingsStoreInterface`. - -```php -$settings = app(SettingsStore::class); -$settings->all(); -$settings->get('feature.enabled', false); -$settings->has('feature.enabled'); -$settings->set('feature.enabled', true); -$settings->forget('feature.enabled'); -``` - ### RateLimiterService Thin wrapper over Laravel's rate limiter. Bound to diff --git a/src/Facades/Laranail.php b/src/Facades/Laranail.php index 665b50d..f5a7a9c 100644 --- a/src/Facades/Laranail.php +++ b/src/Facades/Laranail.php @@ -31,7 +31,6 @@ * @method static \Simtabi\Laranail\Toolkit\Services\Contracts\CacheRepositoryInterface cache() * @method static \Simtabi\Laranail\Toolkit\Support\RuntimeConfigurator runtime() * @method static \Simtabi\Laranail\Toolkit\Services\Contracts\LoggerServiceInterface log() - * @method static \Simtabi\Laranail\Toolkit\Services\Contracts\SettingsStoreInterface settings() * @method static \Simtabi\Laranail\Toolkit\Services\Contracts\RateLimiterServiceInterface rateLimiter() * @method static \Simtabi\Laranail\Toolkit\Services\Contracts\SchedulerServiceInterface scheduler() * @method static \Simtabi\Laranail\Toolkit\Modules\Livewire\LivewireServiceInterface livewire() diff --git a/src/Facades/Toolkit.php b/src/Facades/Toolkit.php index 5513b7f..7e6a624 100644 --- a/src/Facades/Toolkit.php +++ b/src/Facades/Toolkit.php @@ -27,7 +27,6 @@ * @method static \Simtabi\Laranail\Toolkit\Macros\MacroableModels macroableModels() * @method static \Simtabi\Laranail\Toolkit\Support\RuntimeConfigurator runtime() * @method static \Simtabi\Laranail\Toolkit\Services\Contracts\LoggerServiceInterface log() - * @method static \Simtabi\Laranail\Toolkit\Services\Contracts\SettingsStoreInterface settings() * @method static \Simtabi\Laranail\Toolkit\Services\Contracts\RateLimiterServiceInterface rateLimiter() * @method static \Simtabi\Laranail\Toolkit\Services\Contracts\SchedulerServiceInterface scheduler() * @method static \Simtabi\Laranail\Toolkit\Modules\Livewire\LivewireServiceInterface livewire() diff --git a/src/Providers/ToolkitServiceProvider.php b/src/Providers/ToolkitServiceProvider.php index 6f13fa7..9bac79a 100644 --- a/src/Providers/ToolkitServiceProvider.php +++ b/src/Providers/ToolkitServiceProvider.php @@ -23,7 +23,6 @@ use Simtabi\Laranail\Toolkit\Services\ModelService; use Simtabi\Laranail\Toolkit\Services\RouteService; use Simtabi\Laranail\Toolkit\Macros\MacroableModels; -use Simtabi\Laranail\Toolkit\Services\SettingsStore; use Simtabi\Laranail\Toolkit\Services\SystemService; use Simtabi\Laranail\Toolkit\Services\SessionService; use Simtabi\Laranail\Toolkit\Traits\ApiResponseTrait; @@ -51,7 +50,6 @@ use Simtabi\Laranail\Toolkit\Http\Middleware\EmailObfuscatorMiddleware; use Simtabi\Laranail\Toolkit\Modules\Eventing\Listeners\LogCacheEvents; use Simtabi\Laranail\Toolkit\Services\Contracts\LoggerServiceInterface; -use Simtabi\Laranail\Toolkit\Services\Contracts\SettingsStoreInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\SystemServiceInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\SessionServiceInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\CacheRepositoryInterface; @@ -270,8 +268,7 @@ private function registerBindings(): void $this->app->bind(CacheRepositoryInterface::class, CacheService::class); $this->app->bind(LoggerServiceInterface::class, LogService::class); - // Settings store, rate limiter and scheduler service contracts. - $this->app->bind(SettingsStoreInterface::class, SettingsStore::class); + // Rate limiter and scheduler service contracts. $this->app->bind(RateLimiterServiceInterface::class, RateLimiterService::class); $this->app->bind(SchedulerServiceInterface::class, SchedulerService::class); @@ -290,7 +287,7 @@ private function registerBindings(): void // `app(...)` keeps resolving them. $this->loadClass(ApiResponseTrait::class); $this->loadClass(FileProcessingTrait::class); - $this->loadServiceClasses([SettingsStore::class, SchedulerService::class, LogService::class]); + $this->loadServiceClasses([SchedulerService::class, LogService::class]); $this->loadRateLimiterService(); $this->loadCacheService(); } diff --git a/src/Services/Contracts/SettingsStoreInterface.php b/src/Services/Contracts/SettingsStoreInterface.php deleted file mode 100644 index c407c11..0000000 --- a/src/Services/Contracts/SettingsStoreInterface.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ - public function all(): array; - - /** - * Get a setting by dot-notation key. - */ - public function get(string $key, mixed $default = null): mixed; - - /** - * Whether a setting exists for the given dot-notation key. - */ - public function has(string $key): bool; - - /** - * Set (or overwrite) a setting by dot-notation key and persist. - */ - public function set(string $key, mixed $value): void; - - /** - * Remove a setting by dot-notation key and persist. - */ - public function forget(string $key): void; -} diff --git a/src/Services/SettingsStore.php b/src/Services/SettingsStore.php deleted file mode 100644 index f408217..0000000 --- a/src/Services/SettingsStore.php +++ /dev/null @@ -1,115 +0,0 @@ -path = $path ?? ToolkitConfig::string('laranail.toolkit.settings.path', 'laranail/settings.json'); - } - - /** - * All persisted settings. - * - * @return array - */ - public function all(): array - { - $disk = $this->disk(); - - if (! $disk->exists($this->path)) { - return []; - } - - $decoded = json_decode((string) $disk->get($this->path), true); - - return is_array($decoded) ? $decoded : []; - } - - /** - * Get a setting by dot-notation key. - */ - public function get(string $key, mixed $default = null): mixed - { - return Arr::get($this->all(), $key, $default); - } - - public function has(string $key): bool - { - return Arr::has($this->all(), $key); - } - - /** - * Set (or overwrite) a setting by dot-notation key and persist. - */ - public function set(string $key, mixed $value): void - { - $this->mutate(function (array &$all) use ($key, $value): void { - Arr::set($all, $key, $value); - }); - } - - /** - * Remove a setting by dot-notation key and persist. - */ - public function forget(string $key): void - { - $this->mutate(function (array &$all) use ($key): void { - Arr::forget($all, $key); - }); - } - - /** - * Serialise the read-modify-write behind an atomic lock so two concurrent - * writers cannot each read the same snapshot and clobber the other's key. - * - * Cross-process safety requires an atomic cache store (redis/database/…); the - * array/file drivers serialise within a single process. - * - * @param callable(array): void $mutator - */ - private function mutate(callable $mutator): void - { - Cache::lock('laranail:toolkit:settings:' . md5($this->path), 10)->block(5, function () use ($mutator): void { - $all = $this->all(); - $mutator($all); - $this->persist($all); - }); - } - - /** - * @param array $settings - */ - private function persist(array $settings): void - { - $this->disk()->put( - $this->path, - json_encode($settings, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), - ); - } - - private function disk(): Filesystem - { - return $this->disk ??= Storage::disk(ToolkitConfig::string('laranail.toolkit.settings.disk', 'local')); - } -} diff --git a/src/ToolkitManager.php b/src/ToolkitManager.php index 0ebd126..e69943a 100644 --- a/src/ToolkitManager.php +++ b/src/ToolkitManager.php @@ -20,7 +20,6 @@ use Simtabi\Laranail\Toolkit\Modules\Archiver\ArchiverServiceInterface; use Simtabi\Laranail\Toolkit\Modules\Livewire\LivewireServiceInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\LoggerServiceInterface; -use Simtabi\Laranail\Toolkit\Services\Contracts\SettingsStoreInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\SystemServiceInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\SessionServiceInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\CacheRepositoryInterface; @@ -228,14 +227,6 @@ public function log(): LoggerServiceInterface return $this->app->make(LoggerServiceInterface::class); } - /** - * Runtime settings store (dynamic, persisted-at-runtime key/value JSON). - */ - public function settings(): SettingsStoreInterface - { - return $this->app->make(SettingsStoreInterface::class); - } - /** * Named-profile rate limiter (attempts/decay over the cache store). */ diff --git a/tests/Unit/Modules/FacadesTest.php b/tests/Unit/Modules/FacadesTest.php index ed3c01b..bcf4256 100644 --- a/tests/Unit/Modules/FacadesTest.php +++ b/tests/Unit/Modules/FacadesTest.php @@ -21,7 +21,6 @@ use Simtabi\Laranail\Toolkit\Modules\Archiver\ArchiverServiceInterface; use Simtabi\Laranail\Toolkit\Modules\Livewire\LivewireServiceInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\LoggerServiceInterface; -use Simtabi\Laranail\Toolkit\Services\Contracts\SettingsStoreInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\SystemServiceInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\SessionServiceInterface; use Simtabi\Laranail\Toolkit\Services\Contracts\CacheRepositoryInterface; @@ -73,14 +72,12 @@ public function test_toolkit_facade_fronts_the_newly_exposed_services(): void { $this->assertInstanceOf(CacheRepositoryInterface::class, Toolkit::cache()); $this->assertInstanceOf(LoggerServiceInterface::class, Toolkit::log()); - $this->assertInstanceOf(SettingsStoreInterface::class, Toolkit::settings()); $this->assertInstanceOf(RateLimiterServiceInterface::class, Toolkit::rateLimiter()); $this->assertInstanceOf(SchedulerServiceInterface::class, Toolkit::scheduler()); // The same accessors are reachable through the Laranail alias facade. $this->assertInstanceOf(CacheRepositoryInterface::class, Laranail::cache()); $this->assertInstanceOf(LoggerServiceInterface::class, Laranail::log()); - $this->assertInstanceOf(SettingsStoreInterface::class, Laranail::settings()); $this->assertInstanceOf(RateLimiterServiceInterface::class, Laranail::rateLimiter()); $this->assertInstanceOf(SchedulerServiceInterface::class, Laranail::scheduler()); } diff --git a/tests/Unit/Services/SettingsStoreTest.php b/tests/Unit/Services/SettingsStoreTest.php deleted file mode 100644 index fa68e3b..0000000 --- a/tests/Unit/Services/SettingsStoreTest.php +++ /dev/null @@ -1,87 +0,0 @@ -config = new SettingsStore(Storage::disk('local'), 'settings.json'); - } - - public function test_all_is_empty_before_anything_is_written(): void - { - $this->assertSame([], $this->config->all()); - } - - public function test_get_returns_default_for_missing_key(): void - { - $this->assertNull($this->config->get('missing')); - $this->assertSame('fallback', $this->config->get('missing', 'fallback')); - } - - public function test_set_then_get_round_trips_and_persists(): void - { - $this->config->set('mail.from', 'a@b.com'); - - $this->assertSame('a@b.com', $this->config->get('mail.from')); - Storage::disk('local')->assertExists('settings.json'); - - // A fresh instance reads the same persisted store. - $fresh = new SettingsStore(Storage::disk('local'), 'settings.json'); - $this->assertSame('a@b.com', $fresh->get('mail.from')); - } - - public function test_set_uses_dot_notation_nesting(): void - { - $this->config->set('feature.flags.beta', true); - - $this->assertTrue($this->config->get('feature.flags.beta')); - $this->assertSame(['flags' => ['beta' => true]], $this->config->get('feature')); - } - - public function test_has_reflects_presence(): void - { - $this->assertFalse($this->config->has('x')); - $this->config->set('x', 1); - $this->assertTrue($this->config->has('x')); - } - - public function test_forget_removes_a_key(): void - { - $this->config->set('a', 1); - $this->config->set('b', 2); - - $this->config->forget('a'); - - $this->assertFalse($this->config->has('a')); - $this->assertTrue($this->config->has('b')); - } - - public function test_overwrites_an_existing_value(): void - { - $this->config->set('k', 'first'); - $this->config->set('k', 'second'); - - $this->assertSame('second', $this->config->get('k')); - } - - public function test_corrupt_store_degrades_to_empty(): void - { - Storage::disk('local')->put('settings.json', 'not-json'); - - $this->assertSame([], $this->config->all()); - $this->assertNull($this->config->get('anything')); - } -}