501 lines
13 KiB
PHP
501 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Repositories;
|
|
|
|
use App\Contracts\Repositories\ConfigRepositoryInterface;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
/**
|
|
* JSON 파일 기반 설정 저장소
|
|
*
|
|
* 설정을 카테고리별 JSON 파일로 관리합니다.
|
|
*/
|
|
class JsonConfigRepository implements ConfigRepositoryInterface
|
|
{
|
|
/**
|
|
* Storage disk 이름
|
|
*/
|
|
private const STORAGE_DISK = 'settings';
|
|
|
|
/**
|
|
* 백업 파일 저장 디렉토리 (disk 기준)
|
|
*/
|
|
private const BACKUP_DIR = 'backups';
|
|
|
|
/**
|
|
* 기본 설정 파일 경로
|
|
*/
|
|
private const DEFAULTS_FILE = 'settings/defaults.json';
|
|
|
|
/**
|
|
* 기본 설정 캐시
|
|
*
|
|
* @var array<string, mixed>|null
|
|
*/
|
|
private ?array $defaultsCache = null;
|
|
|
|
/**
|
|
* 메모리 캐시
|
|
*
|
|
* @var array<string, array<string, mixed>>|null
|
|
*/
|
|
private ?array $cache = null;
|
|
|
|
/**
|
|
* 모든 카테고리의 설정을 조회합니다.
|
|
*
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
public function all(): array
|
|
{
|
|
if ($this->cache !== null) {
|
|
return $this->cache;
|
|
}
|
|
|
|
$settings = [];
|
|
foreach ($this->getCategories() as $category) {
|
|
$settings[$category] = $this->getCategory($category);
|
|
}
|
|
|
|
$this->cache = $settings;
|
|
|
|
return $settings;
|
|
}
|
|
|
|
/**
|
|
* 특정 카테고리의 설정을 조회합니다.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function getCategory(string $category): array
|
|
{
|
|
if (! $this->categoryExists($category)) {
|
|
return $this->getDefaultsForCategory($category);
|
|
}
|
|
|
|
$path = $this->getCategoryPath($category);
|
|
|
|
if (! Storage::disk(self::STORAGE_DISK)->exists($path)) {
|
|
return $this->getDefaultsForCategory($category);
|
|
}
|
|
|
|
try {
|
|
$content = Storage::disk(self::STORAGE_DISK)->get($path);
|
|
$data = json_decode($content, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
Log::warning("설정 파일 JSON 파싱 실패: {$category}", [
|
|
'error' => json_last_error_msg(),
|
|
]);
|
|
|
|
return $this->getDefaultsForCategory($category);
|
|
}
|
|
|
|
// _meta 키 제거하고 반환
|
|
unset($data['_meta']);
|
|
|
|
// 기본값과 병합 (새 설정 키 자동 추가)
|
|
$defaults = $this->getDefaultsForCategory($category);
|
|
|
|
return array_merge($defaults, $data);
|
|
} catch (\Exception $e) {
|
|
Log::error("설정 파일 읽기 실패: {$category}", [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return $this->getDefaultsForCategory($category);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 도트 노테이션으로 특정 설정값을 조회합니다.
|
|
*
|
|
* @param string $key 예: 'mail.host', 'general.site_name'
|
|
*/
|
|
public function get(string $key, mixed $default = null): mixed
|
|
{
|
|
$parts = explode('.', $key, 2);
|
|
|
|
if (count($parts) === 1) {
|
|
// 카테고리만 지정된 경우
|
|
return $this->getCategory($key);
|
|
}
|
|
|
|
[$category, $settingKey] = $parts;
|
|
$categoryData = $this->getCategory($category);
|
|
|
|
return Arr::get($categoryData, $settingKey, $default);
|
|
}
|
|
|
|
/**
|
|
* 도트 노테이션으로 특정 설정값을 저장합니다.
|
|
*/
|
|
public function set(string $key, mixed $value): bool
|
|
{
|
|
$parts = explode('.', $key, 2);
|
|
|
|
if (count($parts) !== 2) {
|
|
return false;
|
|
}
|
|
|
|
[$category, $settingKey] = $parts;
|
|
$categoryData = $this->getCategory($category);
|
|
Arr::set($categoryData, $settingKey, $value);
|
|
|
|
return $this->saveCategory($category, $categoryData);
|
|
}
|
|
|
|
/**
|
|
* 여러 설정을 일괄 저장합니다.
|
|
*
|
|
* @param array<string, mixed> $settings
|
|
*/
|
|
public function setMany(array $settings): bool
|
|
{
|
|
$grouped = $this->groupByCategory($settings);
|
|
|
|
foreach ($grouped as $category => $categorySettings) {
|
|
$existing = $this->getCategory($category);
|
|
$merged = array_merge($existing, $categorySettings);
|
|
|
|
if (! $this->saveCategory($category, $merged)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 특정 카테고리의 설정을 저장합니다.
|
|
*
|
|
* @param array<string, mixed> $settings
|
|
*/
|
|
public function saveCategory(string $category, array $settings): bool
|
|
{
|
|
if (! $this->categoryExists($category)) {
|
|
return false;
|
|
}
|
|
|
|
$this->ensureDirectoryExists();
|
|
|
|
// 메타 정보 추가
|
|
$data = [
|
|
'_meta' => [
|
|
'version' => '1.0.0',
|
|
'updated_at' => now()->toIso8601String(),
|
|
],
|
|
...$settings,
|
|
];
|
|
|
|
$path = $this->getCategoryPath($category);
|
|
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
|
|
try {
|
|
// 파일 잠금으로 동시 쓰기 방지
|
|
$fullPath = Storage::disk(self::STORAGE_DISK)->path($path);
|
|
$handle = fopen($fullPath, 'c');
|
|
|
|
if ($handle === false) {
|
|
return false;
|
|
}
|
|
|
|
if (flock($handle, LOCK_EX)) {
|
|
ftruncate($handle, 0);
|
|
fwrite($handle, $json);
|
|
fflush($handle);
|
|
flock($handle, LOCK_UN);
|
|
}
|
|
|
|
fclose($handle);
|
|
|
|
// 캐시 무효화
|
|
$this->cache = null;
|
|
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
Log::error("설정 파일 저장 실패: {$category}", [
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 설정 키 존재 여부를 확인합니다.
|
|
*/
|
|
public function has(string $key): bool
|
|
{
|
|
return $this->get($key) !== null;
|
|
}
|
|
|
|
/**
|
|
* 특정 설정을 삭제합니다.
|
|
*/
|
|
public function delete(string $key): bool
|
|
{
|
|
$parts = explode('.', $key, 2);
|
|
|
|
if (count($parts) !== 2) {
|
|
return false;
|
|
}
|
|
|
|
[$category, $settingKey] = $parts;
|
|
$categoryData = $this->getCategory($category);
|
|
|
|
Arr::forget($categoryData, $settingKey);
|
|
|
|
return $this->saveCategory($category, $categoryData);
|
|
}
|
|
|
|
/**
|
|
* 사용 가능한 카테고리 목록을 반환합니다.
|
|
*
|
|
* config/settings/defaults.json의 _meta.categories에서 조회합니다.
|
|
*
|
|
* @return array<string>
|
|
*/
|
|
public function getCategories(): array
|
|
{
|
|
if ($this->defaultsCache === null) {
|
|
$this->loadDefaultsFile();
|
|
}
|
|
|
|
return $this->defaultsCache['_meta']['categories'] ?? [];
|
|
}
|
|
|
|
/**
|
|
* 카테고리 존재 여부를 확인합니다.
|
|
*/
|
|
public function categoryExists(string $category): bool
|
|
{
|
|
return in_array($category, $this->getCategories(), true);
|
|
}
|
|
|
|
/**
|
|
* 설정 파일을 초기화합니다.
|
|
*
|
|
* @param array<string, array<string, mixed>> $settings
|
|
*/
|
|
public function initialize(array $settings = []): bool
|
|
{
|
|
$this->ensureDirectoryExists();
|
|
|
|
$defaults = $this->getDefaults();
|
|
$merged = array_replace_recursive($defaults, $settings);
|
|
|
|
foreach ($merged as $category => $categorySettings) {
|
|
if (! $this->saveCategory($category, $categorySettings)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 설정을 백업합니다.
|
|
*
|
|
* @return string 백업 파일 경로
|
|
*/
|
|
public function backup(): string
|
|
{
|
|
$this->ensureBackupDirectoryExists();
|
|
|
|
$timestamp = now()->format('Y-m-d_His');
|
|
$backupName = "backup_{$timestamp}.zip";
|
|
$backupPath = self::BACKUP_DIR.'/'.$backupName;
|
|
|
|
$zip = new \ZipArchive;
|
|
$fullBackupPath = Storage::disk(self::STORAGE_DISK)->path($backupPath);
|
|
|
|
if ($zip->open($fullBackupPath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
|
|
throw new \RuntimeException(__('exceptions.settings.backup_creation_failed'));
|
|
}
|
|
|
|
foreach ($this->getCategories() as $category) {
|
|
$categoryPath = $this->getCategoryPath($category);
|
|
$fullCategoryPath = Storage::disk(self::STORAGE_DISK)->path($categoryPath);
|
|
|
|
if (file_exists($fullCategoryPath)) {
|
|
$zip->addFile($fullCategoryPath, "{$category}.json");
|
|
}
|
|
}
|
|
|
|
$zip->close();
|
|
|
|
return $backupPath;
|
|
}
|
|
|
|
/**
|
|
* 백업에서 설정을 복원합니다.
|
|
*/
|
|
public function restore(string $backupPath): bool
|
|
{
|
|
$fullBackupPath = Storage::disk(self::STORAGE_DISK)->path($backupPath);
|
|
|
|
if (! file_exists($fullBackupPath)) {
|
|
return false;
|
|
}
|
|
|
|
$zip = new \ZipArchive;
|
|
|
|
if ($zip->open($fullBackupPath) !== true) {
|
|
return false;
|
|
}
|
|
|
|
$this->ensureDirectoryExists();
|
|
|
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
|
$filename = $zip->getNameIndex($i);
|
|
$category = pathinfo($filename, PATHINFO_FILENAME);
|
|
|
|
if ($this->categoryExists($category)) {
|
|
$content = $zip->getFromIndex($i);
|
|
$categoryPath = $this->getCategoryPath($category);
|
|
Storage::disk(self::STORAGE_DISK)->put($categoryPath, $content);
|
|
}
|
|
}
|
|
|
|
$zip->close();
|
|
$this->cache = null;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 기본 설정값을 반환합니다.
|
|
*
|
|
* config/settings/defaults.json 파일에서 기본값을 로드합니다.
|
|
*
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
public function getDefaults(): array
|
|
{
|
|
if ($this->defaultsCache !== null) {
|
|
return $this->defaultsCache['defaults'] ?? [];
|
|
}
|
|
|
|
$this->loadDefaultsFile();
|
|
|
|
return $this->defaultsCache['defaults'] ?? [];
|
|
}
|
|
|
|
/**
|
|
* 프론트엔드 스키마를 반환합니다.
|
|
*
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
public function getFrontendSchema(): array
|
|
{
|
|
if ($this->defaultsCache === null) {
|
|
$this->loadDefaultsFile();
|
|
}
|
|
|
|
return $this->defaultsCache['frontend_schema'] ?? [];
|
|
}
|
|
|
|
/**
|
|
* 기본 설정 파일을 로드합니다.
|
|
*/
|
|
private function loadDefaultsFile(): void
|
|
{
|
|
$path = config_path(self::DEFAULTS_FILE);
|
|
|
|
if (! file_exists($path)) {
|
|
Log::error('설정 기본값 파일을 찾을 수 없습니다. settings:install 명령을 실행하세요.', ['path' => $path]);
|
|
$this->defaultsCache = ['_meta' => ['categories' => []], 'defaults' => [], 'frontend_schema' => []];
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
$content = file_get_contents($path);
|
|
$data = json_decode($content, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
Log::error('설정 기본값 파일 JSON 파싱 실패', [
|
|
'error' => json_last_error_msg(),
|
|
]);
|
|
$this->defaultsCache = ['_meta' => ['categories' => []], 'defaults' => [], 'frontend_schema' => []];
|
|
|
|
return;
|
|
}
|
|
|
|
$this->defaultsCache = $data;
|
|
} catch (\Exception $e) {
|
|
Log::error('설정 기본값 파일 로드 실패', ['error' => $e->getMessage()]);
|
|
$this->defaultsCache = ['_meta' => ['categories' => []], 'defaults' => [], 'frontend_schema' => []];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 카테고리의 기본값을 반환합니다.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function getDefaultsForCategory(string $category): array
|
|
{
|
|
$defaults = $this->getDefaults();
|
|
|
|
return $defaults[$category] ?? [];
|
|
}
|
|
|
|
/**
|
|
* 카테고리 파일 경로를 반환합니다.
|
|
*/
|
|
private function getCategoryPath(string $category): string
|
|
{
|
|
return "{$category}.json";
|
|
}
|
|
|
|
/**
|
|
* 설정 디렉토리가 존재하는지 확인하고 없으면 생성합니다.
|
|
*/
|
|
private function ensureDirectoryExists(): void
|
|
{
|
|
$disk = Storage::disk(self::STORAGE_DISK);
|
|
|
|
if (! $disk->exists('.')) {
|
|
$disk->makeDirectory('.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 백업 디렉토리가 존재하는지 확인하고 없으면 생성합니다.
|
|
*/
|
|
private function ensureBackupDirectoryExists(): void
|
|
{
|
|
$disk = Storage::disk(self::STORAGE_DISK);
|
|
|
|
if (! $disk->exists(self::BACKUP_DIR)) {
|
|
$disk->makeDirectory(self::BACKUP_DIR);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 설정을 카테고리별로 그룹화합니다.
|
|
*
|
|
* @param array<string, mixed> $settings
|
|
* @return array<string, array<string, mixed>>
|
|
*/
|
|
private function groupByCategory(array $settings): array
|
|
{
|
|
$grouped = [];
|
|
|
|
foreach ($settings as $key => $value) {
|
|
$parts = explode('.', $key, 2);
|
|
|
|
if (count($parts) === 2) {
|
|
[$category, $settingKey] = $parts;
|
|
$grouped[$category][$settingKey] = $value;
|
|
}
|
|
}
|
|
|
|
return $grouped;
|
|
}
|
|
}
|