{"record":{"id":"43d36aecefe3503f","repo":"getgrav/grav","slug":"expiration-date-must-be-an-integer-a-dateinterval","errorCode":null,"errorMessage":"Expiration date must be an integer, a DateInterval or null, \"%s\" given","messagePattern":"Expiration date must be an integer, a DateInterval or null, \"(.+?)\" given","errorType":"exception","errorClass":"InvalidArgumentException","httpStatus":null,"severity":"error","filePath":"system/src/Grav/Framework/Cache/CacheTrait.php","lineNumber":366,"sourceCode":"     */\n    protected function convertTtl(DateInterval|int|null $ttl): ?int\n    {\n        if ($ttl === null) {\n            return $this->getDefaultLifetime();\n        }\n\n        if (is_int($ttl)) {\n            return $ttl;\n        }\n\n        if ($ttl instanceof DateInterval) {\n            $date = DateTime::createFromFormat('U', '0');\n            $ttl = $date ? (int)$date->add($ttl)->format('U') : 0;\n\n            return $ttl;\n        }\n\n        throw new InvalidArgumentException(\n            sprintf(\n                'Expiration date must be an integer, a DateInterval or null, \"%s\" given',\n                get_debug_type($ttl)\n            )\n        );\n    }\n}\n","sourceCodeStart":348,"sourceCodeEnd":374,"githubUrl":"https://github.com/getgrav/grav/blob/6040efed04efa69b8209448ed81308e7c24147c2/system/src/Grav/Framework/Cache/CacheTrait.php#L348-L374","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n$cache->set($key, $data, $this->config('cache.lifetime')); // '3600' string -> throws\n\n// after\n$cache->set($key, $data, (int) $this->config('cache.lifetime'));","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"/**\n * @psalm-assert int|\\DateInterval|null $ttl\n */\nfunction assertTtl(mixed $ttl): void\n{\n    if (!is_int($ttl) && !$ttl instanceof \\DateInterval && $ttl !== null) {\n        throw new \\InvalidArgumentException('TTL must be int, DateInterval, or null, got ' . get_debug_type($ttl));\n    }\n}\n\nassertTtl($ttl);\n$cache->set($key, $value, $ttl);","tryCatchPattern":null,"preventionTips":["Cast config-sourced lifetimes: (int) $config->get('system.cache.lifetime').","Convert absolute expiries: $expiresAt->getTimestamp() - time().","Construct DateInterval objects instead of passing 'PT2H' strings."],"tags":["grav","cache","psr-16","ttl","type-validation"],"backgroundTag":"invalid-cache-ttl","analyzedSha":"6040efed04efa69b8209448ed81308e7c24147c2","analyzedAt":"2026-08-17T05:07:31.593Z","schemaVersion":2},"datasetVersion":"2026-08-17T09:17:11.063Z"}