Files
2026-10-29 13:34:31 +07:00

155 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Utils;
/**
* Immutable-ish configuration bag built once at bootstrap from
* config.php defaults + .env overlay (see mergeEnv).
*
* Inject this class into controllers/middleware/models.
* Do not read $_ENV outside bootstrap/env merge.
*/
final class Config {
/** @var array<string,mixed> */
private $data;
/**
* Explicit map of environment variable name => dotted config path.
* Env wins when the variable is set and non-empty.
*
* @var array<string,string>
*/
public const ENV_MAP = [
'APP_ENV' => 'app.env',
'APP_DEBUG' => 'app.debug',
'FLIGHT_BASE_URL' => 'app.base_url',
'APP_TIMEZONE' => 'app.timezone',
'DB_DRIVER' => 'database.driver',
'DB_HOST' => 'database.host',
'DB_DATABASE' => 'database.dbname',
'DB_USERNAME' => 'database.user',
'DB_PASSWORD' => 'database.password',
'DB_SQLITE_PATH' => 'database.file_path',
'DB_SOCKET' => 'database.socket',
];
/**
* @param array<string,mixed> $data Merged configuration array
*/
public function __construct(array $data) {
$this->data = $data;
}
/**
* Get a value by dotted key path (e.g. "database.host").
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get(string $key, $default = null) {
$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;
}
/**
* @return array<string,mixed>
*/
public function all(): array {
return $this->data;
}
public function isDebug(): bool {
$debug = $this->get('app.debug', false);
if (is_bool($debug)) {
return $debug;
}
if (is_string($debug)) {
return in_array(strtolower($debug), ['1', 'true', 'yes', 'on'], true);
}
return (bool) $debug;
}
public function env(): string {
return (string) $this->get('app.env', 'production');
}
/**
* Application base URL with a trailing slash so templates can
* safely join paths: {{ base_url }}posts → /myapp/posts
*/
public function baseUrl(): string {
$url = (string) $this->get('app.base_url', '/');
if ($url === '') return '/';
return rtrim($url, '/');
}
/**
* Overlay mapped environment variables onto file config.
* Env wins when the variable is set and the string is non-empty.
*
* @param array<string,mixed> $fileConfig From config.php (literals only)
* @param array<string,mixed> $env Typically $_ENV after loadEnv
* @return array<string,mixed>
*/
public static function mergeEnv(array $fileConfig, array $env): array {
$merged = $fileConfig;
foreach (self::ENV_MAP as $envKey => $configPath) {
if (!array_key_exists($envKey, $env)) {
continue;
}
$raw = $env[$envKey];
if ($raw === null || $raw === '') {
continue;
}
$value = self::castEnvValue($configPath, (string) $raw);
self::setPath($merged, $configPath, $value);
}
return $merged;
}
/**
* @param string $path
* @param string $raw
* @return mixed
*/
private static function castEnvValue(string $path, string $raw) {
if ($path === 'app.debug') {
return in_array(strtolower($raw), ['1', 'true', 'yes', 'on'], true);
}
return $raw;
}
/**
* @param array<string,mixed> $data
* @param string $path Dotted path
* @param mixed $value
*/
private static function setPath(array &$data, string $path, $value): void {
$segments = explode('.', $path);
$ref = &$data;
foreach ($segments as $i => $segment) {
if ($i === count($segments) - 1) {
$ref[$segment] = $value;
return;
}
if (!isset($ref[$segment]) || !is_array($ref[$segment])) {
$ref[$segment] = [];
}
$ref = &$ref[$segment];
}
}
}