getgrav/grav · error · InvalidArgumentException
Expiration date must be an integer, a DateInterval or null,
Error message
Expiration date must be an integer, a DateInterval or null, "%s" given
What it means
CacheTrait's TTL normalizer (getTtl) accepts exactly three shapes: null (default lifetime), a positive/negative integer of seconds, or a DateInterval. Any other type — string, float, bool, DateTime — throws InvalidArgumentException('Expiration date must be an integer, a DateInterval or null, ...') with the debug type.
Source
Thrown at system/src/Grav/Framework/Cache/CacheTrait.php:366
*/
protected function convertTtl(DateInterval|int|null $ttl): ?int
{
if ($ttl === null) {
return $this->getDefaultLifetime();
}
if (is_int($ttl)) {
return $ttl;
}
if ($ttl instanceof DateInterval) {
$date = DateTime::createFromFormat('U', '0');
$ttl = $date ? (int)$date->add($ttl)->format('U') : 0;
return $ttl;
}
throw new InvalidArgumentException(
sprintf(
'Expiration date must be an integer, a DateInterval or null, "%s" given',
get_debug_type($ttl)
)
);
}
}
View on GitHub (pinned to 6040efed04)
Solutions
- Cast at the boundary: $cache->set($key, $value, (int) $ttl) when TTL comes from config or request data.
- Use DateInterval for human units: new \DateInterval('PT2H') instead of the string 'PT2H'.
- For absolute expiries, convert first: $ttl = $expiresAt->getTimestamp() - time(); then pass the integer.
Example fix
// before
$cache->set($key, $data, $this->config('cache.lifetime')); // '3600' string -> throws
// after
$cache->set($key, $data, (int) $this->config('cache.lifetime')); Defensive patterns
Strategy: type-guard
Type guard
/**
* @psalm-assert int|\DateInterval|null $ttl
*/
function assertTtl(mixed $ttl): void
{
if (!is_int($ttl) && !$ttl instanceof \DateInterval && $ttl !== null) {
throw new \InvalidArgumentException('TTL must be int, DateInterval, or null, got ' . get_debug_type($ttl));
}
}
assertTtl($ttl);
$cache->set($key, $value, $ttl); Prevention
- Cast config-sourced lifetimes: (int) $config->get('system.cache.lifetime').
- Convert absolute expiries: $expiresAt->getTimestamp() - time().
- Construct DateInterval objects instead of passing 'PT2H' strings.
When it happens
Trigger: Passing a TTL read from YAML/JSON config as a string ('3600'); passing 'PT2H' style strings instead of a DateInterval object; passing 60.0 (float) from a division; passing a DateTime instance expecting an absolute expiry (PSR-16 wants relative intervals or seconds); forwarding null-coalesced mixed values from user input.
Common situations: user/config/system.yaml cache lifetime entered as a quoted string; settings forms returning string numerics; libraries computing TTL as float seconds; refactors where a config value loses its int cast.
Related errors
- Cache key must be string, "%s" given
- The class '%s' does not implement the '%s' interface
- Cache keys must be array or Traversable, "%s" given
- Cache values must be array or Traversable, "%s" given
- Cache key length must be greater than zero
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/43d36aecefe3503f.
Report an issue: GitHub.