22
33namespace  Lkrms \Store ;
44
5+ use  Lkrms \Exception \AssertionFailedException ;
56use  Lkrms \Store \Concept \SqliteStore ;
7+ use  Lkrms \Utility \Assert ;
68use  DateTimeInterface ;
79use  InvalidArgumentException ;
810use  LogicException ;
911
1012/** 
1113 * A SQLite-backed key-value store 
14+  * 
15+  * Expired items are not implicitly flushed. {@see CacheStore::flush()} must be 
16+  * called explicitly, e.g. on a schedule or once per run. 
1217 */ 
1318final  class  CacheStore extends  SqliteStore
1419{
@@ -114,30 +119,12 @@ public function set(string $key, $value, $expires = null)
114119     */ 
115120    public  function  has (string  $ keyint  $ maxAgenull ): bool 
116121    {
117-         $ where'item_key = :item_key ' ;
118-         $ bind':item_key ' , $ keySQLITE3_TEXT ];
119- 
120-         $ bindNowfalse ;
121-         if  ($ maxAgenull ) {
122-             $ where"(expires_at IS NULL OR expires_at > DATETIME(:now, 'unixepoch')) " ;
123-             $ bindNowtrue ;
124-         } elseif  ($ maxAge
125-             $ where"DATETIME(set_at, :max_age) > DATETIME(:now, 'unixepoch') " ;
126-             $ bind':max_age ' , "+ $ maxAge seconds " , \SQLITE3_TEXT ];
127-             $ bindNowtrue ;
128-         }
129-         if  ($ bindNow
130-             $ bind':now ' , $ this now (), \SQLITE3_INTEGER ];
131-         }
132- 
133-         $ whereimplode (' AND  ' , $ where
122+         $ where$ this getWhere ($ key$ maxAge$ bind
134123        $ sql<<<SQL 
135124            SELECT 
136125              COUNT(*) 
137126            FROM 
138-               _cache_item 
139-             WHERE 
140-                $ where
127+               _cache_item  $ where
141128            SQL ;
142129        $ db$ this db ();
143130        $ stmt$ dbprepare ($ sql
@@ -164,30 +151,12 @@ public function has(string $key, ?int $maxAge = null): bool
164151     */ 
165152    public  function  get (string  $ keyint  $ maxAgenull )
166153    {
167-         $ where'item_key = :item_key ' ;
168-         $ bind':item_key ' , $ keySQLITE3_TEXT ];
169- 
170-         $ bindNowfalse ;
171-         if  ($ maxAgenull ) {
172-             $ where"(expires_at IS NULL OR expires_at > DATETIME(:now, 'unixepoch')) " ;
173-             $ bindNowtrue ;
174-         } elseif  ($ maxAge
175-             $ where"DATETIME(set_at, :max_age) > DATETIME(:now, 'unixepoch') " ;
176-             $ bind':max_age ' , "+ $ maxAge seconds " , \SQLITE3_TEXT ];
177-             $ bindNowtrue ;
178-         }
179-         if  ($ bindNow
180-             $ bind':now ' , $ this now (), \SQLITE3_INTEGER ];
181-         }
182- 
183-         $ whereimplode (' AND  ' , $ where
154+         $ where$ this getWhere ($ key$ maxAge$ bind
184155        $ sql<<<SQL 
185156            SELECT 
186157              item_value 
187158            FROM 
188-               _cache_item 
189-             WHERE 
190-                $ where
159+               _cache_item  $ where
191160            SQL ;
192161        $ db$ this db ();
193162        $ stmt$ dbprepare ($ sql
@@ -204,6 +173,32 @@ public function get(string $key, ?int $maxAge = null)
204173        return  unserialize ($ row0 ]);
205174    }
206175
176+     /** 
177+      * Retrieve an instance of a class stored under a given key 
178+      * 
179+      * If `$maxAge` is `null` (the default), the item's expiration time is 
180+      * honoured, otherwise it is ignored and the item is considered fresh if: 
181+      * 
182+      * - its age in seconds is less than or equal to `$maxAge`, or 
183+      * - `$maxAge` is `0` 
184+      * 
185+      * @template T 
186+      * 
187+      * @param class-string<T> $class 
188+      * @return T|false `false` if the item has expired or doesn't exist. 
189+      * @throws AssertionFailedException if the item stored under `$key` is not 
190+      * an instance of `$class`. 
191+      */ 
192+     public  function  getInstanceOf (string  $ keystring  $ classint  $ maxAgenull )
193+     {
194+         $ item$ this get ($ key$ maxAge
195+         if  ($ itemfalse ) {
196+             return  false ;
197+         }
198+         Assert::instanceOf ($ item$ class
199+         return  $ item
200+     }
201+ 
207202    /** 
208203     * Delete an item stored under a given key 
209204     * 
@@ -213,8 +208,7 @@ public function delete(string $key)
213208    {
214209        $ db$ this db ();
215210        $ sql<<<SQL 
216-             DELETE FROM 
217-               _cache_item 
211+             DELETE FROM _cache_item 
218212            WHERE 
219213              item_key = :item_key; 
220214            SQL ;
@@ -236,8 +230,7 @@ public function deleteAll()
236230        $ db$ this db ();
237231        $ dbexec (
238232            <<<SQL 
239-             DELETE FROM 
240-               _cache_item; 
233+             DELETE FROM _cache_item; 
241234            SQL 
242235        );
243236
@@ -251,15 +244,16 @@ public function deleteAll()
251244     */ 
252245    public  function  flush ()
253246    {
254-         $ db$ this db ();
255-         $ dbexec (
256-             <<<SQL 
257-             DELETE FROM 
258-               _cache_item 
247+         $ sql<<<SQL 
248+             DELETE FROM _cache_item 
259249            WHERE 
260-               expires_at <= CURRENT_TIMESTAMP; 
261-             SQL 
262-         );
250+               expires_at <= DATETIME(:now, 'unixepoch'); 
251+             SQL ;
252+         $ db$ this db ();
253+         $ stmt$ dbprepare ($ sql
254+         $ stmtbindValue (':now ' , $ this now (), \SQLITE3_INTEGER );
255+         $ stmtexecute ();
256+         $ stmtclose ();
263257
264258        return  $ this 
265259    }
@@ -268,12 +262,14 @@ public function flush()
268262     * Retrieve an item stored under a given key, or get it from a callback and 
269263     * store it for subsequent retrieval 
270264     * 
271-      * @param callable(): mixed $callback 
265+      * @template T 
266+      * 
267+      * @param callable(): T $callback 
272268     * @param DateTimeInterface|int|null $expires `null` or `0` if the value 
273269     * should be cached indefinitely, otherwise a {@see DateTimeInterface} or 
274270     * Unix timestamp representing its expiration time, or an integer 
275271     * representing its lifetime in seconds. 
276-      * @return mixed  
272+      * @return T  
277273     */ 
278274    public  function  maybeGet (string  $ keycallable  $ callback$ expiresnull )
279275    {
@@ -292,6 +288,71 @@ public function maybeGet(string $key, callable $callback, $expires = null)
292288        return  $ value
293289    }
294290
291+     /** 
292+      * Get the number of unexpired items in the store 
293+      * 
294+      * If `$maxAge` is `null` (the default), each item's expiration time is 
295+      * honoured, otherwise it is ignored and items are considered fresh if: 
296+      * 
297+      * - their age in seconds is less than or equal to `$maxAge`, or 
298+      * - `$maxAge` is `0` 
299+      */ 
300+     public  function  getItemCount (?int  $ maxAgenull ): int 
301+     {
302+         $ where$ this getWhere (null , $ maxAge$ bind
303+         $ sql<<<SQL 
304+             SELECT 
305+               COUNT(*) 
306+             FROM 
307+               _cache_item  $ where
308+             SQL ;
309+         $ db$ this db ();
310+         $ stmt$ dbprepare ($ sql
311+         foreach  ($ bindas  $ param
312+             $ stmtbindValue (...$ param
313+         }
314+         $ result$ stmtexecute ();
315+         $ row$ resultfetchArray (\SQLITE3_NUM );
316+         $ stmtclose ();
317+ 
318+         return  $ row0 ];
319+     }
320+ 
321+     /** 
322+      * Get a list of keys under which unexpired items are stored 
323+      * 
324+      * If `$maxAge` is `null` (the default), each item's expiration time is 
325+      * honoured, otherwise it is ignored and items are considered fresh if: 
326+      * 
327+      * - their age in seconds is less than or equal to `$maxAge`, or 
328+      * - `$maxAge` is `0` 
329+      * 
330+      * @return string[] 
331+      */ 
332+     public  function  getAllKeys (?int  $ maxAgenull ): array 
333+     {
334+         $ where$ this getWhere (null , $ maxAge$ bind
335+         $ sql<<<SQL 
336+             SELECT 
337+               item_key 
338+             FROM 
339+               _cache_item  $ where
340+             SQL ;
341+         $ db$ this db ();
342+         $ stmt$ dbprepare ($ sql
343+         foreach  ($ bindas  $ param
344+             $ stmtbindValue (...$ param
345+         }
346+         $ result$ stmtexecute ();
347+         while  (($ row$ resultfetchArray (\SQLITE3_NUM )) !== false ) {
348+             $ keys$ row0 ];
349+         }
350+         $ resultfinalize ();
351+         $ stmtclose ();
352+ 
353+         return  $ keys
354+     }
355+ 
295356    /** 
296357     * Get a copy of the store where items do not expire over time 
297358     * 
@@ -327,4 +388,37 @@ private function now(): int
327388            ? time ()
328389            : $ this Now ;
329390    }
391+ 
392+     /** 
393+      * @param array<string,mixed> $bind 
394+      */ 
395+     private  function  getWhere (?string  $ keyint  $ maxAgearray  &$ bindstring 
396+     {
397+         $ where
398+         $ bind
399+ 
400+         if  ($ keynull ) {
401+             $ where'item_key = :item_key ' ;
402+             $ bind':item_key ' , $ keySQLITE3_TEXT ];
403+         }
404+ 
405+         $ bindNowfalse ;
406+         if  ($ maxAgenull ) {
407+             $ where"(expires_at IS NULL OR expires_at > DATETIME(:now, 'unixepoch')) " ;
408+             $ bindNowtrue ;
409+         } elseif  ($ maxAge
410+             $ where"DATETIME(set_at, :max_age) > DATETIME(:now, 'unixepoch') " ;
411+             $ bind':max_age ' , "+ $ maxAge seconds " , \SQLITE3_TEXT ];
412+             $ bindNowtrue ;
413+         }
414+         if  ($ bindNow
415+             $ bind':now ' , $ this now (), \SQLITE3_INTEGER ];
416+         }
417+ 
418+         $ whereimplode (' AND  ' , $ where
419+         if  ($ where'' ) {
420+             return  '' ;
421+         }
422+         return  "WHERE  $ where ;
423+     }
330424}
0 commit comments