All Snippets Snippet PHP

Environment Config Loader

.env JSON VAULT PARSING... MAIN APP Merging Sources into Immutable Process.env

Merge JSON config defaults with environment variable overrides and automatic type casting.

D
Kunwar "AKA" AJ Sharing what I have learned
Feb 20, 2026 2 min PHP

The Pattern

ConfigLoader.php
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,
        };
    }
}

What Happens Under the Hood

Let us trace what happens when config loads with an environment override:

execution-flow.txt
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.

Usage Example

usage.php
// 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.