Passed
Push — master ( 5a655a...f1e8b0 )
by Yannick
08:54
created

QueryCacheHelper::invalidate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 4
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Helpers;
8
9
use Doctrine\ORM\QueryBuilder;
10
use Symfony\Contracts\Cache\CacheInterface;
11
use Symfony\Contracts\Cache\ItemInterface;
12
13
/**
14
 * QueryCacheHelper
15
 *
16
 * Helper for caching Doctrine query results.
17
 *
18
 * Features:
19
 * - Automatic generation of cache keys.
20
 * - Optionally returns cache key along with result (for debugging or manual invalidation).
21
 * - Supports tagging (if using a TagAware cache adapter).
22
 *
23
 * USAGE EXAMPLES:
24
 *
25
 * // Run a query with caching:
26
 * $result = $this->queryCacheHelper->run(
27
 *     $qb,
28
 *     'findActiveUsers'
29
 * );
30
 *
31
 * // Run a query with parameters:
32
 * $result = $this->queryCacheHelper->run(
33
 *     $qb,
34
 *     'findByRole',
35
 *     ['role' => $role, 'keyword' => $keyword]
36
 * );
37
 *
38
 * // Run a query and get cache key for debugging:
39
 * $result = $this->queryCacheHelper->run(
40
 *     $qb,
41
 *     'findByRole',
42
 *     ['role' => $role, 'keyword' => $keyword],
43
 *     300,
44
 *     true // return key
45
 * );
46
 *
47
 * // Run a query with tags:
48
 * $result = $this->queryCacheHelper->runWithTags(
49
 *     $qb,
50
 *     'findByRole',
51
 *     ['role' => $role, 'keyword' => $keyword],
52
 *     ['users'],
53
 *     300
54
 * );
55
 *
56
 * // Invalidate a specific cached query:
57
 * $this->queryCacheHelper->invalidate('findByRole', [...]);
58
 *
59
 * // Invalidate everything with a given tag:
60
 * $this->queryCacheHelper->invalidateByTag('users');
61
 */
62
class QueryCacheHelper
63
{
64
    private CacheInterface $cache;
65
    private int $defaultTtl;
66
67
    /**
68
     * Constructor.
69
     *
70
     * @param CacheInterface $cache      The cache adapter to store results.
71
     * @param int            $defaultTtl Default TTL (in seconds) for cached queries.
72
     */
73
    public function __construct(CacheInterface $cache, int $defaultTtl = 600)
74
    {
75
        $this->cache = $cache;
76
        $this->defaultTtl = $defaultTtl;
77
    }
78
79
    /**
80
     * Runs a Doctrine QueryBuilder and caches its results.
81
     *
82
     * @param QueryBuilder     $qb             The Doctrine QueryBuilder to execute.
83
     * @param string|null      $operationName  Operation name for generating cache key.
84
     * @param array            $parameters     Parameters affecting the query (included in cache key).
85
     * @param int|null         $ttl            Time-to-live for cache in seconds.
86
     * @param bool             $returnKey      Whether to return the cache key alongside data.
87
     *
88
     * @return array|mixed Either:
89
     *                     - array with keys ['data' => ..., 'cache_key' => ...] if $returnKey is true
90
     *                     - raw query result otherwise
91
     */
92
    public function run(
93
        QueryBuilder $qb,
94
        ?string $operationName = null,
95
        array $parameters = [],
96
        ?int $ttl = 300,
97
        bool $returnKey = true
98
    ) {
99
        $operationName ??= 'anonymous_query';
100
        $cacheKey = $this->buildCacheKey($operationName, $parameters);
101
102
        $result = $this->cache->get($cacheKey, function (ItemInterface $item) use ($qb, $ttl) {
103
            $item->expiresAfter($ttl ?? $this->defaultTtl);
104
            return $qb->getQuery()->getResult();
105
        });
106
107
        if ($returnKey) {
108
            return [
109
                'data' => $result,
110
                'cache_key' => $cacheKey,
111
            ];
112
        }
113
114
        return $result;
115
    }
116
117
    /**
118
     * Runs a Doctrine QueryBuilder and caches the result with tags.
119
     *
120
     * IMPORTANT: Tagging requires a TagAwareAdapter (e.g. Redis).
121
     *
122
     * @param QueryBuilder $qb
123
     * @param string|null  $operationName
124
     * @param array        $parameters
125
     * @param array        $tags
126
     * @param int|null     $ttl
127
     * @param bool         $returnKey
128
     *
129
     * @return array|mixed
130
     */
131
    public function runWithTags(
132
        QueryBuilder $qb,
133
        ?string $operationName = null,
134
        array $parameters = [],
135
        array $tags = [],
136
        ?int $ttl = null,
137
        bool $returnKey = true
138
    ) {
139
        $operationName ??= 'anonymous_query';
140
        $cacheKey = $this->buildCacheKey($operationName, $parameters);
141
142
        $result = $this->cache->get($cacheKey, function (ItemInterface $item) use ($qb, $ttl, $tags) {
143
            $item->expiresAfter($ttl ?? $this->defaultTtl);
144
145
            if (!empty($tags)) {
146
                if (method_exists($item, 'tag')) {
147
                    $item->tag($tags);
148
                }
149
            }
150
151
            return $qb->getQuery()->getResult();
152
        });
153
154
        if ($returnKey) {
155
            return [
156
                'data' => $result,
157
                'cache_key' => $cacheKey,
158
            ];
159
        }
160
161
        return $result;
162
    }
163
164
    /**
165
     * Invalidates the cache for a specific operation and parameters.
166
     *
167
     * @param string $operationName
168
     * @param array  $parameters
169
     */
170
    public function invalidate(string $operationName, array $parameters = []): void
171
    {
172
        $cacheKey = $this->buildCacheKey($operationName, $parameters);
173
        $this->cache->delete($cacheKey);
174
    }
175
176
    /**
177
     * Invalidates all cached entries associated with a specific tag.
178
     *
179
     * Requires a TagAwareAdapter (e.g. Redis) to be configured.
180
     *
181
     * @param string $tag
182
     */
183
    public function invalidateByTag(string $tag): void
184
    {
185
        if (method_exists($this->cache, 'invalidateTags')) {
186
            $this->cache->invalidateTags([$tag]);
187
        }
188
    }
189
190
    /**
191
     * Builds a unique cache key from the operation name and parameters.
192
     *
193
     * @param string $operationName
194
     * @param array  $parameters
195
     *
196
     * @return string
197
     */
198
    public function buildCacheKey(string $operationName, array $parameters): string
199
    {
200
        if (empty($parameters)) {
201
            return $operationName;
202
        }
203
204
        return $operationName . '_' . md5(json_encode($parameters));
205
    }
206
207
    /**
208
     * Generates a cache key for an operation, without executing any query.
209
     * Useful for debugging or manual cache clearing.
210
     *
211
     * @param string $operationName
212
     * @param array  $parameters
213
     *
214
     * @return string
215
     */
216
    public function getCacheKey(string $operationName, array $parameters = []): string
217
    {
218
        return $this->buildCacheKey($operationName, $parameters);
219
    }
220
}
221