Persist system settings in database
This commit is contained in:
@@ -62,10 +62,17 @@ MAIL_FROM_NAME="${APP_NAME}"
|
||||
ASSET_CODE_PREFIX=AST
|
||||
ASSET_QR_ENABLED=true
|
||||
ASSET_WARRANTY_REMINDER_DAYS=30
|
||||
ASSET_DEPRECIATION_METHOD=straight_line
|
||||
ASSET_ATTACHMENT_MAX_SIZE_MB=20
|
||||
SYSTEM_DATE_FORMAT=d/m/Y
|
||||
SYSTEM_TIME_FORMAT=H:i
|
||||
SYSTEM_DEFAULT_CURRENCY=IDR
|
||||
SYSTEM_AUDIT_LOG=true
|
||||
SYSTEM_TABLE_PAGE_SIZE=25
|
||||
NOTIFICATION_EMAIL_ENABLED=true
|
||||
NOTIFICATION_SLACK_WEBHOOK_URL=
|
||||
MAINTENANCE_READONLY_MODE=false
|
||||
MAINTENANCE_WINDOW=
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
|
||||
49
app/Models/Setting.php
Normal file
49
app/Models/Setting.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'key',
|
||||
'group',
|
||||
'type',
|
||||
'value',
|
||||
'description',
|
||||
'is_public',
|
||||
];
|
||||
|
||||
/**
|
||||
* Setter untuk menyimpan nilai dengan aman (array -> json).
|
||||
*/
|
||||
public function setValueAttribute($value): void
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$this->attributes['value'] = json_encode($value);
|
||||
$this->attributes['type'] = $this->attributes['type'] ?? 'array';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->attributes['value'] = (string) $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter untuk casting nilai sesuai tipe yang disimpan.
|
||||
*/
|
||||
public function getValueAttribute($value): mixed
|
||||
{
|
||||
return match ($this->type) {
|
||||
'bool', 'boolean' => filter_var($value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE) ?? false,
|
||||
'int', 'integer' => (int) $value,
|
||||
'float', 'double' => (float) $value,
|
||||
'array', 'json' => json_decode($value, true) ?? [],
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,23 @@
|
||||
namespace App\Services\System;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Models\Setting;
|
||||
|
||||
class SystemSettingService
|
||||
{
|
||||
private const CACHE_KEY = 'system.settings';
|
||||
|
||||
/**
|
||||
* Ambil semua konfigurasi sistem dengan struktur terkelompok.
|
||||
*/
|
||||
public function all(): array
|
||||
{
|
||||
return config('system', []);
|
||||
$defaults = config('system', []);
|
||||
$overrides = Cache::rememberForever(self::CACHE_KEY, fn () => $this->loadFromDatabase());
|
||||
|
||||
// Override nilai default dengan data database agar dapat full customize.
|
||||
return array_replace_recursive($defaults, $overrides);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -25,11 +33,55 @@ class SystemSettingService
|
||||
/**
|
||||
* Set konfigurasi secara runtime tanpa menyentuh database.
|
||||
*/
|
||||
public function set(string $key, mixed $value): void
|
||||
public function set(string $key, mixed $value, ?string $type = null, ?string $description = null, ?string $group = null, bool $isPublic = false): void
|
||||
{
|
||||
$settings = $this->all();
|
||||
Arr::set($settings, $key, $value);
|
||||
Setting::updateOrCreate(
|
||||
['key' => $key],
|
||||
[
|
||||
'group' => $group ?? $this->guessGroupFromKey($key),
|
||||
'type' => $type ?? $this->inferType($value),
|
||||
'value' => $value,
|
||||
'description' => $description,
|
||||
'is_public' => $isPublic,
|
||||
]
|
||||
);
|
||||
|
||||
config(['system' => $settings]);
|
||||
$this->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh cache & config setelah update agar konsisten.
|
||||
*/
|
||||
public function refresh(): void
|
||||
{
|
||||
Cache::forget(self::CACHE_KEY);
|
||||
config(['system' => $this->all()]);
|
||||
}
|
||||
|
||||
private function loadFromDatabase(): array
|
||||
{
|
||||
$settings = [];
|
||||
|
||||
Setting::query()->orderBy('key')->get()->each(function (Setting $setting) use (&$settings) {
|
||||
Arr::set($settings, $setting->key, $setting->value);
|
||||
});
|
||||
|
||||
return $settings;
|
||||
}
|
||||
|
||||
private function inferType(mixed $value): string
|
||||
{
|
||||
return match (true) {
|
||||
is_bool($value) => 'boolean',
|
||||
is_int($value) => 'integer',
|
||||
is_float($value) => 'float',
|
||||
is_array($value) => 'array',
|
||||
default => 'string',
|
||||
};
|
||||
}
|
||||
|
||||
private function guessGroupFromKey(string $key): string
|
||||
{
|
||||
return explode('.', $key)[0] ?? 'general';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,24 @@ return [
|
||||
'code_prefix' => env('ASSET_CODE_PREFIX', 'AST'),
|
||||
'qr_enabled' => env('ASSET_QR_ENABLED', true),
|
||||
'warranty_reminder_days' => env('ASSET_WARRANTY_REMINDER_DAYS', 30),
|
||||
'depreciation_method' => env('ASSET_DEPRECIATION_METHOD', 'straight_line'),
|
||||
'attachment_max_size_mb' => env('ASSET_ATTACHMENT_MAX_SIZE_MB', 20),
|
||||
],
|
||||
'ui' => [
|
||||
'date_format' => env('SYSTEM_DATE_FORMAT', 'd/m/Y'),
|
||||
'time_format' => env('SYSTEM_TIME_FORMAT', 'H:i'),
|
||||
'currency' => env('SYSTEM_DEFAULT_CURRENCY', 'IDR'),
|
||||
'table_page_size' => env('SYSTEM_TABLE_PAGE_SIZE', 25),
|
||||
],
|
||||
'security' => [
|
||||
'audit_log' => env('SYSTEM_AUDIT_LOG', true),
|
||||
],
|
||||
'notification' => [
|
||||
'email_enabled' => env('NOTIFICATION_EMAIL_ENABLED', true),
|
||||
'slack_webhook_url' => env('NOTIFICATION_SLACK_WEBHOOK_URL'),
|
||||
],
|
||||
'maintenance' => [
|
||||
'readonly_mode' => env('MAINTENANCE_READONLY_MODE', false),
|
||||
'window' => env('MAINTENANCE_WINDOW'),
|
||||
],
|
||||
];
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('settings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('key')->unique();
|
||||
$table->string('group')->nullable();
|
||||
$table->string('type')->default('string');
|
||||
$table->text('value')->nullable();
|
||||
$table->string('description')->nullable();
|
||||
$table->boolean('is_public')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('settings');
|
||||
}
|
||||
};
|
||||
@@ -19,5 +19,8 @@ class DatabaseSeeder extends Seeder
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
|
||||
// Seed setting default agar siap dikustomisasi.
|
||||
$this->call(SettingSeeder::class);
|
||||
}
|
||||
}
|
||||
|
||||
47
database/seeders/SettingSeeder.php
Normal file
47
database/seeders/SettingSeeder.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class SettingSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed awal agar sistem langsung bisa dikustomisasi.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$settings = [
|
||||
// Aplikasi & UI
|
||||
['key' => 'application.name', 'value' => 'Asset Management System', 'type' => 'string', 'group' => 'application', 'description' => 'Nama aplikasi yang tampil di UI'],
|
||||
['key' => 'application.timezone', 'value' => 'Asia/Jakarta', 'type' => 'string', 'group' => 'application'],
|
||||
['key' => 'ui.date_format', 'value' => 'd/m/Y', 'type' => 'string', 'group' => 'ui'],
|
||||
['key' => 'ui.time_format', 'value' => 'H:i', 'type' => 'string', 'group' => 'ui'],
|
||||
['key' => 'ui.currency', 'value' => 'IDR', 'type' => 'string', 'group' => 'ui'],
|
||||
['key' => 'ui.table_page_size', 'value' => 25, 'type' => 'integer', 'group' => 'ui', 'description' => 'Default jumlah baris tabel per halaman'],
|
||||
|
||||
// Aset
|
||||
['key' => 'asset.code_prefix', 'value' => 'AST', 'type' => 'string', 'group' => 'asset'],
|
||||
['key' => 'asset.qr_enabled', 'value' => true, 'type' => 'boolean', 'group' => 'asset'],
|
||||
['key' => 'asset.warranty_reminder_days', 'value' => 30, 'type' => 'integer', 'group' => 'asset'],
|
||||
['key' => 'asset.depreciation_method', 'value' => 'straight_line', 'type' => 'string', 'group' => 'asset'],
|
||||
['key' => 'asset.attachment_max_size_mb', 'value' => 20, 'type' => 'integer', 'group' => 'asset', 'description' => 'Batas upload lampiran aset (MB)'],
|
||||
|
||||
// Keamanan & audit
|
||||
['key' => 'security.audit_log', 'value' => true, 'type' => 'boolean', 'group' => 'security', 'description' => 'Aktifkan audit log aktivitas'],
|
||||
|
||||
// Notifikasi
|
||||
['key' => 'notification.email_enabled', 'value' => true, 'type' => 'boolean', 'group' => 'notification'],
|
||||
['key' => 'notification.slack_webhook_url', 'value' => null, 'type' => 'string', 'group' => 'notification'],
|
||||
|
||||
// Maintenance
|
||||
['key' => 'maintenance.readonly_mode', 'value' => false, 'type' => 'boolean', 'group' => 'maintenance', 'description' => 'Jika true, seluruh fitur tulis dimatikan'],
|
||||
['key' => 'maintenance.window', 'value' => null, 'type' => 'string', 'group' => 'maintenance', 'description' => 'Jadwal maintenance terencana'],
|
||||
];
|
||||
|
||||
foreach ($settings as $setting) {
|
||||
Setting::updateOrCreate(['key' => $setting['key']], $setting);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,33 +2,69 @@
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Services\System\SystemSettingService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SystemSettingServiceTest extends TestCase
|
||||
{
|
||||
public function test_it_reads_system_settings_from_config(): void
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_it_reads_config_when_database_empty(): void
|
||||
{
|
||||
// Override konfigurasi untuk memastikan service mengambil nilai terbaru.
|
||||
config([
|
||||
'system.asset.code_prefix' => 'ASTX',
|
||||
'system.asset.code_prefix' => 'CFG',
|
||||
'system.application.name' => 'Custom AMS',
|
||||
]);
|
||||
|
||||
$service = app(SystemSettingService::class);
|
||||
|
||||
$this->assertSame('ASTX', $service->get('asset.code_prefix'));
|
||||
$this->assertSame('CFG', $service->get('asset.code_prefix'));
|
||||
$this->assertSame('Custom AMS', $service->get('application.name'));
|
||||
$this->assertTrue($service->get('asset.qr_enabled'));
|
||||
}
|
||||
|
||||
public function test_it_can_override_runtime_settings(): void
|
||||
public function test_it_prefers_database_overrides_and_casts_types(): void
|
||||
{
|
||||
// Simulasikan override via DB.
|
||||
Setting::create([
|
||||
'key' => 'asset.qr_enabled',
|
||||
'value' => false,
|
||||
'type' => 'boolean',
|
||||
'group' => 'asset',
|
||||
]);
|
||||
|
||||
Setting::create([
|
||||
'key' => 'ui.table_page_size',
|
||||
'value' => 50,
|
||||
'type' => 'integer',
|
||||
'group' => 'ui',
|
||||
]);
|
||||
|
||||
$service = app(SystemSettingService::class);
|
||||
|
||||
// config default = true, namun DB override -> false
|
||||
$this->assertFalse($service->get('asset.qr_enabled'));
|
||||
$this->assertSame(50, $service->get('ui.table_page_size'));
|
||||
}
|
||||
|
||||
public function test_it_sets_and_refreshes_cache_and_config(): void
|
||||
{
|
||||
$service = app(SystemSettingService::class);
|
||||
|
||||
$service->set('ui.date_format', 'Y-m-d');
|
||||
// Panggilan awal untuk memicu cache default.
|
||||
$this->assertFalse($service->get('maintenance.readonly_mode'));
|
||||
|
||||
$this->assertSame('Y-m-d', $service->get('ui.date_format'));
|
||||
$this->assertSame('Y-m-d', config('system.ui.date_format'));
|
||||
// Ubah via service, pastikan DB dan cache/config ter-update.
|
||||
$service->set('maintenance.readonly_mode', true);
|
||||
|
||||
$this->assertDatabaseHas('settings', [
|
||||
'key' => 'maintenance.readonly_mode',
|
||||
'value' => '1',
|
||||
'type' => 'boolean',
|
||||
]);
|
||||
|
||||
$this->assertTrue($service->get('maintenance.readonly_mode'));
|
||||
$this->assertTrue(config('system.maintenance.readonly_mode'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user