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

  1. Cast at the boundary: $cache->set($key, $value, (int) $ttl) when TTL comes from config or request data.
  2. Use DateInterval for human units: new \DateInterval('PT2H') instead of the string 'PT2H'.
  3. 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

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


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/43d36aecefe3503f. Report an issue: GitHub.