A clean application has three layers of “settings”:
- Environment (
.envor server variables): secrets and per-machine values. - Config files (
config/*.php): turn environment values into a structured, typed array with defaults. - Code: receives what it needs through constructors. It never reads the environment itself.
Layer 1: the environment
DB_HOST=127.0.0.1
DB_NAME=shop
APP_DEBUG=true
Layer 2: config files
A config file returns an array. This is the only place that knows the variable names.
<?php
return [
"host" => getenv("DB_HOST") ?: "127.0.0.1",
"name" => getenv("DB_NAME") ?: "shop",
"user" => getenv("DB_USER") ?: "root",
"pass" => getenv("DB_PASS") ?: "",
"charset" => "utf8mb4",
];
<?php
return [
"debug" => filter_var(getenv("APP_DEBUG") ?: "false", FILTER_VALIDATE_BOOLEAN),
"name" => "Code Corner",
];
Layer 3: code receives what it needs
<?php
final class DatabaseSettings
{
public function __construct(
public readonly string $host,
public readonly string $name,
public readonly string $charset = "utf8mb4",
) {}
public function dsn(): string
{
return "mysql:host={$this->host};dbname={$this->name};charset={$this->charset}";
}
public static function fromArray(array $config): self
{
return new self($config["host"], $config["name"], $config["charset"] ?? "utf8mb4");
}
}
$settings = DatabaseSettings::fromArray(["host" => "127.0.0.1", "name" => "shop"]);
echo $settings->dsn();
mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4
The class has no idea that an environment exists. A test can create it with fake values.
The bootstrap file
One file builds the objects, once, at the start. It is the composition root.
<?php
declare(strict_types=1);
require __DIR__ . "/vendor/autoload.php";
$config = [
"app" => require __DIR__ . "/config/app.php",
"database" => require __DIR__ . "/config/database.php",
];
error_reporting(E_ALL);
ini_set("display_errors", $config["app"]["debug"] ? "1" : "0");
$db = $config["database"];
$pdo = new PDO(
"mysql:host={$db['host']};dbname={$db['name']};charset={$db['charset']}",
$db["user"],
$db["pass"],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
);
return [
"config" => $config,
"pdo" => $pdo,
];
Pages then start with $app = require __DIR__ . "/../bootstrap.php"; and pass $app["pdo"] to the classes that need it.
What to avoid
| Habit | Problem |
|---|---|
getenv() inside classes | Hidden dependency, hard to test |
A global $config | Anything can change it |
define("DB_HOST", ...) constants everywhere | Cannot be replaced in tests |
| Passwords in the code | They end up in Git |
A tested example of the layering
<?php
putenv("DB_HOST=db.internal");
$configFile = [
"host" => getenv("DB_HOST") ?: "127.0.0.1",
"name" => getenv("DB_NAME") ?: "shop",
];
echo $configFile["host"] . " / " . $configFile["name"];
db.internal / shop
The host came from the environment, and the name used the default.