Skip to content

Commit ca53a2e

Browse files
committed
Add tags to cache
- Update ArrayCacheProvider to implement tags - Add CacheItem to type and validate parameters - Add tests
1 parent 4f1355f commit ca53a2e

File tree

10 files changed

+366
-49
lines changed

10 files changed

+366
-49
lines changed

phpstan.dist.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ includes:
22
- vendor/szepeviktor/phpstan-wordpress/extension.neon
33

44
parameters:
5+
editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%'
56
level: 5
67
paths:
78
- src

src/Cache/ArrayCacheProvider.php

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use DataKit\DataViews\Clock\Clock;
66
use DataKit\DataViews\Clock\SystemClock;
7+
use DateInterval;
8+
use Exception;
79

810
/**
911
* Cache provider backed by an array.
@@ -18,10 +20,19 @@ final class ArrayCacheProvider implements CacheProvider {
1820
*
1921
* @since $ver$
2022
*
21-
* @var array
23+
* @var CacheItem[]
2224
*/
2325
private array $items;
2426

27+
/**
28+
* Contains the reference to the tags with their tagged cache keys.
29+
*
30+
* @since $ver$
31+
*
32+
* @var array<string, string[]>
33+
*/
34+
private array $tags = [];
35+
2536
/**
2637
* The clock instance.
2738
*
@@ -36,8 +47,8 @@ final class ArrayCacheProvider implements CacheProvider {
3647
*
3748
* @since $ver$
3849
*
39-
* @param Clock|null $clock The clock instance.
40-
* @param array $items The pre-filled cache items.
50+
* @param Clock|null $clock The clock instance.
51+
* @param CacheItem[] $items The pre-filled cache items.
4152
*/
4253
public function __construct( ?Clock $clock = null, array $items = [] ) {
4354
$this->clock = $clock ?? new SystemClock();
@@ -49,12 +60,18 @@ public function __construct( ?Clock $clock = null, array $items = [] ) {
4960
*
5061
* @since $ver$
5162
*/
52-
public function set( string $key, $value, ?int $ttl = null ): void {
53-
$time = $ttl
54-
? ( $this->clock->now()->getTimestamp() + $ttl )
55-
: null;
63+
public function set( string $key, $value, ?int $ttl = null, array $tags = [] ): void {
64+
try {
65+
$time = (int) $ttl > 0
66+
? ( $this->clock->now()->add( new DateInterval( 'PT' . $ttl . 'S' ) ) )
67+
: null;
68+
} catch ( Exception $e ) {
69+
throw new \InvalidArgumentException( $e->getMessage(), $e->getCode(), $e );
70+
}
71+
72+
$this->items[ $key ] = new CacheItem( $key, $value, $time, $tags );
5673

57-
$this->items[ $key ] = compact( 'value', 'time' );
74+
$this->add_tags( $key, $tags );
5875
}
5976

6077
/**
@@ -63,15 +80,15 @@ public function set( string $key, $value, ?int $ttl = null ): void {
6380
* @since $ver$
6481
*/
6582
public function get( string $key, $fallback = null ) {
66-
$item = $this->items[ $key ] ?? [];
83+
$item = $this->items[ $key ] ?? null;
6784

68-
if ( $this->is_expired( $item ) ) {
85+
if ( ! $item || $item->is_expired( $this->clock ) ) {
6986
unset( $this->items[ $key ] );
7087

7188
return $fallback;
7289
}
7390

74-
return $item['value'] ?? $fallback;
91+
return $item->value();
7592
}
7693

7794
/**
@@ -82,7 +99,7 @@ public function get( string $key, $fallback = null ) {
8299
public function has( string $key ): bool {
83100
if (
84101
isset( $this->items[ $key ] )
85-
&& ! $this->is_expired( $this->items[ $key ] )
102+
&& ! $this->items[ $key ]->is_expired( $this->clock )
86103
) {
87104
return true;
88105
}
@@ -103,30 +120,52 @@ public function delete( string $key ): bool {
103120
return true;
104121
}
105122

123+
/**
124+
* @inheritDoc
125+
*
126+
* @since $ver$
127+
*/
128+
public function delete_by_tags( array $tags ): bool {
129+
foreach ( $tags as $tag ) {
130+
foreach ( $this->tags[ $tag ] ?? [] as $key ) {
131+
$this->delete( $key );
132+
}
133+
134+
unset( $this->tags[ $tag ] );
135+
}
136+
137+
return true;
138+
}
139+
106140
/**
107141
* @inheritDoc
108142
*
109143
* @since $ver$
110144
*/
111145
public function clear(): bool {
112146
$this->items = [];
147+
$this->tags = [];
113148

114149
return true;
115150
}
116151

117152
/**
118-
* Returns whether the provided cache item is expired.
153+
* Records a key for all provided tags.
119154
*
120155
* @since $ver$
121156
*
122-
* @param array $item The cache item.
123-
*
124-
* @return bool Whether the cache is expired.
157+
* @param string $key The key to tag.
158+
* @param array $tags The tags.
125159
*/
126-
private function is_expired( array $item ): bool {
127-
return (
128-
( $item['time'] ?? null )
129-
&& $this->clock->now()->getTimestamp() > $item['time']
130-
);
160+
private function add_tags( string $key, array $tags ): void {
161+
foreach ( $tags as $tag ) {
162+
if ( ! is_string( $tag ) ) {
163+
throw new \InvalidArgumentException( 'A tag must be a string.' );
164+
}
165+
166+
$this->tags[ $tag ] ??= [];
167+
168+
$this->tags[ $tag ] = array_unique( array_merge( $this->tags[ $tag ], [ $key ] ) );
169+
}
131170
}
132171
}

src/Cache/CacheItem.php

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<?php
2+
3+
namespace DataKit\DataViews\Cache;
4+
5+
use DataKit\DataViews\Clock\Clock;
6+
use DateTime;
7+
use DateTimeInterface;
8+
9+
/**
10+
* Represents a single cache item.
11+
*
12+
* @since $ver$
13+
*/
14+
final class CacheItem {
15+
/**
16+
* The key for the cache item.
17+
*
18+
* @since $ver$
19+
*
20+
* @var string
21+
*/
22+
private string $key;
23+
24+
/**
25+
* The cached value.
26+
*
27+
* @since $ver$
28+
*
29+
* @var mixed
30+
*/
31+
private $value;
32+
33+
/**
34+
* The unix timestamp when the cache expires.
35+
*
36+
* @since $ver$
37+
*
38+
* @var DateTimeInterface|null
39+
*/
40+
private ?DateTimeInterface $expires_at;
41+
42+
/**
43+
* The tags for this cache item.
44+
*
45+
* @since $ver$
46+
*
47+
* @var string[]
48+
*/
49+
private array $tags;
50+
51+
/**
52+
* Creates the Cache Item.
53+
*
54+
* @param string $key The cache key.
55+
* @param mixed $value The cached value.
56+
* @param DateTimeInterface|null $expires_at The timestamp the cache item expires.
57+
* @param string[] $tags The tags for the cache item.
58+
*/
59+
public function __construct( string $key, $value, ?DateTimeInterface $expires_at = null, array $tags = [] ) {
60+
$this->set_key( $key );
61+
$this->value = $value;
62+
$this->expires_at = $expires_at;
63+
64+
$this->add_tags( ...$tags );
65+
}
66+
67+
/**
68+
* Sets the key, and ensures it is valid.
69+
*
70+
* @since $ver$
71+
*
72+
* @param string $key The cache key.
73+
*/
74+
private function set_key( string $key ): void {
75+
if ( strlen( $key ) > 64 ) {
76+
throw new \InvalidArgumentException( 'Cache keys may not exceed a length of 64 characters.' );
77+
}
78+
79+
if ( preg_replace( '/[^a-z0-9_.]+/i', '', $key ) !== $key ) {
80+
throw new \InvalidArgumentException( 'Cache keys may only contain a-z, A-Z, 0-9, underscores (_) and periods (.).' );
81+
}
82+
83+
$this->key = $key;
84+
}
85+
86+
/**
87+
* Returns the key for the cache item.
88+
*
89+
* @since $ver$
90+
*
91+
* @return string The cache key.
92+
*/
93+
public function key(): string {
94+
return $this->key;
95+
}
96+
97+
/**
98+
* Returns the value on the CacheItem.
99+
*
100+
* @since $ver$
101+
*
102+
* @return mixed The cached value.
103+
*/
104+
public function value() {
105+
return $this->value;
106+
}
107+
108+
/**
109+
* Returns the tags for the cache item.
110+
*
111+
* @since $ver$
112+
*
113+
* @return string[] The tags.
114+
*/
115+
public function tags(): array {
116+
return $this->tags;
117+
}
118+
119+
/**
120+
* Returns whether the cache item is expired.
121+
*
122+
* @param Clock $clock The clock to test expiration.
123+
*
124+
* @return bool Whether the cache item is expired.
125+
*/
126+
public function is_expired( Clock $clock ): bool {
127+
if ( null === $this->expires_at ) {
128+
return false;
129+
}
130+
131+
return $clock->now() > $this->expires_at;
132+
}
133+
134+
/**
135+
* Ensures all tags are strings.
136+
*
137+
* @since $ver$
138+
*
139+
* @param string ...$tags The tags.
140+
*/
141+
private function add_tags( string ...$tags ): void {
142+
$this->tags = $tags;
143+
}
144+
}

src/Cache/CacheProvider.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ interface CacheProvider {
1717
* @param mixed $value The value to cache.
1818
* @param int|null $ttl The time to live in seconds.
1919
*/
20-
public function set( string $key, $value, ?int $ttl = null ): void;
20+
public function set( string $key, $value, ?int $ttl = null, array $tags = [] ): void;
2121

2222
/**
2323
* Retrieves the value from the cache, or the default if no value is stored.
@@ -56,6 +56,17 @@ public function has( string $key ): bool;
5656
*/
5757
public function delete( string $key ): bool;
5858

59+
/**
60+
* .Deletes any entries tagged with one of the provided tags.
61+
*
62+
* @since $ver$
63+
*
64+
* @param string[] $tags The tags to clear.
65+
*
66+
* @return bool Whether the deletion of the cache items was successful.
67+
*/
68+
public function delete_by_tags( array $tags ): bool;
69+
5970
/**
6071
* Clears the entire cache.
6172
*

src/Clock/Clock.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace DataKit\DataViews\Clock;
44

5-
use DateTimeInterface;
5+
use DateTimeImmutable;
66

77
/**
88
* Object that represents a clock.
@@ -15,5 +15,5 @@ interface Clock {
1515
*
1616
* @since $ver$
1717
*/
18-
public function now(): DateTimeInterface;
18+
public function now(): DateTimeImmutable;
1919
}

0 commit comments

Comments
 (0)