= expiresAt is expired) and for why the entry does not * carry its own clock. */ final class CachedEntry { /** * Cached value. Opaque to this VO; each cache adapter narrows * the runtime type at its own boundary. * * @var mixed */ private $value; /** * Instant at which the entry was written. */ private DateTimeImmutable $storedAt; /** * Instant at which the entry becomes expired (inclusive). */ private DateTimeImmutable $expiresAt; /** * Construct an entry. * * @param mixed $value Cached value. * @param DateTimeImmutable $storedAt Write instant. * @param DateTimeImmutable $expiresAt Expiry instant; must be * greater than or equal to * `$storedAt`. * * @throws InvalidArgumentException When `$expiresAt` precedes * `$storedAt`. */ public function __construct( $value, DateTimeImmutable $storedAt, DateTimeImmutable $expiresAt ) { if ( $expiresAt < $storedAt ) { throw new InvalidArgumentException( 'CachedEntry: expiresAt must be greater than or equal to storedAt.' ); } $this->value = $value; $this->storedAt = $storedAt; $this->expiresAt = $expiresAt; } /** * Cached value, returned verbatim. * * @return mixed */ public function value() { return $this->value; } /** * Instant at which the entry was written. */ public function storedAt(): DateTimeImmutable { return $this->storedAt; } /** * Instant at which the entry becomes expired (inclusive). */ public function expiresAt(): DateTimeImmutable { return $this->expiresAt; } /** * Report whether the entry is expired relative to the given * instant. * * The boundary is inclusive: `$now == $expiresAt` is treated * as expired so a TTL of N seconds yields exactly N seconds * of liveness, with the Nth-second boundary crossing into * the miss state. * * @param DateTimeImmutable $now Reference instant supplied by * the caller's clock. */ public function isExpired( DateTimeImmutable $now ): bool { return $now >= $this->expiresAt; } }