When writing a framework-agnostic PHP library that should log and cache, how do you depend on PSR-3 and PSR-16 without forcing either on the application?
answer
- require psr/log and psr/simple-cache only
- constructor injection of the interfaces
- NullLogger as the default
- cache failures degrade to a miss
- LoggerAwareInterface for setter wiring
basics
~10 sRequire only psr/log and psr/simple-cache, accept LoggerInterface and CacheInterface in the constructor, default the logger to NullLogger and make the cache optional. The application injects its own implementations; the library never names a vendor.
solid answer
~40 sThe library's `composer.json` requires the interface packages `psr/log` and `psr/simple-cache`, never a concrete logger or cache. Its constructor accepts `Psr\Log\LoggerInterface` and `Psr\SimpleCache\CacheInterface`, with `new NullLogger()` as the logger default and a nullable cache that simply means no caching. Code then calls the logger unconditionally and guards only the cache. PSR-16's `set()` reports a failed write by returning `false`, so the library logs that and carries on with the fetched value: caching stays an optimisation, never a requirement. `LoggerAwareInterface` with `LoggerAwareTrait` adds `setLogger()` for containers that wire loggers by setter, but a constructor parameter keeps the dependency visible. The application then injects whatever logger and cache it already uses.
code
php · 31 lines<?php
declare(strict_types=1);
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Psr\SimpleCache\CacheInterface;
final class ExchangeRates
{
public function __construct(
private RateSource $source,
private ?CacheInterface $cache = null,
private LoggerInterface $logger = new NullLogger(),
private int $ttlSeconds = 600,
) {}
public function rate(string $pair): float
{
$key = 'rate.' . $pair;
$cached = $this->cache?->get($key);
if (is_float($cached)) {
return $cached;
}
$rate = $this->source->fetch($pair);
if ($this->cache !== null && !$this->cache->set($key, $rate, $this->ttlSeconds)) {
$this->logger->warning('Could not cache rate for {pair}', ['pair' => $pair]);
}
return $rate;
}
}go deeper
Know that a library should ask for LoggerInterface and a cache interface, not a specific logging or caching package.
Explain the NullLogger default, why the cache is optional, and how LoggerAwareInterface differs from constructor injection.
Design the library so logging and caching failures never break its main operation, choose PSR-16 or PSR-6 by need, and test with fakes.
Set the rules packages follow across a codebase for optional infrastructure dependencies, balancing ease of adoption against observability guarantees.
## The goal A reusable library, such as an exchange-rate client, benefits from logging (why a fetch failed) and caching (not calling the remote API on every request). But if it requires a specific logging or caching package, every application that installs it inherits that choice, possibly in a conflicting version. **PSR-3** and **PSR-16** exist so the library can ask for the capability without choosing the implementation. ## Dependencies in composer.json - Require `psr/log` for `LoggerInterface`, `NullLogger` and `LogLevel`. - Require `psr/simple-cache` for `CacheInterface`; choose `psr/cache` instead only if the library needs PSR-6 features such as deferred saves or caching `null`. - Do **not** require any concrete logger or cache package. The application brings its own. - Allow the widest package range your supported PHP versions can use, since both packages have typed 2.x and 3.x lines that applications may already be locked to. ## Constructor design | Dependency | Parameter | Default | Effect of the default | |---|---|---|---| | Logger | `LoggerInterface $logger` | `new NullLogger()` | records are discarded | | Cache | `?CacheInterface $cache` | `null` | no caching; every call goes to the API | | TTL | `int $ttlSeconds` | e.g. `600` | how long rates stay fresh | PHP 8.1 and later allow `new` in parameter default values, so the logger default can be written directly in a promoted constructor parameter. A `NullLogger` default lets the rest of the code call `$this->logger->warning(...)` without `null` checks. The cache stays nullable instead of defaulting to an in-memory cache, because an in-memory cache inside a short-lived PHP request is rarely what the application expects. ## Setter wiring with LoggerAwareInterface `Psr\Log\LoggerAwareInterface` declares a single `setLogger(LoggerInterface $logger)` method, and `Psr\Log\LoggerAwareTrait` implements it with a `$this->logger` property. The specification describes it as a way for frameworks to auto-wire arbitrary instances with a logger. It is useful when a container is configured that way, but: - the dependency becomes invisible in the constructor; - the property holds no logger until `setLogger()` runs, so code must not assume one exists before that. Constructor injection with a `NullLogger` default is the clearer choice for a library; implementing `LoggerAwareInterface` in addition costs little if some consumers want it. ## Treating the cache as an optimisation PSR-6 says an error in a cache system should not result in application failure: implementations may only throw the exceptions the interfaces define and should trap errors from the underlying store. PSR-16 does not repeat that section, but its write methods return booleans, and it describes itself as a convenience layer over PSR-6. For the library that means: 1. a `false` from `set()`, `delete()` or `setMultiple()` is logged and the call continues with the fetched value; 2. the one exception to expect in normal use is the invalid-argument exception for an illegal key, which is a bug in the library to fix, not to catch; 3. a missing or unreadable entry is simply a miss, and the library fetches from the API as if uncached. ## Logging well from a library - Use static messages with `{placeholders}` and pass data in the context. - Put caught exceptions under the `'exception'` key. - Choose levels from the library's point of view: a failed fetch with a stale fallback is a `warning`, a failed fetch with no fallback is an `error`. Leave `alert` and `emergency` to the application. - Never log secrets such as API keys, even at `debug`. ## Testing Tests pass a small in-memory `CacheInterface` fake and either `NullLogger` or a recording logger built on `AbstractLogger`, which only needs `log()` implemented. That lets a test assert that a failed fetch logs one `warning` with the right context and that a second call is served from the cache. ## What this buys the application The application installs its own logger and cache, wires them in once, and the library's records land in the same place as everything else, under the same routing, filtering and retention rules. Swapping the cache backend later needs no change to the library.
- Why default a library's logger to NullLogger instead of making it nullable?With `new NullLogger()` as the default, every log call can be written unconditionally, with no `null` checks scattered through the code. The specification offers `NullLogger` exactly as a fallback black hole, and notes that conditional logging may still be better where building the context is expensive.
- What should the library do if Psr\SimpleCache\CacheInterface::set() returns false?Continue with the freshly fetched value and log the failure with placeholders. `set()` returns a boolean precisely so a failed write can be reported without an exception, and PSR-6, which PSR-16 layers over, says cache errors should not cause application failure. Failing the user's call would turn an optimisation into a dependency.
saying these in an interview costs you the question
- Requires a concrete logging or caching package in a reusable library's composer.json.
- Makes the logger nullable and wraps every log call in a null check.
- Lets a cache write failure fail the library's main operation.
- Relies only on LoggerAwareTrait and assumes the logger is always set.
- Defaults to an in-memory cache and calls that production caching.