Package · logger

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:

php
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.
text
monolog/monolog installed   →  real channels  →  formatted output
monolog/monolog absent      →  NullLogger     →  silent no-op, no error

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

See also