Configuration
The .env file, typed access, and why the real environment always wins.
Configuration is read once at boot into an immutable Environment, registered as a singleton. A worker touches the filesystem once per process, and settings cannot drift mid-process — every request sees the same values.
The .env file
# Copy .env.example to .env. The file is git-ignored; the example is not.
APP_DEBUG=false
APP_URL=http://localhost:8080
LOG_LEVEL=info
DB_DATABASE=storage/app.sqlite
SESSION_LIFETIME=7200
SESSION_COOKIE=orbit_session
UPLOAD_MAX_BYTES=1048576
LOGIN_MAX_ATTEMPTS=5
LOGIN_WINDOW_SECONDS=900
TRUSTED_PROXIES=Values already present in the process environment override the file. The .env is a development convenience and a source of defaults; in production the values injected by systemd, Docker or Kubernetes are the ones that must apply, and a stale .env left on a server must never override them.
./orbit serve --debug works by setting APP_DEBUG in the process environment — the same rule, not an exception to it.
Reading settings
There is no get(): mixed. Everything in a .env is a string, and the conversion has to happen somewhere; doing it here means a bad value fails at boot with a readable message instead of behaving strangely later.
<?php
use PhpOrbit\Config\Environment;
final class ReportController implements Handler
{
public function __construct(private readonly Environment $config)
{
}
public function handle(ServerRequest $request): Response
{
$perPage = $this->config->int('REPORT_PAGE_SIZE', 50);
$verbose = $this->config->bool('REPORT_VERBOSE', false);
$title = $this->config->string('REPORT_TITLE', 'Monthly report');
// ...
}
}| Method | Behaviour |
|---|---|
string($key, $default = null) | Throws if absent and no default given. |
required($key) | Throws if absent or blank. Use for secrets. |
int($key, $default = null) | Throws unless the value is digits, optionally signed. |
bool($key, $default = null) | Accepts true/false, 1/0, yes/no, on/off. Anything else throws. |
strings($key, $default = []) | Comma-separated list, blanks dropped. |
path($key, $root, $default = null) | Resolves relative values against the project root. |
raw($key) | The literal string, or null. Blank and absent are distinguishable. |
has($key), keys() | Presence, and the key names (never values). |
required() versus string()
<?php
// APP_KEY= (present, but blank)
$config->string('APP_KEY'); // returns '' — present, technically
$config->required('APP_KEY'); // throws: "present but empty"For a secret those are the same problem, which is why required() exists.
path() and the working directory
<?php
// DB_DATABASE=storage/app.sqlite
$config->path('DB_DATABASE', $root); // /srv/app/storage/app.sqlite
// Absolute values and driver-specific ones pass through untouched.
// DB_DATABASE=/var/lib/app.sqlite → /var/lib/app.sqlite
// DB_DATABASE=:memory: → :memory:Without this, whether a relative path works depends on the directory the process happened to start in — which differs between the CLI, a web server and cron.
Typos fail at boot
$ ./orbit routes
Configuration error: Setting "APP_DEBUG" is not a valid boolean.
Accepted values: true/false, 1/0, yes/no, on/off.A typo'd APP_DEBUG=treu that silently became false would hide errors; one that became true would put stack traces in production. Neither is acceptable, so it refuses to start.
File syntax
PLAIN=no quotes needed
TRAILING=value # a comment needs a space before the hash
HASH=pass#word # no space, so this stays part of the value
QUOTED="escapes \n \t and ${OTHER_KEY} expansion"
LITERAL='no escapes, no expansion, $ and \ are ordinary'
MULTILINE="-----BEGIN KEY-----
a quoted value may span lines
-----END KEY-----"
export ALSO_FINE=1- Double quotes support
\n \r \t \\ \" \$and${VAR}expansion. - Single quotes are entirely literal — the right choice for a password containing backslashes or dollars.
${VAR}reads keys defined earlier in the file, or the real environment. An undefined reference is an error, not an empty string: silently expanding a password to nothing fails far from its cause.- A bare
$is left alone; only the braced form is a reference.
Two secrecy rules
Parse failures name the key and the line number only. Exception messages travel into logs, error pages and bug reports.
Environment::__debugInfo() returns key names and <redacted>. A var_dump in a stack trace cannot spill every credential the object holds.
<?php
$config = Environment::fromArray(['DB_PASSWORD' => 'hunter2']);
print_r($config);
// PhpOrbit\Config\Environment Object
// (
// [keys] => DB_PASSWORD
// [values] => <redacted>
// )Adding your own settings
Read them in app/bootstrap.php and pass concrete values into your services. Services should take what they need, not the whole Environment:
<?php
$app->container->singleton(
Mailer::class,
static fn (): Mailer => new Mailer(
host: $env->required('MAIL_HOST'),
port: $env->int('MAIL_PORT', 587),
timeout: $env->int('MAIL_TIMEOUT', 10),
),
);That way a missing setting fails at boot, and the service is testable without an environment at all.
Where .env lives
Beside composer.json, one level above public/, so it is unreachable even if a rewrite rule is misconfigured. ServeStaticFiles also refuses dotfiles outright — two independent protections.