feat: add app and database modules
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\SystemConfig;
|
||||
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class SettingFileUploader
|
||||
{
|
||||
private const FIXED_PATHS = [
|
||||
'app_logo' => 'assets/img/logo.png',
|
||||
'app_favicon' => 'assets/img/favicon.png',
|
||||
'maintenance_mode_image' => 'assets/img/maintenance.png',
|
||||
];
|
||||
|
||||
public function replace(string $key, UploadedFile $file, mixed $oldValue): ?string
|
||||
{
|
||||
$dir = public_path('assets/img');
|
||||
|
||||
if (! is_dir($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
}
|
||||
|
||||
if (isset(self::FIXED_PATHS[$key])) {
|
||||
$filename = basename(self::FIXED_PATHS[$key]);
|
||||
$file->move($dir, $filename);
|
||||
|
||||
return self::FIXED_PATHS[$key];
|
||||
}
|
||||
|
||||
return $file->store('uploads/settings', 'public');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\SystemConfig;
|
||||
|
||||
class SettingValueCaster
|
||||
{
|
||||
public static function normalize(mixed $value, string $type): mixed
|
||||
{
|
||||
return match ($type) {
|
||||
'bool' => filter_var($value, FILTER_VALIDATE_BOOL),
|
||||
'int' => $value === null || $value === '' ? null : (int) $value,
|
||||
'float' => $value === null || $value === '' ? null : (float) $value,
|
||||
'json' => is_array($value) ? $value : (json_decode((string) $value, true) ?: []),
|
||||
'image_path' => is_string($value) ? trim($value) : null,
|
||||
default => $value === null ? null : trim((string) $value),
|
||||
};
|
||||
}
|
||||
|
||||
public static function serialize(mixed $value): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value ? '1' : '0';
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return json_encode($value, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
return (string) $value;
|
||||
}
|
||||
|
||||
public static function deserialize(?string $value, string $type): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match ($type) {
|
||||
'bool' => $value === '1',
|
||||
'int' => (int) $value,
|
||||
'float' => (float) $value,
|
||||
'json' => json_decode($value, true) ?: [],
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
public static function isUnchanged(mixed $oldValue, mixed $newValue): bool
|
||||
{
|
||||
return self::serialize($oldValue) === self::serialize($newValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ============================================================
|
||||
*
|
||||
* @project biiproject
|
||||
*
|
||||
* @author Andika Debi Putra
|
||||
*
|
||||
* @email andikadebiputra@gmail.com
|
||||
*
|
||||
* @website https://biiproject.com
|
||||
*
|
||||
* @copyright Copyright (c) 2026 Andika Debi Putra
|
||||
* @license Proprietary - All Rights Reserved
|
||||
*
|
||||
* @version 1.0.0
|
||||
*
|
||||
* @created 2026-05-01
|
||||
* ============================================================
|
||||
*
|
||||
* Unauthorized copying, modification, distribution, or use
|
||||
* of this file is strictly prohibited without prior written
|
||||
* permission from the author.
|
||||
* ============================================================
|
||||
*/
|
||||
|
||||
namespace App\Services\SystemConfig;
|
||||
|
||||
use App\Models\SystemSettingRevision;
|
||||
use App\Repositories\SystemSettingRepository;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SystemConfigService
|
||||
{
|
||||
private const CACHE_KEY = 'system_settings.all';
|
||||
|
||||
private const PUBLIC_CACHE_KEY = 'system_settings.public';
|
||||
|
||||
private const CACHE_TTL_MINUTES = 60;
|
||||
|
||||
protected static ?array $resolvedSettings = null;
|
||||
|
||||
public function __construct(
|
||||
protected SystemSettingRepository $repository,
|
||||
protected SettingFileUploader $fileUploader = new SettingFileUploader,
|
||||
) {}
|
||||
|
||||
public static function definitions(): array
|
||||
{
|
||||
return SettingDefinitions::ALL;
|
||||
}
|
||||
|
||||
public function all(bool $forceRefresh = false): array
|
||||
{
|
||||
if ($forceRefresh) {
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
if (self::$resolvedSettings !== null) {
|
||||
return self::$resolvedSettings;
|
||||
}
|
||||
|
||||
try {
|
||||
return self::$resolvedSettings = Cache::remember(
|
||||
self::CACHE_KEY,
|
||||
now()->addMinutes(self::CACHE_TTL_MINUTES),
|
||||
fn () => $this->resolveAll(),
|
||||
);
|
||||
} catch (\Exception) {
|
||||
return self::$resolvedSettings = $this->resolveAll();
|
||||
}
|
||||
}
|
||||
|
||||
public function getPublicSettings(): array
|
||||
{
|
||||
try {
|
||||
return Cache::remember(
|
||||
self::PUBLIC_CACHE_KEY,
|
||||
now()->addMinutes(self::CACHE_TTL_MINUTES),
|
||||
fn () => $this->buildPublicSettings(),
|
||||
);
|
||||
} catch (\Exception) {
|
||||
return $this->buildPublicSettings();
|
||||
}
|
||||
}
|
||||
|
||||
public function grouped(): array
|
||||
{
|
||||
$all = $this->all();
|
||||
$grouped = [];
|
||||
|
||||
foreach (SettingDefinitions::ALL as $key => $meta) {
|
||||
$grouped[$meta['group']][$key] = $all[$key] ?? $meta['default'] ?? null;
|
||||
}
|
||||
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
if (! array_key_exists($key, SettingDefinitions::ALL)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $this->all()[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function update(array $input, array $files = [], ?int $actorId = null, ?Request $request = null): void
|
||||
{
|
||||
$current = $this->all();
|
||||
|
||||
foreach (SettingDefinitions::ALL as $key => $meta) {
|
||||
$oldValue = $current[$key] ?? $meta['default'] ?? null;
|
||||
$newValue = $this->resolveNewValue($key, $meta, $input, $files, $oldValue);
|
||||
|
||||
if (SettingValueCaster::isUnchanged($oldValue, $newValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$setting = $this->repository->upsert([
|
||||
'key' => $key,
|
||||
'value' => SettingValueCaster::serialize($newValue),
|
||||
'type' => $meta['type'],
|
||||
'group' => $meta['group'],
|
||||
'is_public' => (bool) $meta['is_public'],
|
||||
'description' => $meta['description'],
|
||||
'created_by' => $actorId,
|
||||
'updated_by' => $actorId,
|
||||
]);
|
||||
|
||||
$this->writeRevision($setting->id, $key, $oldValue, $newValue, $actorId, $request);
|
||||
}
|
||||
|
||||
$this->invalidateCache();
|
||||
}
|
||||
|
||||
protected function resolveNewValue(string $key, array $meta, array $input, array $files, mixed $oldValue): mixed
|
||||
{
|
||||
if ($meta['type'] === 'image_path') {
|
||||
return isset($files[$key])
|
||||
? $this->fileUploader->replace($key, $files[$key], $oldValue)
|
||||
: $oldValue;
|
||||
}
|
||||
|
||||
return array_key_exists($key, $input)
|
||||
? SettingValueCaster::normalize($input[$key], (string) $meta['type'])
|
||||
: $oldValue;
|
||||
}
|
||||
|
||||
protected function resolveAll(): array
|
||||
{
|
||||
try {
|
||||
if (! $this->repository->tableExists()) {
|
||||
return $this->definitionDefaults();
|
||||
}
|
||||
$rows = $this->repository->all();
|
||||
} catch (\Exception) {
|
||||
return $this->definitionDefaults();
|
||||
}
|
||||
|
||||
$resolved = [];
|
||||
foreach (SettingDefinitions::ALL as $key => $meta) {
|
||||
$record = $rows->firstWhere('key', $key);
|
||||
$resolved[$key] = $record
|
||||
? SettingValueCaster::deserialize($record->value, (string) $record->type)
|
||||
: ($meta['default'] ?? null);
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
protected function buildPublicSettings(): array
|
||||
{
|
||||
$all = $this->all();
|
||||
$public = [];
|
||||
|
||||
foreach (SettingDefinitions::ALL as $key => $meta) {
|
||||
if (! $meta['is_public']) {
|
||||
continue;
|
||||
}
|
||||
$public[$key] = $all[$key] ?? $meta['default'] ?? null;
|
||||
}
|
||||
|
||||
return $public;
|
||||
}
|
||||
|
||||
protected function definitionDefaults(): array
|
||||
{
|
||||
$defaults = [];
|
||||
foreach (SettingDefinitions::ALL as $key => $meta) {
|
||||
$defaults[$key] = $meta['default'] ?? null;
|
||||
}
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
protected function writeRevision(
|
||||
int $settingId,
|
||||
string $key,
|
||||
mixed $oldValue,
|
||||
mixed $newValue,
|
||||
?int $actorId,
|
||||
?Request $request,
|
||||
): void {
|
||||
SystemSettingRevision::query()->create([
|
||||
'system_setting_id' => $settingId,
|
||||
'key' => $key,
|
||||
'old_value' => $oldValue === null ? null : json_encode($oldValue, JSON_UNESCAPED_SLASHES),
|
||||
'new_value' => $newValue === null ? null : json_encode($newValue, JSON_UNESCAPED_SLASHES),
|
||||
'changed_by' => $actorId,
|
||||
'changed_ip' => $request?->ip(),
|
||||
'changed_agent' => $request?->userAgent(),
|
||||
'created_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function invalidateCache(): void
|
||||
{
|
||||
Cache::forget(self::CACHE_KEY);
|
||||
Cache::forget(self::PUBLIC_CACHE_KEY);
|
||||
self::$resolvedSettings = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user