Files
2026-07-13 15:23:29 +08:00

347 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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();
}
}