Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
8.33% |
1 / 12 |
|
25.00% |
1 / 4 |
CRAP | |
0.00% |
0 / 1 |
| ResultCache | |
8.33% |
1 / 12 |
|
25.00% |
1 / 4 |
33.73 | |
0.00% |
0 / 1 |
| remember | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| forgetDomain | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| rememberKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| forget | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Services; |
| 4 | |
| 5 | use Illuminate\Cache\TaggableStore; |
| 6 | use Illuminate\Support\Facades\Cache; |
| 7 | |
| 8 | /** |
| 9 | * FIRE-1145: Centralised read-through query cache with domain-scoped invalidation. |
| 10 | * |
| 11 | * Replaces the legacy pattern of `Cache::get/put` (with `base64_encode($sql)` as |
| 12 | * key) + `Cache::flush()` on every write — which destroyed the entire app cache |
| 13 | * on every mutation across 45 call sites, regardless of domain. |
| 14 | * |
| 15 | * Usage on the READ path (drop-in replacement for the old Cache::get/put block): |
| 16 | * |
| 17 | * $result = ResultCache::remember('quotations', $query, 600, fn() => DB::select($query)); |
| 18 | * |
| 19 | * Usage on the WRITE path (drop-in replacement for `Cache::flush()`): |
| 20 | * |
| 21 | * ResultCache::forgetDomain(['quotations', 'users']); |
| 22 | * |
| 23 | * Domains in use (see findings/FIRE-1145_Investigation.md for the full map): |
| 24 | * - 'quotations' — list_quotations + analytics endpoints |
| 25 | * - 'ongoing_jobs' — list_ongoing_jobs |
| 26 | * - 'pipelines' — list_pipelines |
| 27 | * - 'leave' — list_leave |
| 28 | * - 'itv' — list_itv |
| 29 | * - 'digital_campaign_analytics' — DigitalCampaignAnalytics endpoints |
| 30 | * - 'users' — get_commercial_with_pendings, get_commercials, etc. |
| 31 | * |
| 32 | * Cross-domain dependencies (some mutations invalidate more than one domain — see |
| 33 | * the inventory in the findings file before adding new call sites). |
| 34 | * |
| 35 | * Raw `Cache::flush()` is BANNED — a CI grep gate in bitbucket-pipelines enforces this. |
| 36 | */ |
| 37 | final class ResultCache |
| 38 | { |
| 39 | /** |
| 40 | * Read-through cache for query result sets, scoped to a domain tag. |
| 41 | * |
| 42 | * @param string $domain Domain tag (one of the values listed in the class docblock). |
| 43 | * @param string $sql The raw SQL string — used to derive the cache key. |
| 44 | * @param int $ttl TTL in seconds. Existing call sites use 600 (10 minutes). |
| 45 | * @param callable $fn Closure that runs the query on cache miss. |
| 46 | */ |
| 47 | public static function remember(string $domain, string $sql, int $ttl, callable $fn): mixed |
| 48 | { |
| 49 | // Domain-scoped invalidation relies on cache tags, which only the |
| 50 | // redis/memcached/array stores support (prod = redis). On a store that |
| 51 | // can't tag (e.g. local CACHE_DRIVER=file) `Cache::tags()` throws |
| 52 | // "This cache store does not support tagging." Fall back to running the |
| 53 | // query uncached so the app still works locally instead of 500-ing. |
| 54 | if (! Cache::getStore() instanceof TaggableStore) { |
| 55 | return $fn(); |
| 56 | } |
| 57 | |
| 58 | return Cache::tags([$domain])->remember( |
| 59 | 'sql:'.md5($sql), |
| 60 | $ttl, |
| 61 | $fn |
| 62 | ); |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * Invalidate all cache entries tagged with the given domain(s). |
| 67 | * Pass a string for one domain, or an array for several at once. |
| 68 | */ |
| 69 | public static function forgetDomain(string|array $domains): void |
| 70 | { |
| 71 | // Nothing to invalidate when the store can't tag (remember() didn't |
| 72 | // cache anything in that case). |
| 73 | if (! Cache::getStore() instanceof TaggableStore) { |
| 74 | return; |
| 75 | } |
| 76 | |
| 77 | Cache::tags((array) $domains)->flush(); |
| 78 | } |
| 79 | |
| 80 | /** |
| 81 | * Read-through cache with an explicit (non-SQL) key. Use this when |
| 82 | * the cached value isn't a query result — e.g. `company_ids:{$userId}` |
| 83 | * built from an Eloquent `pluck()` call. |
| 84 | * |
| 85 | * @param string $domain Domain tag (one of the values listed in the class docblock). |
| 86 | * @param string $key Explicit cache key (NOT hashed — caller picks). |
| 87 | * @param int $ttl TTL in seconds. |
| 88 | * @param callable $fn Closure that builds the value on cache miss. |
| 89 | */ |
| 90 | public static function rememberKey(string $domain, string $key, int $ttl, callable $fn): mixed |
| 91 | { |
| 92 | return Cache::tags([$domain])->remember($key, $ttl, $fn); |
| 93 | } |
| 94 | |
| 95 | /** |
| 96 | * Invalidate a single explicit key inside a domain. Use when the |
| 97 | * mutation only affects one user / one row and a full domain flush |
| 98 | * would be wasteful. |
| 99 | */ |
| 100 | public static function forget(string $domain, string $key): void |
| 101 | { |
| 102 | Cache::tags([$domain])->forget($key); |
| 103 | } |
| 104 | } |