*/ private array $data; private string $path; /** @param array $data */ private function __construct(string $path, array $data) { $this->path = $path; $this->data = array_replace_recursive(self::defaults(), $data); } public static function load(string $path): self { $data = []; if (is_file($path)) { $decoded = json_decode((string) file_get_contents($path), true); if (is_array($decoded)) { $data = $decoded; } } return new self($path, $data); } /** @return mixed */ public function get(string $key, mixed $default = null): mixed { $segments = explode('.', $key); $value = $this->data; foreach ($segments as $segment) { if (!is_array($value) || !array_key_exists($segment, $value)) { return $default; } $value = $value[$segment]; } return $value; } public function set(string $key, mixed $value): void { $segments = explode('.', $key); $cursor = &$this->data; foreach ($segments as $segment) { if (!isset($cursor[$segment]) || !is_array($cursor[$segment])) { $cursor[$segment] = []; } $cursor = &$cursor[$segment]; } $cursor = $value; } /** @return array */ public function all(): array { return $this->data; } public function save(): void { $dir = dirname($this->path); if (!is_dir($dir)) { mkdir($dir, 0775, true); } file_put_contents( $this->path, json_encode($this->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL ); } public function baseUrl(): string { $configured = trim((string) $this->get('base_url', '')); if ($configured !== '') { $parts = parse_url($configured); if (is_array($parts) && isset($parts['scheme'], $parts['host'])) { $port = isset($parts['port']) ? ':' . $parts['port'] : ''; return $parts['scheme'] . '://' . $parts['host'] . $port; } return rtrim($configured, '/'); } if (PHP_SAPI === 'cli') { return ''; } $https = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'); $scheme = $https ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; return $scheme . '://' . $host; } public function basePath(): string { $configured = trim((string) $this->get('base_path', '')); if ($configured !== '') { return self::normalizeBasePath($configured); } $baseUrl = trim((string) $this->get('base_url', '')); if ($baseUrl !== '') { $path = parse_url($baseUrl, PHP_URL_PATH); if (is_string($path) && $path !== '') { return self::normalizeBasePath($path); } } if (PHP_SAPI === 'cli') { return ''; } $scriptName = str_replace('\\', '/', (string) ($_SERVER['SCRIPT_NAME'] ?? '')); $scriptDir = dirname($scriptName); if ($scriptDir === '.' || $scriptDir === '/') { return ''; } return self::normalizeBasePath($scriptDir); } /** @return array */ private static function defaults(): array { return [ 'site' => [ 'title' => 'Neon Notes', 'tagline' => 'A fast file-first blog CMS', 'description' => 'A Markdown blog powered by PHP, SQLite, JSON, and plain files.', 'author' => 'Editor', 'language' => 'en', ], 'base_url' => '', 'base_path' => '', 'timezone' => 'UTC', 'theme' => 'neon', 'posts_per_page' => 6, 'comments' => [ 'enabled' => true, 'moderate' => true, 'captcha' => true, 'honeypot_field' => 'website_url', ], 'stats' => [ 'enabled' => true, 'hash_salt' => 'change-me', ], 'social' => [], ]; } private static function normalizeBasePath(string $path): string { $path = '/' . trim($path, '/'); return $path === '/' ? '' : $path; } }