PSR-3: Logger Interface
debt(d5/e5/b5/t5)
Closest to 'specialist tool catches it' (d5). phpstan/phpcs/semgrep (from detection_hints) can flag error_log() calls or concrete Monolog type-hints, but only when configured with rules; default linter won't catch it.
Closest to 'touches multiple files / significant refactor' (e5). Quick_fix says type-hint LoggerInterface in all classes and inject — that's a sweep across every service constructor, not a one-line change, especially when replacing static facades or error_log() calls.
Closest to 'persistent productivity tax' (b5). Logger is reached across web/cli/queue contexts (applies_to); the interface choice shapes constructor signatures everywhere but is a well-known standard so doesn't define system shape.
Closest to 'notable trap most devs eventually learn' (t5). Misconception cites that accepting any logger isn't PSR-3 — the eight severity methods and context array convention are documented gotchas, and the context-array-vs-string-interpolation mistake is the classic learned lesson.
Also Known As
TL;DR
Explanation
PSR-3 defines a standard LoggerInterface that any logging library can implement, allowing application code to type-hint LoggerInterface rather than a specific library. Methods map to RFC 5424 severity levels: emergency, alert, critical, error, warning, notice, info, debug. Each accepts a message string and optional context array (interpolated as {key} placeholders). Monolog is the most widely used PSR-3 implementation — it supports structured logging, multiple handlers (file, Slack, Datadog, Sentry), and formatters. Libraries and frameworks that accept a LoggerInterface become logger-agnostic.
Common Misconception
Why It Matters
Common Mistakes
- Type-hinting against a concrete logger class instead of LoggerInterface — locks the codebase to one library.
- Not using the context array parameter — embedding variables in the message string prevents structured log parsing.
- Using the wrong log level — debug for production-visible events, or error for expected business conditions.
- Not injecting the logger — using a static logger facade prevents swapping for a NullLogger in tests.
Code Examples
// Concrete dependency — cannot swap logger:
public function __construct(private Monolog\Logger $logger) {}
// Not using context array — unstructured log:
$this->logger->info('User ' . $userId . ' logged in from ' . $ip);
// Correct:
public function __construct(private Psr\Log\LoggerInterface $logger) {}
$this->logger->info('User logged in', ['user_id' => $userId, 'ip' => $ip]);
// PSR-3 Logger — always inject the interface, not a concrete logger
use Psr\Log\LoggerInterface;
class OrderService {
public function __construct(private LoggerInterface $logger) {}
public function place(Cart $cart): Order {
$this->logger->info('Placing order', ['cart_id' => $cart->id]);
try {
$order = $this->createOrder($cart);
$this->logger->info('Order placed', ['order_id' => $order->id]);
return $order;
} catch (\Throwable $e) {
$this->logger->error('Order failed', [
'cart_id' => $cart->id,
'exception' => $e->getMessage(),
]);
throw $e;
}
}
}
// Levels: emergency, alert, critical, error, warning, notice, info, debug