Skip to content

Commit 20d3671

Browse files
authored
Fix #150: Add Ttl value object for working with time-to-live duration
1 parent 2d6835f commit 20d3671

11 files changed

Lines changed: 414 additions & 112 deletions

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## 3.1.1 under development
44

5-
- no changes in this release.
5+
- New #150: Add `Ttl` value object for working with time-to-live duration (@Pekhov14)
66

77
## 3.1.0 June 01, 2025
88

README.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,103 @@ Set a default TTL:
6666
$cache = new \Yiisoft\Cache\Cache($arrayCache, 60 * 60); // 1 hour
6767
```
6868

69+
## `Ttl` object
70+
71+
`Ttl` is a simple immutable value object that represents cache time-to-live (TTL) in seconds.
72+
It eliminates magic numbers (like 60 * 60 or 3600), improves readability, and provides convenient factory methods.
73+
74+
Below are examples on how to use it.
75+
76+
If you're using PSR-16 cache adapter directly:
77+
78+
- TTL must be an integer number of seconds or `null` for infinite lifetime.
79+
- Always use `->toSeconds()` when using `Ttl` object.
80+
81+
```php
82+
use Yiisoft\Cache\Ttl;
83+
use Yiisoft\Cache\ArrayCache;
84+
85+
$cache = new ArrayCache();
86+
87+
$cache->set('key1', 'value1', Ttl::seconds(30)->toSeconds()); // 30 seconds
88+
$cache->set('key2', 'value2', Ttl::minutes(15)->toSeconds()); // 15 minutes
89+
$cache->set('key3', 'value3', Ttl::hours(2)->toSeconds()); // 2 hours
90+
$cache->set('key4', 'value4', Ttl::days(1)->toSeconds()); // 1 day
91+
92+
// Complex durations
93+
$ttl = Ttl::create(sec: 30, min: 10, hour: 1); // 1 hour 10 minutes 30 seconds
94+
$cache->set('key', 'value', $ttl->toSeconds());
95+
96+
// Infinity / no expiration
97+
$cache->set('key6', 'value6', Ttl::forever()); // shorthand for null
98+
$cache->set('key7', 'value7', Ttl::from(null));
99+
```
100+
101+
### Creating and Normalizing TTL
102+
103+
The `Ttl::from()` method normalizes various TTL representations (`Ttl`, `DateInterval`, `int`, `string`, or `null`) into a `Ttl` object.
104+
105+
```php
106+
$ttl = Ttl::from(new DateInterval('PT45M')); // 45 minutes
107+
$ttl = Ttl::from(10); // 10 seconds
108+
$ttl = Ttl::from('12'); // 12 seconds
109+
$ttl = Ttl::from(null); // Infinity / no expiration
110+
$ttl = Ttl::from(Ttl::seconds(500));
111+
112+
$ttl = Ttl::create(sec: 30, min: 15);
113+
114+
// From DateInterval
115+
$ttl = Ttl::fromInterval(new DateInterval('PT45M'));
116+
$cache->set('key', 'value', $ttl->toSeconds());
117+
118+
// Ttl::forever() is just a shorthand for `null` TTL (no expiration)
119+
$cache->set('key', 'value', Ttl::forever()->toSeconds());
120+
// or
121+
$cache->set('key', 'value', Ttl::from(null)->toSeconds());
122+
```
123+
124+
### When using `Ttl` with Yii cache wrapper:
125+
126+
- You can pass a `Ttl` object in the constructor as the default value.
127+
- You can pass it to methods like `getOrSet()` which expect integer number of seconds or `null`.
128+
129+
```php
130+
use Yiisoft\Cache\Cache;
131+
use Yiisoft\Cache\ArrayCache;
132+
use Yiisoft\Cache\Ttl;
133+
134+
$cache = new Cache(new ArrayCache(), Ttl::minutes(5)); // default TTL
135+
$cache->getOrSet('key', 'value'); // // Uses default TTL
136+
137+
// Custom TTL
138+
$cache->getOrSet('key2', fn() => 'value2', Ttl::seconds(30)->toSeconds());
139+
$cache->getOrSet('key3', fn() => 'value3', Ttl::forever()->toSeconds()); // No expiration
140+
141+
Use `isForever()` to check if a TTL represents "forever" (i.e., no expiration). It returns true when the TTL value is null.
142+
143+
```php
144+
if (Ttl::from(null)->isForever()) {
145+
// No expiration
146+
}
147+
````
148+
149+
### Accessing TTL Value
150+
151+
Use `toSeconds()` to get the TTL in seconds (`int`) or `null` for "forever". The public `$value` property can be accessed directly (e.g., `Ttl::seconds(30)->value`), but `toSeconds()` is preferred for clarity.
152+
153+
```php
154+
$ttl = Ttl::seconds(60);
155+
$seconds = $ttl->toSeconds(); // Returns 60
156+
$seconds = $ttl->value; // Also 60
157+
```
158+
159+
### Invalid TTL values
160+
161+
```php
162+
$ttl = Ttl::from('abc'); // Converts to 0 (expired)
163+
$ttl = Ttl::from(1.5); // TypeError: invalid TTL type
164+
```
165+
69166
## General usage
70167

71168
Typical PSR-16 cache usage is the following:

src/ArrayCache.php

Lines changed: 8 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace Yiisoft\Cache;
66

77
use DateInterval;
8-
use DateTime;
98
use Traversable;
109
use Yiisoft\Cache\Exception\InvalidArgumentException;
1110

@@ -48,7 +47,7 @@ public function get(string $key, mixed $default = null): mixed
4847
public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool
4948
{
5049
$this->validateKey($key);
51-
$expiration = $this->ttlToExpiration($ttl);
50+
$expiration = $this->ttlToExpiration(Ttl::from($ttl));
5251

5352
if ($expiration < 0) {
5453
return $this->delete($key);
@@ -131,42 +130,21 @@ private function isExpired(string $key): bool
131130

132131
/**
133132
* Converts TTL to expiration.
133+
*
134+
* @param Ttl $ttl
134135
*/
135-
private function ttlToExpiration(DateInterval|int|null $ttl): int
136+
private function ttlToExpiration(Ttl $ttl): int
136137
{
137-
$ttl = $this->normalizeTtl($ttl);
138-
139-
if ($ttl === null) {
138+
if ($ttl->isForever()) {
140139
return self::EXPIRATION_INFINITY;
141140
}
142141

143-
if ($ttl <= 0) {
142+
$seconds = $ttl->toSeconds();
143+
if ($seconds <= 0) {
144144
return self::EXPIRATION_EXPIRED;
145145
}
146146

147-
return $ttl + time();
148-
}
149-
150-
/**
151-
* Normalizes cache TTL handling strings and {@see DateInterval} objects.
152-
*
153-
* @param DateInterval|int|string|null $ttl Raw TTL.
154-
*
155-
* @return int|null TTL value as UNIX timestamp or null meaning infinity
156-
*/
157-
private function normalizeTtl(DateInterval|int|string|null $ttl): ?int
158-
{
159-
if ($ttl instanceof DateInterval) {
160-
return (new DateTime('@0'))
161-
->add($ttl)
162-
->getTimestamp();
163-
}
164-
165-
if ($ttl === null) {
166-
return null;
167-
}
168-
169-
return (int) $ttl;
147+
return $seconds + time();
170148
}
171149

172150
/**

src/Cache.php

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace Yiisoft\Cache;
66

77
use DateInterval;
8-
use DateTime;
98
use Yiisoft\Cache\Dependency\Dependency;
109
use Yiisoft\Cache\Exception\InvalidArgumentException;
1110
use Yiisoft\Cache\Exception\RemoveCacheException;
@@ -36,22 +35,22 @@ final class Cache implements CacheInterface
3635
private readonly CacheItems $items;
3736

3837
/**
39-
* @var int|null The default TTL for a cache entry. null meaning infinity, negative or zero results in the
38+
* @var Ttl The default TTL for a cache entry. null meaning infinity, negative or zero results in the
4039
* cache key deletion. This value is used by {@see getOrSet()}, if the duration is not explicitly given.
4140
*/
42-
private readonly ?int $defaultTtl;
41+
private readonly Ttl $defaultTtl;
4342

4443
/**
4544
* @param \Psr\SimpleCache\CacheInterface $handler The actual cache handler.
46-
* @param DateInterval|int|null $defaultTtl The default TTL for a cache entry.
45+
* @param DateInterval|int|Ttl|null $defaultTtl The default TTL for a cache entry.
4746
* null meaning infinity, negative or zero results in the cache key deletion.
4847
* This value is used by {@see getOrSet()}, if the duration is not explicitly given.
4948
*/
50-
public function __construct(\Psr\SimpleCache\CacheInterface $handler, DateInterval|int|null $defaultTtl = null)
49+
public function __construct(\Psr\SimpleCache\CacheInterface $handler, Ttl|DateInterval|int|null $defaultTtl = null)
5150
{
5251
$this->psr = new DependencyAwareCache($this, $handler);
5352
$this->items = new CacheItems();
54-
$this->defaultTtl = $this->normalizeTtl($defaultTtl);
53+
$this->defaultTtl = Ttl::from($defaultTtl);
5554
}
5655

5756
public function psr(): \Psr\SimpleCache\CacheInterface
@@ -66,10 +65,12 @@ public function getOrSet(
6665
Dependency|null $dependency = null,
6766
float $beta = 1.0
6867
) {
68+
$ttlObj = Ttl::from($ttl ?? $this->defaultTtl);
69+
6970
$key = CacheKeyNormalizer::normalize($key);
7071
$value = $this->getValue($key, $beta);
7172

72-
return $value ?? $this->setAndGet($key, $callable, $ttl, $dependency);
73+
return $value ?? $this->setAndGet($key, $callable, $ttlObj, $dependency);
7374
}
7475

7576
public function remove(mixed $key): void
@@ -119,7 +120,7 @@ private function getValue(string $key, float $beta): mixed
119120
* @param callable $callable The callable or closure that will be used to generate a value to be cached.
120121
* @psalm-param callable(\Psr\SimpleCache\CacheInterface): mixed $callable
121122
*
122-
* @param DateInterval|int|null $ttl The TTL of this value. If not set, default value is used.
123+
* @param DateInterval|int|Ttl|null $ttl The TTL of this value. If not set, default value is used.
123124
* @param Dependency|null $dependency The dependency of the cache value.
124125
*
125126
* @throws InvalidArgumentException Must be thrown if the `$key` or `$ttl` is not a legal value.
@@ -130,11 +131,10 @@ private function getValue(string $key, float $beta): mixed
130131
private function setAndGet(
131132
string $key,
132133
callable $callable,
133-
DateInterval|int|null $ttl,
134+
Ttl|DateInterval|int|null $ttl,
134135
?Dependency $dependency
135136
): mixed {
136-
$ttl = $this->normalizeTtl($ttl);
137-
$ttl ??= $this->defaultTtl;
137+
$ttl = Ttl::from($ttl ?? $this->defaultTtl);
138138
$value = $callable($this->psr);
139139

140140
if ($dependency !== null) {
@@ -143,35 +143,11 @@ private function setAndGet(
143143

144144
$item = new CacheItem($key, $ttl, $dependency);
145145

146-
if (!$this->psr->set($key, [$value, $item], $ttl)) {
146+
if (!$this->psr->set($key, [$value, $item], $ttl->toSeconds())) {
147147
throw new SetCacheException($key, $value, $item);
148148
}
149149

150150
$this->items->set($item);
151151
return $value;
152152
}
153-
154-
/**
155-
* Normalizes cache TTL handling `null` value and {@see DateInterval} objects.
156-
*
157-
* @param DateInterval|int|null $ttl raw TTL.
158-
*
159-
* @throws InvalidArgumentException For invalid TTL.
160-
*
161-
* @return int|null TTL value as UNIX timestamp or null meaning infinity.
162-
*/
163-
private function normalizeTtl(DateInterval|int|null $ttl): ?int
164-
{
165-
if ($ttl === null) {
166-
return null;
167-
}
168-
169-
if ($ttl instanceof DateInterval) {
170-
return (new DateTime('@0'))
171-
->add($ttl)
172-
->getTimestamp();
173-
}
174-
175-
return $ttl;
176-
}
177153
}

src/DependencyAwareCache.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ public function get(string $key, mixed $default = null): mixed
3434
return $this->checkAndGetValue($key, $value, $default);
3535
}
3636

37-
public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool
37+
public function set(string $key, mixed $value, Ttl|null|int|DateInterval $ttl = null): bool
3838
{
39-
return $this->handler->set($key, $value, $ttl);
39+
return $this->handler->set($key, $value, Ttl::from($ttl)->toSeconds());
4040
}
4141

4242
public function delete($key): bool
@@ -60,9 +60,9 @@ public function getMultiple(iterable $keys, mixed $default = null): iterable
6060
return $values;
6161
}
6262

63-
public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool
63+
public function setMultiple(iterable $values, Ttl|null|int|DateInterval $ttl = null): bool
6464
{
65-
return $this->handler->setMultiple($values, $ttl);
65+
return $this->handler->setMultiple($values, Ttl::from($ttl)->toSeconds());
6666
}
6767

6868
public function deleteMultiple($keys): bool

src/Metadata/CacheItem.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Yiisoft\Cache\CacheInterface;
88
use Yiisoft\Cache\Dependency\Dependency;
99
use Yiisoft\Cache\Exception\InvalidArgumentException;
10+
use Yiisoft\Cache\Ttl;
1011

1112
use function ceil;
1213
use function log;
@@ -29,14 +30,16 @@ final class CacheItem
2930

3031
/**
3132
* @param string $key The key that identifies the cache item.
32-
* @param int|null $ttl The TTL value of this item. null means infinity.
33+
* @param int|Ttl|null $ttl The TTL value of this item. null means infinity.
3334
* @param Dependency|null $dependency The cache invalidation dependency or null for none.
3435
*/
3536
public function __construct(
3637
private readonly string $key,
37-
?int $ttl,
38+
Ttl|int|null $ttl,
3839
private ?Dependency $dependency
3940
) {
41+
$ttl = Ttl::from($ttl)->toSeconds();
42+
4043
$this->expiry = ($ttl > 0) ? time() + $ttl : $ttl;
4144
$this->updated = microtime(true);
4245
}

src/PrefixedCache.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public function get(string $key, mixed $default = null): mixed
3434
return $this->cache->get($this->prefix . $key, $default);
3535
}
3636

37-
public function set(string $key, mixed $value, null|int|DateInterval $ttl = null): bool
37+
public function set(string $key, mixed $value, Ttl|null|int|DateInterval $ttl = null): bool
3838
{
3939
return $this->cache->set($this->prefix . $key, $value, $ttl);
4040
}
@@ -60,7 +60,7 @@ public function getMultiple(iterable $keys, mixed $default = null): iterable
6060
return $this->cache->getMultiple($prefixedKeys, $default);
6161
}
6262

63-
public function setMultiple(iterable $values, null|int|DateInterval $ttl = null): bool
63+
public function setMultiple(iterable $values, Ttl|null|int|DateInterval $ttl = null): bool
6464
{
6565
$prefixedValues = [];
6666

0 commit comments

Comments
 (0)