Configuration¶
Two independent pieces: loading a .env file into the real process
environment, and typed access to whatever’s in it.
.env loading¶
APP_ENV=production
DB_HOST=db.internal
DB_PORT=3306
DEBUG=false
Both public/index.php and bin/kinetis call
Kinetis\Config\EnvFile::safeLoad($projectRoot) unconditionally, before
Kinetis\Runtime\AppEnvironment::detect() — APP_ENV itself might be
defined for the first time in .env, not already set in the real process
environment.
.env is entirely optional and never an error to omit:
EnvFile::safeLoad() uses safeLoad(), not load() — a missing file is the
normal case for a deployment with real environment variables already set
by the platform, not something to throw over. A real environment variable
that’s already set always wins over .env too, so a checked-in
.env.example copied to .env locally can never accidentally override a
production secret set through Docker, systemd, or a secrets manager.
Note
No AppEnvironment check gates this — .env loading always runs, in
every environment, not just development. Traditional shared hosting and
plenty of FPM-only deployments give you only file access, with no way
to set a real process environment variable at all — no Docker, no systemd
unit you control, sometimes not even FPM pool config access. For that
shape of deployment, .env is the only way to configure the app in
production, not a dev-only convenience.
Typed config access¶
use Kinetis\Config\Config;
use Kinetis\Http\Attributes\Get;
final readonly class OrderController
{
public function __construct(
private Config $config,
) {}
#[Get('/orders')]
public function index(): array
{
$host = $this->config->string('DB_HOST', 'localhost');
$debug = $this->config->bool('DEBUG', false);
// ...
}
}
Kinetis\Config\Config is a plain snapshot of the environment, taken once
— not live getenv() calls scattered through your business logic.
Environment variables are worker-lifetime configuration, not per-request
state, so there’s nothing to keep re-reading for.
Method |
Returns |
|---|---|
|
The raw value, or |
|
Same as |
|
Cast to |
|
Cast to |
|
|
|
The raw value, or throws |
required() is for config with no sane default — a missing database
password should fail fast and clearly, not silently proceed as an empty
string and fail somewhere far less obvious later:
$password = $this->config->required('DB_PASSWORD');
// throws Kinetis\Config\Exception\MissingConfigException if unset
Named connections¶
Any storage technology — Redis, a SQL database, and any future one — can be configured more than once under a name, alongside the usual unnamed default connection. A name is inserted as a segment right after the variable’s own prefix:
REDIS_HOST=cache.internal # default
REDIS_CACHE2_HOST=cache2.internal # named "cache2"
DB_HOST=db.internal # default
DB_DB2_HOST=db2.internal # named "db2"
Config::scopedKey(string $key, string $connection = 'default'): string
is the one shared helper every technology’s connection builder — see
Persistence for RedisSimpleCache/SqlConnectionFactory — uses to
compute which exact variable to read:
Config::scopedKey('REDIS_HOST'); // 'REDIS_HOST'
Config::scopedKey('REDIS_HOST', 'cache2'); // 'REDIS_CACHE2_HOST'
'default' always resolves to the plain, unprefixed key — a connection
you never name behaves exactly as if this feature didn’t exist. A named
connection is never resolved automatically by constructor type-hinting;
retrieve it explicitly from the container, or construct it directly,
wherever it’s needed.
Resolving Config¶
Kinetis\Container\AppScope::boot() registers a Config singleton
automatically — Config::fromEnvironment() — unless you’ve already
registered your own:
use Kinetis\Config\Config;
use Kinetis\Container\AppScope;
$app = new AppScope();
$app->instance(Config::class, new Config(['DB_HOST' => 'test-db']));
$app->boot(); // your registration above is kept, not overwritten
Resolvable anywhere via constructor injection — including through
RequestScope, which delegates to this same AppScope-registered
instance automatically (see Container), the same as any other
service you never explicitly registered on RequestScope itself.
Registering services before boot: bootstrap.php¶
public/index.php, bin/kinetis, and kinetis/queue’s bin/queue each
construct a plain AppScope and call boot() on it — with no bindings of
your own registered yet. An optional bootstrap.php at your project root
is the place to register anything a controller, command, or job actually
needs before that lock happens:
<?php
declare(strict_types=1);
use Amp\Mysql\MysqlConnectionPool;
use Kinetis\Config\Config;
use Kinetis\Container\AppScope;
use Kinetis\Persistence\SqlConnectionFactory;
return static function (AppScope $app, Config $config): void {
$app->instance(MysqlConnectionPool::class, SqlConnectionFactory::fromConfig($config));
};
It returns a callable(AppScope, Config): void, run with $config
passed directly rather than resolved from $app — Config itself isn’t
registered on $app yet at this point, since boot() is what registers
the default one. Every entry point that supports bootstrap.php already
has a Config on hand to pass in, built the same way:
$config = Config::fromEnvironment();
$app->instance(Config::class, $config);
Kinetis\Cache\RoutesFile::loadBootstrap($projectRoot)($app, $config);
$app->boot();
Entirely optional — a project with no bootstrap.php at its root boots
exactly as if this feature didn’t exist.
What’s not cached¶
Config and .env are deliberately outside the AOT compilation Kinetis
builds for production (see Caching & AOT Compilation). That cache’s entire value
proposition is being reproducible from source code alone — delete it,
rebuild it, and you get back the identical artifact. Environment
variables break that by definition: the process that ran bin/kinetis build and the one serving requests later can legitimately have different
values injected into them. Baking .env into a compiled cache file would
mean a changed value silently does nothing until someone remembers to
rebuild the cache.
See also¶
Container —
AppScope’s registration-lock discipline, and howRequestScopedelegates to aConfigit never explicitly registered itself.Caching & AOT Compilation — the AOT cache’s reproducible-from-source invariant that keeps environment configuration out of it.
Appendix: System Layout — the
Kinetis\Confignamespace in the full system map.