WireCache never calls $func if preloading was used
Short description of the issue
When preloading multiple cached items, the preloads array is filled with empty strings for those items that have not been cached yet (“ensure there is at least a placeholder for all requested caches”). Later when the preloaded elements are accessed individually, their presence is checked using isset(). The empty string is then returned immediately, because the empty string is indeed “set”.
Expected behavior
The generating function from the $func argument should be called when no cache exists, i.e. the preloaded value is ''. I imagine '' may also be a valid cache value in some situations, so perhaps the placeholder should be something else entirely.
Actual behavior
As far as I understand, $func is never executed because cache()->get() bails out as soon as it sees '' in the cache()->preloads array.
Optional: Suggestion for a possible fix
Easiest fix would be replacing isset() with !empty(), but that still leaves cases in which '' is the desired cached value (it would be preloaded successfully but regenerated every time regardless).
Steps to reproduce the issue
cache()->preload([ 'imaginary', 'names' ]); //these don’t exist.
//now cache()->preloads is [ 'imaginary' => '', 'names' => '' ] (it’s protected though)
$freshlyGeneratedValue = cache()->get('imaginary', WireCache::expireDaily, function() {
return 'so fresh'; //this never happens
});
var_dump($freshlyGeneratedValue); // string(0) ""
Setup/Environment
- ProcessWire version: 3.0.191
✨ B-B-B-BONUS ISSUE ✨
cache()->preload()’s doc-comment claims to accept a string for the first arg, but it only takes arrays.
Thanks!
I'd argue the problem is not with the isset, but PWs dirty type handling. Hence, since preloading internally uses WireCache::get, preloading should not populate WireCache::$preloads with keys for cache values that do not exist in the database.
Maybe with added $preloading / $options
function get($name, $expire = null, $func = null, $options = []) {
$values = $this->cacher()->find([
"names" => (array)$name,
# ...
/*
// fetching bla blub
$query = $sql->query("select name, data from cache where name in (:names)");
foreach($query as $row)
$values[$row["name"]] = $row["data"];
*/
]);
// preloading - return now
if($options["preloading"]??false)
return $values;
// not preloading - add default values
$values+= array_fill_keys((array)$name, '');
// or use value function
$values[$name] = $func();
return is_array($name) ? reset($values) : $values;
}
This would also allow for $options["nojsonparse"] if you put a JSON string there and want it back unscathed.
As an alternative, the cache could store the cached type (string/wirejsonstring/int/float/array/object/whatever), effectively getting rid of type mismatches, my suggested json option and the need for looksLikeJSON.
And to stray a little off topic: This would allow for multiple functions:
$nameMulti = [
"imaginary" => [
"expire" => null,
"func" => function() { return "so fresh"; },
],
];
function get($nameMulti, $expire = null, $func = null, $options = []){
# ...
}
function getMulti($nameMulti, $options = []){
// a new function might be preferable to changing the existing signature too drastically
}