Safe Nested Array Access
Access deeply nested array values using dot notation with graceful fallback on missing keys.
Merge JSON config defaults with environment variable overrides and automatic type casting.
class ConfigLoader
{
public static function load(string $jsonPath, string $envPrefix = 'APP'): array
{
// Load base config from JSON file
$config = json_decode(file_get_contents($jsonPath), true);
if ($config === null) {
throw new RuntimeException("Invalid config: {$jsonPath}");
}
// Override with environment variables
return self::mergeEnv($config, $envPrefix);
}
private static function mergeEnv(array $config, string $prefix, string $path = ''): array
{
foreach ($config as $key => $value) {
$envKey = strtoupper($prefix . '_' . ($path ? $path . '_' : '') . $key);
if (is_array($value)) {
$config[$key] = self::mergeEnv($value, $prefix, $path ? $path . '_' . $key : $key);
} elseif (($envValue = getenv($envKey)) !== false) {
$config[$key] = self::castType($envValue, $value);
}
}
return $config;
}
private static function castType(string $envValue, mixed $original): mixed
{
return match (true) {
is_bool($original) => filter_var($envValue, FILTER_VALIDATE_BOOLEAN),
is_int($original) => (int) $envValue,
default => $envValue,
};
}
}
Let us trace what happens when config loads with an environment override:
config.json: { "database": { "host": "localhost", "port": 3306 } }
Environment: APP_DATABASE_HOST=prod-db.example.com
Step 1: Load JSON → { "database": { "host": "localhost", "port": 3306 } }
Step 2: Walk through keys, check environment variables:
→ APP_DATABASE_HOST exists? → Yes: "prod-db.example.com"
→ Original is string, keep as string
→ Override: "localhost" → "prod-db.example.com"
→ APP_DATABASE_PORT exists? → No
→ Keep original: 3306
Result: { "database": { "host": "prod-db.example.com", "port": 3306 } }
The type casting is important. Environment variables are always strings, but your config might expect integers or booleans. The loader checks the original type and casts accordingly — “3306” becomes the integer 3306, “true” becomes the boolean true.
// Load config: JSON defaults + environment overrides
$config = ConfigLoader::load(__DIR__ . '/config.json', 'APP');
// In development: uses config.json values (localhost, debug=true)
// In production: environment variables override what needs to change
// APP_DATABASE_HOST=prod-db.example.com
// APP_DEBUG=false
$dbHost = $config['database']['host']; // "prod-db.example.com" in prod
One JSON file defines your complete config with sensible defaults. Environment variables override only what differs per environment. No separate config files per environment. No config management complexity. One source of truth with targeted overrides.