CluPilotCloud/app/Support/Settings.php

86 lines
3.0 KiB
PHP

<?php
namespace App\Support;
use Illuminate\Support\Facades\Cache;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* Runtime settings an operator can change from the console.
*
* Cached because the public-site gate reads one of these on every single
* request; the cache is dropped on write, so a flip takes effect immediately
* rather than "within a minute".
*/
final class Settings
{
private const CACHE_PREFIX = 'app_setting:';
public static function get(string $key, mixed $default = null): mixed
{
$cacheKey = self::CACHE_PREFIX.$key;
if (Cache::has($cacheKey)) {
$raw = Cache::get($cacheKey);
} else {
try {
$row = DB::table('app_settings')->where('key', $key)->value('value');
} catch (QueryException $e) {
// The gate reads a setting on every request, so an unreachable
// or not-yet-migrated table would take the whole site down —
// including during a deploy where new code runs before migrate.
// Fall back to the caller's default, but do NOT cache that:
// caching a failure would keep a hidden site publicly visible
// long after the database came back.
Log::warning('app_settings unavailable, using default', [
'key' => $key, 'error' => $e->getMessage(),
]);
return $default;
}
// Sentinel: distinguishes "stored null" from "never stored", so a
// missing row does not get re-queried on every request.
$raw = $row ?? '__missing__';
Cache::forever($cacheKey, $raw);
}
return $raw === '__missing__' ? $default : json_decode($raw, true);
}
public static function set(string $key, mixed $value): void
{
DB::table('app_settings')->updateOrInsert(
['key' => $key],
['value' => json_encode($value), 'updated_at' => now(), 'created_at' => now()],
);
Cache::forget(self::CACHE_PREFIX.$key);
}
public static function bool(string $key, bool $default = false): bool
{
return (bool) self::get($key, $default);
}
/**
* Remove a setting entirely, falling back to the caller's own default
* from then on.
*
* Not `set($key, null)`: that would still leave a row (and a cache
* entry) behind — indistinguishable to `get()`, but a migration's
* `down()` undoing its own `up()` should leave no row, not a null one.
* Deletes the row before dropping the cache, the same order `set()`
* writes-then-invalidates in — a reader between the two statements sees
* the OLD value from a cache that is about to be dropped, never a stale
* one that outlives the row it described.
*/
public static function forget(string $key): void
{
DB::table('app_settings')->where('key', $key)->delete();
Cache::forget(self::CACHE_PREFIX.$key);
}
}