Monolog is optional
Winter Logger is a thin layer over Monolog — yet Monolog is only a suggested dependency. Without it, every logger becomes a no-op. This is a design decision, not an oversight, and it shapes how the library behaves.
The gate
LoggerManager::build() checks for Monolog before doing anything else. If the class isn’t
loadable, it returns a PSR-3 NullLogger and stops:
private function build(string $channel): LoggerInterface
{
if (!class_exists(MonologLogger::class)) {
return new NullLogger();
}
// ... otherwise build the real Monolog channel
}Because channel() caches per name, the check runs once per channel. Every logging call
afterward hits a NullLogger, whose methods are empty — the cost of logging with Monolog
absent is effectively zero, and nothing throws.
Why suggest, not require
Monolog is declared under suggest in composer.json, so composer require flytachi/winter-logger
alone never pulls it in. That gives consumers a real choice:
- Libraries that support logging optionally can depend on Winter Logger without forcing Monolog onto every downstream app.
- Microservices can toggle logging per environment by including or excluding one package — no config flag, no dead code.
- Tests can drop Monolog to silence output entirely.
monolog/monolog installed → real channels → formatted output
monolog/monolog absent → NullLogger → silent no-op, no errorThe trade-off: silent, not loud
The cost of “logging turns off by not installing a package” is that a forgotten install looks identical to an intentional one — logs simply don’t appear, with no warning. That’s the accepted trade-off: the library treats “no Monolog” as a valid, supported state rather than an error to shout about.
If your logs vanished
No output and no error usually means Monolog isn’t installed. Run
composer require monolog/monolog and the same code starts producing lines — no other change
needed.
How it fits the philosophy
This mirrors the library’s central rule: it makes no assumptions about its environment. It won’t assume Monolog is present, just as it won’t read env vars or detect the runtime. Both choices push responsibility outward — to the app and framework that do know their environment — and keep the logger itself small, portable, and trivial to test.
The LoggerFactory respects the same gate:
when the underlying channel isn’t a real Logger (i.e. it’s a NullLogger or a custom PSR-3
impl), getLogger() returns it as-is instead of wrapping it — so per-class naming degrades
gracefully too.