Configuration
There is no config/ directory in Winter. Values live in
.env, the application’s make-up is declared with attributes on the
entry class, and everything else is described by ordinary classes the scanner
finds. Nothing has to be registered.
Three levels
Configuration is split across three places, and the division is simple.
| Where | What lives there | Example |
|---|---|---|
.env |
Values that change between environments | database host, log level |
| The application class | What the application consists of | #[EnableWeb], #[EnableScheduler] |
| Configurer classes | Behaviour expressed as code | CORS, server settings, beans |
Why there are no config files. Anything that is a value belongs to the environment and changes without a rebuild. Anything that is a decision — which implementation to use, how to assemble an object, which origins to allow — is expressed as code, where types and editor hints work. The intermediate layer of arrays in PHP files is deliberately absent.
.env — the values
The file is created from a template by php call cfg init and holds three lines:
WINTER_KEY=<generated key>
TIME_ZONE=UTC
DEBUG=true| Variable | Default | What it sets |
|---|---|---|
WINTER_KEY |
— | The project secret: signatures, tokens. Generated locally |
TIME_ZONE |
UTC |
Default timezone |
DEBUG |
false |
Development mode |
What `DEBUG` actually changes
It is not just “show errors”. The variable switches four things:
- error display — at
falsePHP hides them and a failing script goes quiet; - the error response body — stack traces and debug data reach the client only
at
true; - the class-list cache — at
falsethe project is scanned once and the result is written todi.php; attruethe cache is not used at all; #[Async]proxies — attruethey are regenerated when the source changes.
In production: false, always. At true the application’s internals leak
outwards.
Logging
| Variable | Default | What it sets |
|---|---|---|
LOG_LEVEL |
unset — output disabled | Minimum level to record |
LOG_OUTPUT |
auto |
auto, stdout, stderr, file, syslog, null |
LOG_FORMAT |
line |
line or json |
LOG_FILE |
storage/logs/<channel>.log |
Path when LOG_OUTPUT=file |
LOG_FILE_MAX |
30 |
How many days of rotated files to keep |
LOG_COLOR |
auto |
auto, always, never |
Any of them can be set for one channel with the LOG_{CHANNEL}_ prefix:
LOG_LEVEL=info
LOG_HTTP_LEVEL=warning # from requests — problems only
LOG_SYS_OUTPUT=file # system events into a fileMore on the Logging page.
Web server
These apply only when the application declares #[EnableWeb].
| Variable | What it sets |
|---|---|
SERVER_PROFILE |
Memory profile: stable, balance, performance, stress |
SERVER_WORKERS |
Number of worker processes |
SERVER_TASKS |
Number of Swoole task processes |
SERVER_MAX_CONCURRENCY |
Concurrent requests per worker |
SERVER_MAX_CONNECTIONS |
Concurrent connections |
SERVER_MAX_REQUEST |
Requests before a worker is replaced |
SERVER_MAX_REQUEST_GRACE |
Jitter so workers do not all recycle at once |
SERVER_MAX_REQUEST_SIZE |
Largest request in bytes |
SERVER_REQUEST_TIMEOUT |
Request deadline in seconds |
SERVER_IDLE_TIMEOUT |
When to close a silent connection |
SERVER_MEMORY_LIMIT |
Memory ceiling for each worker |
SERVER_MEMORY_TRIM |
When to return unused memory to the system |
What each one means and how they relate is on the
Web-layer configuration page. Address and port are
not set by variables — they arrive as the --host and --port flags.
Everything else
| Variable | What it sets |
|---|---|
WINTER_BANNER |
off removes the startup banner |
PPA_POOL_TELEMETRY |
How often pool statistics are published; 0 disables it |
Database variables are declared by you
The core does not read DB_HOST or anything like it — connecting is your
configuration class’s job, and which variables it asks for is your decision. The
convention is to read them through env(); see
Database.
The application manifest
What the application consists of is declared with attributes on the entry class:
#[EnableWeb]
#[EnableScheduler]
#[EnableProcess(\Main\Process\QueueWorker::class)]
final class Application extends WinterApplication
{
public static function main(array $argv): never
{
parent::run($argv);
}
}Every available attribute and how they combine is on the Components page.
The one hook — configure()
You need it only when the project layout is unusual: this method runs before the scan and decides where the scanner should look.
use Flytachi\Winter\Kernel\App\ApplicationArguments;
use Flytachi\Winter\Kernel\Kernel;
protected static function configure(ApplicationArguments $args): void
{
Kernel::init(
pathRoot: __DIR__,
pathResource: __DIR__ . '/assets',
pathStorage: '/var/lib/my-app',
);
}Without an override the paths are derived from where the application class sits, and for a normal layout there is no reason to touch it.
Configurer classes
Everything else is described by ordinary classes. They need no registration — the scanner finds them by the interface they implement or the attribute they carry.
| What to configure | With what |
|---|---|
Container bindings, factories, values from .env |
#[Configuration] with #[Bean] methods |
| CORS and web-server settings | A class extending WebConfigurerAdapter |
| Extra log channels | A class implementing LoggingConfigurer |
| Plugins | The #[Import] attribute on the application class |
| Diagnostic endpoints | The #[EnableActuator] attribute |
#[Configuration]
final class AppConfig
{
#[Bean]
public function cache(#[Value('REDIS_URL')] string $url): CacheInterface
{
return new RedisCache($url);
}
}Each has its own page: Dependency injection, Web-layer configuration, Logging, Packages.
Boot order
The same for every entry point — the server and a console command alike:
1. configure() — paths, .env, timezone, logging
2. Container — created empty
3. One project walk:
· classes carrying a scope → container
· #[Configuration] / #[Bean] → factories
· WebConfigurer, LoggingConfigurer → collected
· the scope graph → checked
4. #[Singleton] ↔ #[Request] conflict check
5. Apply what was found: logging, CORS, plugins, actuator
6. Then either the server or the console commandThe project walk is one, not one per kind of configuration. Two consequences follow:
- the order in which configurer classes are declared does not matter — they are collected in a single pass and applied afterwards;
- a new configurer is picked up by a restart, like everything else.
Scope conflicts are caught here
Step 4 walks the dependency graph and refuses to start the application when a
#[Singleton] holds a #[Request] object: the first request’s data would otherwise
be frozen inside it for the worker’s whole life. The log shows the chain
A → $prop: B.
Managing the key
php call cfg key -g # generate a new one
php call cfg key -s # show the current one
php call cfg env -s # show every environment variableNext
- Components — what the
#[Enable*]attributes declare - Dependency injection —
#[Configuration]and#[Bean] - Web-layer configuration — CORS and server settings
- Project structure — where things live