/visit-private (e.g. /home/USER/visit-private) * 3. /private (local development only) */ final class Config { /** @var array */ private static array $values = []; private static ?string $privateDir = null; private static bool $loaded = false; public static function load(): void { if (self::$loaded) { return; } self::$loaded = true; $dir = self::privateDir(); $env = $dir . '/.env'; if (is_readable($env)) { foreach (file($env, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) { $line = trim($line); if ($line === '' || $line[0] === '#' || !str_contains($line, '=')) { continue; } [$k, $v] = explode('=', $line, 2); $k = trim($k); $v = trim($v); if (str_starts_with($k, 'export ')) { $k = trim(substr($k, 7)); } if ((str_starts_with($v, '"') && str_ends_with($v, '"')) || (str_starts_with($v, "'") && str_ends_with($v, "'"))) { $v = substr($v, 1, -1); } self::$values[$k] = $v; } } } public static function privateDir(): string { if (self::$privateDir !== null) { return self::$privateDir; } $candidates = []; $fromEnv = getenv('VISIT_PRIVATE_DIR') ?: ($_SERVER['VISIT_PRIVATE_DIR'] ?? ($_SERVER['REDIRECT_VISIT_PRIVATE_DIR'] ?? null)); if ($fromEnv) { $candidates[] = $fromEnv; } if (!empty($_SERVER['DOCUMENT_ROOT'])) { $candidates[] = dirname(rtrim($_SERVER['DOCUMENT_ROOT'], '/')) . '/visit-private'; } $candidates[] = dirname(__DIR__, 3) . '/private'; // repo/private (dev) foreach ($candidates as $c) { if (is_dir($c)) { return self::$privateDir = rtrim($c, '/'); } } // Fall back to the first candidate so error messages point somewhere useful. return self::$privateDir = rtrim($candidates[0], '/'); } public static function get(string $key, ?string $default = null): ?string { $env = getenv($key); if ($env !== false && $env !== '') { return $env; } return (isset(self::$values[$key]) && self::$values[$key] !== '') ? self::$values[$key] : $default; } public static function require(string $key): string { $v = self::get($key); if ($v === null) { throw new RuntimeException("Missing required configuration: $key"); } return $v; } public static function bool(string $key, bool $default = false): bool { $v = self::get($key); if ($v === null) { return $default; } return in_array(strtolower($v), ['1', 'true', 'yes', 'on'], true); } public static function int(string $key, int $default): int { $v = self::get($key); return $v === null ? $default : (int)$v; } /** "mock" (default when unset), "live". */ public static function mode(): string { return strtolower(self::get('INTEGRATION_MODE', 'mock') ?? 'mock') === 'live' ? 'live' : 'mock'; } public static function isMock(): bool { return self::mode() === 'mock'; } public static function dataDir(): string { $d = self::get('DATA_DIR') ?: self::privateDir() . '/data'; if (!is_dir($d)) { @mkdir($d, 0750, true); } return $d; } }