Skip to content

PHP Tutorial Practical PHP Development

Separating Configuration from Code

Lesson 317 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

A clean application has three layers of “settings”:

  1. Environment (.env or server variables): secrets and per-machine values.
  2. Config files (config/*.php): turn environment values into a structured, typed array with defaults.
  3. Code: receives what it needs through constructors. It never reads the environment itself.

Layer 1: the environment

Text
PLAIN
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.

config/database.php
PHP
<?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",
];
config/app.php
PHP
<?php

return [
    "debug" => filter_var(getenv("APP_DEBUG") ?: "false", FILTER_VALIDATE_BOOLEAN),
    "name" => "Code Corner",
];

Layer 3: code receives what it needs

index.php
PHP
<?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();
Output
OUTPUT
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.

bootstrap.php
PHP
<?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

HabitProblem
getenv() inside classesHidden dependency, hard to test
A global $configAnything can change it
define("DB_HOST", ...) constants everywhereCannot be replaced in tests
Passwords in the codeThey end up in Git

A tested example of the layering

index.php
PHP
<?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"];
Output
OUTPUT
db.internal / shop

The host came from the environment, and the name used the default.

lightbulb
A good test
Can you create the main objects of your app in a test without setting any environment variable? If yes, your code and configuration are properly separated.