In PSR-3, why should a log message use {placeholder} tokens filled from the context array, and where does a caught exception belong?
answer
- message stays a static string
- {name} must match a context key
- logger escapes, caller does not
- context values must never throw
- the 'exception' key, any Throwable
basics
~20 sPSR-3 intends the message to be a static string with {name} placeholders and all variable data in the context array, so the logger can escape it per output format. A caught exception goes in the context under the 'exception' key.
solid answer
~50 sPSR-3 messages may contain placeholders such as `{user}` whose names must match keys in the `$context` array, delimited by single braces with no whitespace. The meta document states the intent: the message stays a static value and all variable data travels in the context. That keeps messages translatable and groupable, and it moves escaping to the logger, which knows whether the record ends up as HTML, JSON or a syslog line; callers should not pre-escape. Interpolation is optional for implementations, so structured loggers may keep the context as fields. An exception goes under the `'exception'` key, where the implementation can extract the stack trace; the errata say any `Throwable` is allowed there, and the implementation must still check the value's type. Context values of any kind must never make the logger throw or raise a PHP warning.
code
php · 18 lines<?php
declare(strict_types=1);
use Psr\Log\LoggerInterface;
final class RateFetcher
{
public function __construct(private LoggerInterface $logger) {}
public function logFailure(string $pair, string $source, \Throwable $e): void
{
$this->logger->error('Fetching {pair} from {source} failed', [
'pair' => $pair,
'source' => $source,
'exception' => $e, // any Throwable, under this exact key
]);
}
}go deeper
Remember the shape of a good call: a fixed message with {name} placeholders, and the values plus any exception in the context array.
Explain the static-message intent, why escaping belongs to the logger, the placeholder naming rules and the 'exception' key accepting any Throwable.
Enforce placeholder-based messages in review so log search and alert grouping work, and keep user input out of message templates.
Decide how far a codebase commits to structured logging: context field naming conventions, what is safe to log at all, and how that affects downstream search and retention.
## The rule in the specification **PSR-3** separates a log call into two parts: - the **message**: a string, or an object with `__toString()`; - the **context**: an array with any extra data. The message may contain **placeholders**. Their rules: 1. a placeholder name **must** correspond to a key in the context array; 2. it **must** be delimited by a single `{` and a single `}` with no whitespace inside; 3. names **should** use only `A-Z`, `a-z`, `0-9`, underscore and period; other characters are reserved for future use. Implementations **may** replace placeholders with context values; they are not required to. A logger that writes structured records can keep the message template as is and store the context values as separate fields. ## Why the message should stay static The PSR-3 meta document says the message passed to a logging method is intended to always be a **static value**, with any variable data provided through `$context` and referenced by a placeholder. It gives two reasons, and practice adds a third: | Reason | What goes wrong with interpolation by the caller | |---|---| | **Translation** | a message with data baked in cannot be looked up and localised | | **Escaping** | the caller cannot know whether the record will be shown as HTML, stored as JSON or sent as syslog, so it cannot escape correctly | | **Grouping** | `"Payment 81723 failed"` and `"Payment 81724 failed"` are different strings; `"Payment {id} failed"` is one message with many occurrences | The escaping point matters for security: context values often contain user input. The specification says users **should not** pre-escape placeholder values, because escaping is the implementation's job for each output format. ## The context must never break logging The context array can contain anything, and implementations **must** treat it leniently: a given value in the context must not throw an exception or raise any PHP error, warning or notice. An array that cannot be cast to a string, an object without `__toString()` or a resource must all be handled without failing the log call. The reference interpolation function in the specification simply skips values that cannot be turned into strings. ## Exceptions go under 'exception' If you pass an exception, it **must** be under the `'exception'` key. That convention lets an implementation extract a stack trace when its backend supports it. Two details: - PSR-3 was written when PHP only had `Exception`. Its errata say the key should be read as accepting any **`Throwable`**, so an `Error` such as a `TypeError` belongs there too. - Because the key may contain anything, the implementation must still verify it is actually a `Throwable` before using it as one. Putting only `$e->getMessage()` in the message loses the class, the trace and any previous exception chain. ## The exchange-rate library example ```php // Weak: data baked into the message, trace lost $this->logger->error("Fetching $pair from $url failed: " . $e->getMessage()); // PSR-3 style: static message, data in context, exception under its key $this->logger->error('Fetching {pair} from {source} failed', [ 'pair' => $pair, 'source' => $url, 'exception' => $e, ]); ``` The second call produces one groupable message, lets the logger escape `pair` and `source` for its output format, and keeps the full exception. ## Common mistakes - Using `%s` or `sprintf()` style markers: PSR-3 placeholders are brace-delimited names. - Writing `{ pair }` with spaces: whitespace between the braces and the name is not allowed. - Passing a placeholder with no matching context key: the name must correspond to a key. - Calling `htmlspecialchars()` on values before logging: pre-escaping is discouraged, and it corrupts JSON or plain-text outputs. - Building the context eagerly with expensive data when the level is usually filtered out; the specification suggests conditional logging may be better than a `NullLogger` when context creation is expensive. ## Where placeholders are replaced Because interpolation is optional, the same call can end up as `Fetching EURUSD from https://rates.example failed` in a plain-text file, or as a JSON record with the template and separate `pair` and `source` fields in a log collector. The caller writes the same code in both cases, which is exactly the portability PSR-3 aims for.
- Can a TypeError go under PSR-3's 'exception' context key?Yes. PSR-3 predates `Throwable`, and its errata say the `exception` key should be read as allowing any `Throwable`, which includes `Error` subclasses such as `TypeError`. Implementations must still check that the value really is a `Throwable` before using it as one.
- Is a PSR-3 implementation required to replace {placeholders} in the message?No. The specification says implementors MAY replace placeholders with context values. A structured logger can keep the template and store the context as separate fields. Callers write the same code either way, which is why they should not interpolate the data themselves.
saying these in an interview costs you the question
- Interpolates user data straight into the message string before logging.
- Puts the exception message in the log text and drops the exception object.
- Escapes context values with htmlspecialchars() before passing them to the logger.
- Uses %s markers and expects PSR-3 to fill them like sprintf().
- Believes the 'exception' key only accepts Exception, not Error.
- Assumes every PSR-3 logger must interpolate placeholders into the message.