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 and bin/kinetis each construct a plain AppScope
and call boot() on it — with no bindings of your own registered yet.
Two things run before that lock: any installed package’s own bootstrap
class (declared via extra.kinetis — see CLI — the way
kinetis/persistence and kinetis/queue bind a configured connection
and queue backend with no wiring of yours), then an optional
bootstrap.php at your project root, the place to register anything a
controller, command, or job actually needs — and to override any binding
a package made, since your registration runs last:
<?php
declare(strict_types=1);
use Kinetis\Persistence\Contract\MysqlLink;
use Kinetis\Config\Config;
use Kinetis\Container\AppScope;
use Kinetis\Persistence\SqlConnectionFactory;
return static function (AppScope $app, Config $config): void {
$app->instance(MysqlLink::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.
Reference: every key in one place¶
Everything Kinetis and its packages read from the environment, grouped
by subsystem. Keys marked scoped follow the named-connection
convention above — DB_HOST becomes DB_REPORTING_HOST for a
connection named reporting. Application-defined keys (a JWT_SECRET
your own bootstrap reads via Config::required(), for instance) are
yours to invent and aren’t listed here.
Application (core)¶
Key |
Default |
Purpose |
|---|---|---|
|
|
|
|
— |
Comma-separated |
|
|
Request-body cap in bytes, enforced against declared |
|
|
|
|
|
|
|
— |
|
|
— |
|
|
|
HSTS max-age in seconds. |
|
|
Appends |
|
|
Appends |
|
— |
|
|
— |
|
|
— |
|
Discovery restriction (core)¶
All optional; comma-separated sub-paths relative to each PSR-4 base directory, for large applications that want a bounded scan (see CLI).
Key |
Restricts the scan for |
|---|---|
|
HTTP controllers ( |
|
CLI commands ( |
|
MCP tools and resources ( |
|
Global middleware ( |
|
Event listeners ( |
Database (kinetis/persistence) — all scoped¶
Key |
Default |
Purpose |
|---|---|---|
|
(required) |
|
|
|
Server host. |
|
|
Per dialect. |
|
|
Database name. |
|
|
User. |
|
(required) |
Password. |
|
|
|
|
|
Connection charset. |
|
— |
MySQL collation ( |
|
— |
|
|
— |
CA bundle path for the verify modes. |
|
— |
Client certificate for mutual TLS; requires |
|
— |
Client private key; requires |
|
— |
Seconds. |
|
— |
Postgres |
|
— |
MySQL protocol compression. |
|
|
Async drivers’ pool width — per worker thread under FrankenPHP (see Performance tuning). |
|
|
Connections opened at boot instead of first use — load-bearing for the mysqli driver under worker mode. |
|
— |
Legacy key=value string, translated where canonical equivalents exist. |
Redis (kinetis/cache-redis) — all scoped¶
With none of REDIS_URL/REDIS_HOST/REDIS_CLUSTER set, Redis is
simply off and CacheInterface binds to NullSimpleCache.
Key |
Default |
Purpose |
|---|---|---|
|
— |
Full |
|
— |
Server host. |
|
|
Port. |
|
— |
Password. |
|
|
Database index (single-node only; Cluster has no |
|
|
Connect timeout, seconds. |
|
|
Connect over TLS. |
|
|
Verify the server certificate. |
|
— |
CA certificate for verification. |
|
|
Use Redis Cluster mode. |
|
— |
Comma-separated seed nodes for Cluster bootstrap. |
Queue (kinetis/queue + backend packages)¶
Read by kinetis queue:work and kinetis/queue’s package bootstrap;
the backend-specific keys are scoped.
Key |
Default |
Purpose |
|---|---|---|
|
(required) |
|
|
|
Which named |
|
|
Worker-level default attempts cap ( |
|
|
Seconds per |
|
— |
|
|
(required for sqs) |
AWS region. |
|
— |
SQS-compatible endpoint (LocalStack). |
|
— |
Queue-name prefix for shared AWS accounts. |
|
(required for rabbitmq) |
|
|
— |
Queue-name prefix. |
AWS credentials are deliberately never read from Config — the SQS
(and S3) clients use AWS’s own default credential provider chain.
Migrations (kinetis/migrations)¶
Read by the migrate* commands, which connect through the same DB_*
keys as persistence.
Key |
Default |
Purpose |
|---|---|---|
|
|
Which named |
File storage (kinetis/storage + kinetis/storage-s3) — all scoped¶
Key |
Default |
Purpose |
|---|---|---|
|
|
|
|
(required for local) |
Local disk root path. |
|
(required for s3) |
Bucket name. |
|
(required for s3) |
AWS region. |
|
— |
Key prefix. |
|
— |
S3-compatible endpoint (MinIO). |
|
|
Path-style addressing, needed by most non-AWS S3 services. |
Mail (kinetis/mailer) — scoped¶
Key |
Default |
Purpose |
|---|---|---|
|
(required) |
Symfony Mailer transport DSN ( |
Search (kinetis/search-opensearch) — all scoped¶
Key |
Default |
Purpose |
|---|---|---|
|
(required) |
Base URI of the node. |
|
— |
Basic-auth user. |
|
— |
Basic-auth password. |
|
|
Verify the server certificate. |
Sessions (kinetis/session)¶
Key |
Default |
Purpose |
|---|---|---|
|
— |
|
|
|
Seconds a session stays readable from its last write. |
|
|
Cookie name. A |
|
|
Cookie |
|
|
Cookie |
|
system temp |
The |
MCP (kinetis/mcp)¶
MCP_DISCOVERY_PATHS, in the discovery table above, also belongs to
this package.
Key |
Default |
Purpose |
|---|---|---|
|
(empty) |
Comma-separated exact |
Telemetry (kinetis/telemetry)¶
Key |
Default |
Purpose |
|---|---|---|
|
— |
Collector’s OTLP/HTTP base URL. Unset means tracing is off (no-op provider). |
|
|
The |
|
— |
Export-request headers, |
|
|
One of the standard OTel sampler names; |
|
|
Sampling ratio for the |
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.