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

.env
# 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=
The real environment wins

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
<?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');

        // ...
    }
}
MethodBehaviour
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
<?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
<?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

Shell
$ ./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

.env
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

Errors never quote values

Parse failures name the key and the line number only. Exception messages travel into logs, error pages and bug reports.

Debug output is redacted

Environment::__debugInfo() returns key names and <redacted>. A var_dump in a stack trace cannot spill every credential the object holds.

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