Configuration issues usually arise gradually. Initially, the database connection is written directly in one PHP file. Then, an API key is added to another file. After that, service addresses, debug modes, and admin passwords are also scattered throughout the code. The application continues to run, but every server switch or credential change becomes a prone-to-error task.
Healthy configuration allows the application code to be unaware of whether it is running on a developer's laptop, a staging server, or in production. The code reads values provided by the environment in which the application runs. In PHP, one common way to retrieve these values is through the getenv() function, which returns the value of an environment variable or false if the variable is not available.
Differentiate between regular settings and secrets
Not all configurations carry the same level of risk. Application names, time zones, or display modes may not be secrets. Conversely, database passwords, API tokens, private keys, and payment service credentials should be treated as secrets.
This distinction is important because regular settings can still be stored in project configurations, while secrets should be provided through environment variables, secret management systems, or deployment mechanisms with access restrictions. OWASP also recommends that secrets have a clear lifespan, can be revoked, are not logged, and are only accessible to those who truly need them.
Do not assume that a .env file is automatically secure. This file is just one way to load configurations. If it gets committed to Git, is readable by the web server, included in unprotected backups, or accessible to too many people, secrets remain at risk of leaking.
Centralize configuration retrieval
Instead of calling getenv() in dozens of files, create a single configuration layer. This way, variable names, default values, validations, and required rules are all in one place.
<?php
function requiredEnv(string $name): string
{
$value = getenv($name);
if ($value === false || trim($value) === '') {
throw new RuntimeException("Required configuration is not available: {$name}");
}
return $value;
}
$config = [
'app_env' => getenv('APP_ENV') ?: 'production',
'app_debug' => filter_var(
getenv('APP_DEBUG') ?: 'false',
FILTER_VALIDATE_BOOLEAN
),
'db_host' => requiredEnv('DB_HOST'),
'db_name' => requiredEnv('DB_NAME'),
'db_user' => requiredEnv('DB_USER'),
'db_password' => requiredEnv('DB_PASSWORD'),
];This example does two important things. First, the application fails early if critical configurations are not available. Second, developers do not have to guess whether an empty password means it was intentional or a deployment error.
Validation also needs to match data types. Values like APP_DEBUG should be converted to boolean, rather than loosely compared to strings. If the application has ports, timeouts, or upload size limits, validate them as numbers and check if the values fall within a reasonable range.
Do not mix configuration with database connections
Configuration files should only consist of values. The process of creating a database connection can be placed in another layer. This separation makes testing easier and reduces the likelihood of passwords being scattered throughout the code.
<?php
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=utf8mb4',
$config['db_host'],
$config['db_name']
);
$pdo = new PDO($dsn, $config['db_user'], $config['db_password'], [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);With this pattern, controllers or services do not need to know where the password comes from. They simply receive a connection object or service that is ready to use. This is not just about security, but also about architecture: each part of the application has clearer responsibilities.
Be aware of invisible leaks
Secrets often leak not because they are intentionally displayed on pages, but because they end up in other places. Common sources of leaks include:
- Error messages that display the complete connection string.
- Request logs that record the
Authorizationheader or sensitive payloads. - Active
phpinfo()output in production. - Dumping configuration variables during debugging.
- Backup files, terminal screenshots, and CI/CD artifacts.
- Git repositories along with their old commit histories.
Therefore, avoid printing the entire configuration array when troubleshooting. Create functions to mask sensitive values or display only the names of available variables. If a secret has already entered the repository, simply deleting the latest line is not enough. Those credentials need to be considered leaked and rotated immediately.
Use defaults with caution
Defaults are useful for safe values, such as time zones or local environment names. However, defaults are dangerous if used for passwords, tokens, or security configurations.
An example to avoid is:
$dbPassword = getenv('DB_PASSWORD') ?: 'password123';This code makes the application appear to run successfully when important configurations are actually not available. It is better for the application to stop with a clear message than to run using weak credentials that may be forgotten.
For debug modes, defaults also need to be considered. On local machines, debug can aid development. In production, error details should be hidden from users and directed to a logging system with restricted access.
Design a consistent deployment flow
Good configuration does not stop at code. Teams need to determine where each value comes from in each environment. For example, developers use local files that are not committed, staging uses secrets from CI/CD, while production uses a secret manager or the cloud platform's secret facilities.
Use consistent variable names, document required variables, and provide examples without secret values like .env.example. This example should contain:
APP_ENV=local
APP_DEBUG=true
DB_HOST=127.0.0.1
DB_NAME=database_name
DB_USER=user_name
DB_PASSWORD=Ensure the example file does not contain real passwords. Add rules to ignore local files in .gitignore, but do not rely solely on .gitignore for protection. Access to servers, pipelines, backups, and secret managers must still be restricted.
What does this mean for us?
Separating configuration from code makes applications easier to move, test, and recover when credentials need to be changed. However, environment variables are not a magic solution. These values can still be read by processes, pipelines, or administrators with overly broad access.
Practical steps that can be taken now include creating a centralized configuration, marking which variables are required, removing secrets from source code, checking logs to ensure credentials are not recorded, and testing deployments with intentionally incomplete configurations. If the application fails with a clear message and does not leak sensitive values, your configuration foundation is already much better.
Sources & further reading
β Rio Yotto @rioyotto
