Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.62% covered (success)
90.62%
29 / 32
87.50% covered (warning)
87.50%
14 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
CachedQBMapper
90.62% covered (success)
90.62%
29 / 32
87.50% covered (warning)
87.50%
14 / 16
23.44
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 insert
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 updateWithoutCache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 invalidateEntityCache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 cachePrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 cacheKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 cacheGet
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 cacheSet
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 cacheRemove
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 cacheClear
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getEntityCacheKeys
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 cacheEntity
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 invalidateCachedEntity
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 evictEntity
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4/**
5 * SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
6 * SPDX-License-Identifier: AGPL-3.0-or-later
7 */
8
9namespace OCA\Libresign\Db;
10
11use OCP\AppFramework\Db\Entity;
12use OCP\AppFramework\Db\QBMapper;
13use OCP\ICache;
14use OCP\ICacheFactory;
15use OCP\IDBConnection;
16
17/**
18 * @template T of Entity
19 * @template-extends QBMapper<T>
20 */
21abstract class CachedQBMapper extends QBMapper {
22    protected const DEFAULT_CACHE_TTL = 30;
23
24    private ?ICache $cache = null;
25
26    public function __construct(
27        IDBConnection $db,
28        ICacheFactory $cacheFactory,
29        string $tableName,
30        private int $cacheTtl = self::DEFAULT_CACHE_TTL,
31    ) {
32        parent::__construct($db, $tableName);
33        if ($cacheFactory->isAvailable()) {
34            $this->cache = $cacheFactory->createDistributed($tableName);
35        }
36    }
37
38    #[\Override]
39    public function insert(Entity $entity): Entity {
40        $inserted = parent::insert($entity);
41        $this->cacheEntity($inserted);
42        return $inserted;
43    }
44
45    #[\Override]
46    public function update(Entity $entity): Entity {
47        $updated = parent::update($entity);
48        $this->cacheEntity($updated);
49        return $updated;
50    }
51
52    /**
53     * Keep uncommitted entity state out of the distributed cache.
54     * @param T $entity
55     * @return T
56     */
57    public function updateWithoutCache(Entity $entity): Entity {
58        return parent::update($entity);
59    }
60
61    /** @param T $entity Invalidate cached state after a transaction boundary. */
62    public function invalidateEntityCache(Entity $entity): void {
63        $this->evictEntity($entity);
64    }
65
66    #[\Override]
67    public function delete(Entity $entity): Entity {
68        $deleted = parent::delete($entity);
69        $this->evictEntity($deleted);
70        return $deleted;
71    }
72
73    private function cachePrefix(): string {
74        return $this->getTableName();
75    }
76
77    private function cacheKey(string $key): string {
78        return $this->cachePrefix() . ':' . $key;
79    }
80
81    protected function cacheGet(string $key): mixed {
82        return $this->cache?->get($this->cacheKey($key));
83    }
84
85    protected function cacheSet(string $key, mixed $value): void {
86        if ($this->cache) {
87            $this->cache->set($this->cacheKey($key), $value, $this->cacheTtl);
88        }
89    }
90
91    protected function cacheRemove(string $key): void {
92        if ($this->cache) {
93            $this->cache->remove($this->cacheKey($key));
94        }
95    }
96
97    private function cacheClear(): void {
98        if ($this->cache) {
99            $this->cache->clear($this->cachePrefix() . ':');
100        }
101    }
102
103    /**
104     * @return list<string>
105     */
106    protected function getEntityCacheKeys(Entity $entity): array {
107        $id = $entity->getId();
108        if ($id === null) {
109            return [];
110        }
111        return ['id:' . $id];
112    }
113
114    protected function cacheEntity(Entity $entity): void {
115        foreach ($this->getEntityCacheKeys($entity) as $key) {
116            $this->cacheSet($key, $entity);
117        }
118    }
119
120    /**
121     * Drop speculative cache writes after a transaction commits or rolls back.
122     * A subsequent lookup reloads the authoritative database state.
123     */
124    public function invalidateCachedEntity(Entity $entity): void {
125        $this->evictEntity($entity);
126    }
127
128    private function evictEntity(Entity $entity): void {
129        foreach ($this->getEntityCacheKeys($entity) as $key) {
130            $this->cacheRemove($key);
131        }
132    }
133}