Fully Implementing PSR-16 Simple Cache is Less Than Simple
- Comments:
- 0
Foreword: Please note this falls firmly into "nitpick" territory rather than shining light onto any sort of major issue. I just thought it was an interesting gotcha and wanted to share.
TL;DR: To fully implement the PSR-16 specification as defined, an implementation MUST be contravariant of the defined interface in its accepted parameters when implementing v2 or later.
I work on an application with a proprietary caching layer. Recently I wanted to build a PSR-16 "Simple Cache" adapter for it, and started going through the spec and provided interface in detail. I wanted my implementation to be as correct as possible.
While working my way through getMultiple
/setMultiple
/deleteMultiple
I noted something that seemed like an oversight.
/**
* Obtains multiple cache items by their unique keys.
*
* @param iterable<string> $keys A list of keys that can be obtained in a single operation.
* @param mixed $default Default value to return for keys that do not exist.
*
* @return iterable<string, mixed> A list of key => value pairs. Cache keys that do not exist or are stale will have $default as value.
*
* @throws \Psr\SimpleCache\InvalidArgumentException
* MUST be thrown if $keys is neither an array nor a Traversable,
* or if any of the $keys are not a legal value.
*/
public function getMultiple(iterable $keys, mixed $default = null): iterable;
Take a moment to read this carefully
@throws \Psr\SimpleCache\InvalidArgumentException
MUST be thrown if $keys is neither an array nor a Traversable
or if any of the $keys are not a legal value.
Then, inspect the definition
public function getMultiple(iterable $keys, mixed $default = null): iterable;
The takeaway here should be:
iterable $keys
To implement getMultiple
, we MUST throw a \Psr\SimpleCache\InvalidArgumentException
when $keys
is invalid, yet $keys
is defined as iterable
which makes it impossible to handle if an implementation matches this signature.
Passing anything non-iterable will throw a TypeError
before the code even runs. This would make it impossible to throw an InvalidArgumentException.
Parameter types were added to the interface with the release of v2
. My suspicion was that the @throws
had simply not been updated, because if the parameter type is explicitly defined as iterable, the function cannot throw a custom exception.
I originally suspected this would make it impossible to fulfill the spec. However, I had not considered contravariance. The implementing methods accepted parameter types may be wider than the interface it is fulfilling. In this instance, $keys
does not need to be defined as iterable
in the implementation.
This means a technically complete implementation must omit iterable
and look something like
public function getMultiple($keys, mixed $default = null): iterable {
if(!is_array($keys) && !$var instanceof Traversable) {
// Must implement \Psr\SimpleCache\InvalidArgumentException;
throw new ExampleException;
}
}
Note \Psr\SimpleCache\InvalidArgumentException
is an interface. An implementation must define a concrete exception class that implements it before it can be thrown
Only manually checking $keys
type like this would allow an implementation to completely fulfill the specification.
Doing a little investigation, it seems like most major implementations still target v1 || v2 || v3
and as v1
did not have typed parameters, neither do the implementations iterable parameters.
There are a couple of implementations that get this wrong.
I'm sure there are others, but I found these three in my quick search. It is a very easy mistake to make when implementing v1
.
This is not Java. There are no checked exceptions forcing anyone's hand. In practice, this is a minor quirk of the spec. If a TypeError
gets thrown instead of an \Psr\SimpleCache\InvalidArgumentException
, almost certainly nothing meaningful breaks. I doubt anything depends on this behaviour. It’s just an inconsistency between what the spec says and what implementations are doing. It is nothing to lose sleep over, but it might be worth fixing for consistency's sake.
In summary, to fully implement the getMultiple
/setMultiple
/deleteMultiple
methods of the PSR-16 spec, one cannot simply copy their definitions directly from the CacheInterface. The implementation must check the validity of their iterable parameters and not have them defined as an iterable
.