347 lines
12 KiB
PHP
347 lines
12 KiB
PHP
<?php
|
||
|
||
namespace Modules\SystemTool\Base;
|
||
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\Crypt;
|
||
use Illuminate\Support\Facades\DB;
|
||
use InvalidArgumentException;
|
||
use Modules\SystemTool\Attributes\Setting;
|
||
use Modules\SystemTool\Enum\ESettingType;
|
||
use ReflectionClass;
|
||
|
||
/**
|
||
* 设置定义基类
|
||
*/
|
||
abstract class SettingsDefinition
|
||
{
|
||
/**
|
||
* 聚合缓存键名 — 全局中间件 LoadAppSettingsMiddleware 使用
|
||
* 所有设置一次性缓存在此键中,set() 写入时自动清除。
|
||
*/
|
||
const AGGREGATE_CACHE_KEY = 'app-settings-all';
|
||
|
||
/**
|
||
* 解析后的定义缓存(类名 → 定义数组)
|
||
*
|
||
* @var array<string, array<string, array{config: string, type: string, description: string|null}>>
|
||
*/
|
||
protected static array $definitions = [];
|
||
|
||
/**
|
||
* 从 #[Setting] Attribute 解析设置定义
|
||
*
|
||
* @return array<string, array{config: string, type: string, description: string|null}>
|
||
*/
|
||
public static function getDefinition(): array
|
||
{
|
||
$class = static::class;
|
||
|
||
if (! isset(self::$definitions[$class])) {
|
||
$reflection = new ReflectionClass($class);
|
||
$attributes = $reflection->getAttributes(Setting::class);
|
||
$definition = [];
|
||
|
||
foreach ($attributes as $attribute) {
|
||
/** @var Setting $instance */
|
||
$instance = $attribute->newInstance();
|
||
$definition[$instance->config] = [
|
||
'config' => $instance->config,
|
||
'type' => $instance->type->value,
|
||
'description' => $instance->description,
|
||
];
|
||
}
|
||
|
||
self::$definitions[$class] = $definition;
|
||
}
|
||
|
||
return self::$definitions[$class];
|
||
}
|
||
|
||
/**
|
||
* 判断某个 key 是否在本类的定义中
|
||
*/
|
||
public static function hasDefinitionKey(string $key): bool
|
||
{
|
||
return array_key_exists($key, static::getDefinition());
|
||
}
|
||
|
||
/**
|
||
* 获取数据库表名
|
||
*/
|
||
public static function getTableName(): string
|
||
{
|
||
return config('app_settings.table', 'sys_app_settings');
|
||
}
|
||
|
||
/**
|
||
* 将本类的所有配置项定义同步到 应用配置 表
|
||
*
|
||
* - 新 key 插入,值来自当前 config() 的默认值
|
||
* - 已有 key 只更新 description,不覆盖值
|
||
* - 部署时在 migration 中调用
|
||
*/
|
||
public static function init(): void
|
||
{
|
||
$definition = static::getDefinition();
|
||
$table = static::getTableName();
|
||
|
||
foreach ($definition as $key => $setting) {
|
||
$exists = DB::table($table)->where('key', $key)->exists();
|
||
|
||
if (! $exists) {
|
||
$value = static::getConfigValue($key);
|
||
static::set($key, $value);
|
||
} else {
|
||
DB::table($table)
|
||
->where('key', $key)
|
||
->update([
|
||
'description' => $setting['description'] ?? null,
|
||
'updated_at' => now(),
|
||
]);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取配置值
|
||
*
|
||
* @throws InvalidArgumentException 当 key 未定义时
|
||
*/
|
||
public static function get(string $key, mixed $default = null): mixed
|
||
{
|
||
if (! static::hasDefinitionKey($key)) {
|
||
throw new InvalidArgumentException("Setting key '{$key}' is not defined in " . static::class);
|
||
}
|
||
|
||
return static::getCacheValue($key, function () use ($key, $default) {
|
||
return static::getDBValue($key, function () use ($key, $default) {
|
||
return static::setCacheValue(
|
||
key: $key,
|
||
value: static::getConfigValue($key, $default),
|
||
);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 写入配置值 → DB + Cache
|
||
*
|
||
* @throws InvalidArgumentException 当 key 未定义或类型不匹配时
|
||
*/
|
||
public static function set(string $key, mixed $value): void
|
||
{
|
||
if (! static::hasDefinitionKey($key)) {
|
||
throw new InvalidArgumentException("Setting key '{$key}' is not defined in " . static::class);
|
||
}
|
||
|
||
static::setDBValue($key, $value);
|
||
static::setCacheValue($key, $value);
|
||
|
||
// 当前请求立即生效(中间件在下一个请求才会重新加载)
|
||
config([$key => $value]);
|
||
|
||
// 清除聚合缓存,使下次请求通过中间件重新从 DB 加载
|
||
Cache::forget(self::AGGREGATE_CACHE_KEY);
|
||
}
|
||
|
||
/**
|
||
* 从 Laravel config 读取(最终 fallback)
|
||
*/
|
||
public static function getConfigValue(string $key, mixed $default = null): mixed
|
||
{
|
||
return config($key, $default);
|
||
}
|
||
|
||
/**
|
||
* 从缓存读取
|
||
*
|
||
* @param mixed $default 默认值或闭包 (fn() => mixed)
|
||
*/
|
||
public static function getCacheValue(string $key, mixed $default = null): mixed
|
||
{
|
||
if ($default instanceof \Closure) {
|
||
return Cache::rememberForever('app-setting-' . $key, $default);
|
||
}
|
||
|
||
return Cache::get('app-setting-' . $key, $default);
|
||
}
|
||
|
||
/**
|
||
* 写入缓存
|
||
*/
|
||
public static function setCacheValue(string $key, mixed $value, ?int $ttl = null): mixed
|
||
{
|
||
if ($ttl !== null) {
|
||
Cache::put('app-setting-' . $key, $value, $ttl);
|
||
} else {
|
||
Cache::forever('app-setting-' . $key, $value);
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* 将本类所有配置项从 DB 加载到 Laravel config() 运行时
|
||
*
|
||
* 调用后 config('filesystems.default') 等可直接返回 DB 值。
|
||
* 在 getConfig() 批量读取场景下避免逐个 get() 的开销。
|
||
*/
|
||
public static function reloadIntoConfig(): void
|
||
{
|
||
$table = static::getTableName();
|
||
$rows = DB::table($table)->whereIn('key', array_keys(static::getDefinition()))->get();
|
||
|
||
foreach ($rows as $row) {
|
||
$value = match ((int) $row->type) {
|
||
ESettingType::String->value => $row->s,
|
||
ESettingType::Bool->value => is_null($row->n) ? null : (bool) $row->n,
|
||
ESettingType::Number->value => is_null($row->n) ? null : (int) $row->n,
|
||
ESettingType::Array->value => is_null($row->e) ? null : json_decode($row->e, true),
|
||
ESettingType::Object->value => is_null($row->e) ? null : unserialize(base64_decode($row->e)),
|
||
ESettingType::EncryptedString->value => is_null($row->e) ? null : \Illuminate\Support\Facades\Crypt::decrypt(base64_decode($row->e)),
|
||
default => $row->s ?? null,
|
||
};
|
||
|
||
config([$row->key => $value]);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清除某个 key 的缓存
|
||
*/
|
||
public static function forgetCache(string $key): void
|
||
{
|
||
Cache::forget('app-setting-' . $key);
|
||
|
||
// 同时清除聚合缓存,保持一致性
|
||
Cache::forget(self::AGGREGATE_CACHE_KEY);
|
||
}
|
||
|
||
|
||
/**
|
||
* 从数据库读取(按类型自动转换)
|
||
*
|
||
* @param mixed $default 默认值或闭包
|
||
*/
|
||
public static function getDBValue(string $key, mixed $default = null): mixed
|
||
{
|
||
$rec = DB::table(static::getTableName())->where('key', $key)->first();
|
||
|
||
if (! $rec) {
|
||
return $default instanceof \Closure ? $default() : value($default);
|
||
}
|
||
|
||
return match ((int) $rec->type) {
|
||
ESettingType::String->value => $rec->s,
|
||
ESettingType::Bool->value => is_null($rec->n) ? null : (bool) $rec->n,
|
||
ESettingType::Number->value => is_null($rec->n) ? null : (int) $rec->n,
|
||
ESettingType::Array->value => is_null($rec->e) ? null : json_decode($rec->e, true),
|
||
ESettingType::Object->value => is_null($rec->e) ? null : unserialize(base64_decode($rec->e)),
|
||
ESettingType::EncryptedString->value => is_null($rec->e) ? null : Crypt::decrypt(base64_decode($rec->e)),
|
||
default => $rec->s ?? $rec->value ?? null,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 写入数据库(按类型选择列)
|
||
*
|
||
* @throws InvalidArgumentException
|
||
*/
|
||
public static function setDBValue(string $key, mixed $value): void
|
||
{
|
||
$definition = static::getDefinition();
|
||
|
||
if (! isset($definition[$key])) {
|
||
throw new InvalidArgumentException("Setting key '{$key}' is not defined in " . static::class);
|
||
}
|
||
|
||
$type = $definition[$key]['type'];
|
||
$description = $definition[$key]['description'] ?? null;
|
||
$table = static::getTableName();
|
||
|
||
$data = [
|
||
'key' => $key,
|
||
'type' => $type,
|
||
'description' => $description,
|
||
'updated_at' => now(),
|
||
];
|
||
|
||
switch ((int) $type) {
|
||
case ESettingType::String->value:
|
||
if (! is_string($value) && ! is_null($value)) {
|
||
throw new InvalidArgumentException("Value for '{$key}' must be a string.");
|
||
}
|
||
$data['s'] = $value;
|
||
$data['n'] = null;
|
||
$data['e'] = null;
|
||
break;
|
||
|
||
case ESettingType::Bool->value:
|
||
if (! is_bool($value) && ! is_null($value)) {
|
||
throw new InvalidArgumentException("Value for '{$key}' must be a boolean.");
|
||
}
|
||
$data['n'] = is_null($value) ? null : (int) $value;
|
||
$data['s'] = null;
|
||
$data['e'] = null;
|
||
break;
|
||
|
||
case ESettingType::Number->value:
|
||
if (! is_numeric($value) && ! is_null($value)) {
|
||
throw new InvalidArgumentException("Value for '{$key}' must be a number.");
|
||
}
|
||
$data['n'] = $value;
|
||
$data['s'] = null;
|
||
$data['e'] = null;
|
||
break;
|
||
|
||
case ESettingType::Array->value:
|
||
if (! is_array($value) && ! is_null($value)) {
|
||
throw new InvalidArgumentException("Value for '{$key}' must be an array.");
|
||
}
|
||
$data['e'] = is_null($value) ? null : json_encode($value, JSON_UNESCAPED_UNICODE);
|
||
$data['s'] = null;
|
||
$data['n'] = null;
|
||
break;
|
||
|
||
case ESettingType::Object->value:
|
||
if (! is_object($value) && ! is_null($value)) {
|
||
throw new InvalidArgumentException("Value for '{$key}' must be an object.");
|
||
}
|
||
$data['e'] = is_null($value) ? null : base64_encode(serialize($value));
|
||
$data['s'] = null;
|
||
$data['n'] = null;
|
||
break;
|
||
|
||
case ESettingType::EncryptedString->value:
|
||
if (! is_string($value) && ! is_null($value)) {
|
||
throw new InvalidArgumentException("Value for '{$key}' must be a string.");
|
||
}
|
||
$data['e'] = is_null($value) ? null : base64_encode(Crypt::encrypt($value));
|
||
$data['s'] = null;
|
||
$data['n'] = null;
|
||
break;
|
||
|
||
default:
|
||
throw new InvalidArgumentException("Unknown setting type '{$type}' for key '{$key}'.");
|
||
}
|
||
|
||
$exists = DB::table($table)->where('key', $key)->exists();
|
||
|
||
if ($exists) {
|
||
DB::table($table)->where('key', $key)->update($data);
|
||
} else {
|
||
$data['created_at'] = now();
|
||
DB::table($table)->insert($data);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 从数据库删除某个配置项
|
||
*/
|
||
public static function deleteDBValue(string $key): void
|
||
{
|
||
DB::table(static::getTableName())->where('key', $key)->delete();
|
||
}
|
||
}
|