Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.95% covered (success)
91.95%
160 / 174
91.67% covered (success)
91.67%
22 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
PersistentMetadataCache
91.91% covered (success)
91.91%
159 / 173
91.67% covered (success)
91.67%
22 / 24
29.45
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findModule
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findModuleById
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findAllActiveModules
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 findFields
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findGlobalSearchFields
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findSections
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findFilter
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findModuleFilters
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 findAllModuleOptions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findFacetedFilterOptions
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 findAllUserOptions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findAllStructureOptions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findAllPicklistOptions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findAllUitypeOptions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findPicklistValues
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findFilterGrid
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findModuleFiltersGrid
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 findWidget
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 findAllActiveWidgets
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 findGridRelationsByModule
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findRelationsBySourceModule
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 findMmRelationsBySourceModule
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 clearCache
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Engine\Infrastructure\Cache;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Model\FieldMetadata;
12use App\Core\Engine\Domain\Model\FilterGridMetadata;
13use App\Core\Engine\Domain\Model\FilterMetadata;
14use App\Core\Engine\Domain\Model\ModuleMetadata;
15use App\Core\Engine\Domain\Model\PermissionContext;
16use App\Core\Engine\Domain\Model\Relation1mMetadata;
17use App\Core\Engine\Domain\Model\RelationGridMetadata;
18use App\Core\Engine\Domain\Model\RelationMmMetadata;
19use App\Core\Engine\Domain\Model\SectionMetadata;
20use App\Core\Engine\Domain\Model\WidgetMetadata;
21use App\Core\Engine\Domain\Repository\MetadataRepositoryInterface;
22use Yiisoft\Cache\CacheInterface;
23
24/**
25 * Persistent Metadata Cache Decorator.
26 *
27 * Decorates MetadataRepositoryInterface using Yiisoft CacheInterface for
28 * cross-request persistent caching of schemas, fields, picklists and filters.
29 *
30 * @package App\Core\Engine\Infrastructure\Cache
31 */
32final readonly class PersistentMetadataCache implements MetadataRepositoryInterface // NOSONAR
33{
34    use PersistentCacheHelperTrait;
35
36    private const string KEY_PREFIX = 'ammonly_meta_';
37    private const int DEFAULT_TTL = 3600;
38
39    /**
40     * PersistentMetadataCache constructor.
41     *
42     * @param MetadataRepositoryInterface $inner Decorated metadata repository.
43     * @param CacheInterface              $cache Yiisoft Cache implementation.
44     * @param int                         $ttl   Time to live in seconds.
45     */
46    public function __construct(
47        private MetadataRepositoryInterface $inner,
48        private CacheInterface $cache,
49        private int $ttl = self::DEFAULT_TTL,
50    ) {
51    }
52
53    /** {@inheritdoc} */
54    public function findModule(string $moduleName): ModuleMetadata
55    {
56        $key = self::KEY_PREFIX . 'mod_name_' . $moduleName;
57
58        /** @var ModuleMetadata */
59        return $this->safeGetOrSet(
60            $this->cache,
61            $key,
62            fn(): ModuleMetadata => $this->inner->findModule($moduleName),
63            $this->ttl
64        );
65    }
66
67    /** {@inheritdoc} */
68    public function findModuleById(int $moduleId): ModuleMetadata
69    {
70        $key = self::KEY_PREFIX . 'mod_id_' . $moduleId;
71
72        /** @var ModuleMetadata */
73        return $this->safeGetOrSet(
74            $this->cache,
75            $key,
76            fn(): ModuleMetadata => $this->inner->findModuleById($moduleId),
77            $this->ttl
78        );
79    }
80
81    /** {@inheritdoc} */
82    public function findAllActiveModules(): array
83    {
84        $key = self::KEY_PREFIX . 'all_active_mods';
85
86        /** @var array<string, ModuleMetadata>|null $cached */
87        $cached = $this->safeGetOrSet(
88            $this->cache,
89            $key,
90            fn(): array => $this->inner->findAllActiveModules(),
91            $this->ttl
92        );
93
94        return $cached ?? [];
95    }
96
97    /** {@inheritdoc} */
98    public function findFields(int $moduleId): array
99    {
100        $key = self::KEY_PREFIX . 'fields_mod_' . $moduleId;
101
102        /** @var array<int, FieldMetadata> */
103        return $this->safeGetOrSet(
104            $this->cache,
105            $key,
106            fn(): array => $this->inner->findFields($moduleId),
107            $this->ttl
108        );
109    }
110
111    /** {@inheritdoc} */
112    public function findGlobalSearchFields(int $moduleId): array
113    {
114        $key = self::KEY_PREFIX . 'search_fields_mod_' . $moduleId;
115
116        /** @var array<int, FieldMetadata> */
117        return $this->safeGetOrSet(
118            $this->cache,
119            $key,
120            fn(): array => $this->inner->findGlobalSearchFields($moduleId),
121            $this->ttl
122        );
123    }
124
125    /** {@inheritdoc} */
126    public function findSections(int $moduleId): array
127    {
128        $key = self::KEY_PREFIX . 'sections_mod_' . $moduleId;
129
130        /** @var array<int, SectionMetadata> */
131        return $this->safeGetOrSet(
132            $this->cache,
133            $key,
134            fn(): array => $this->inner->findSections($moduleId),
135            $this->ttl
136        );
137    }
138
139    /** {@inheritdoc} */
140    public function findFilter(int $moduleId, ?int $filterId): FilterMetadata
141    {
142        $key = self::KEY_PREFIX . 'filter_mod_' . $moduleId . '_' . ($filterId ?? 'default');
143
144        /** @var FilterMetadata */
145        return $this->safeGetOrSet(
146            $this->cache,
147            $key,
148            fn(): FilterMetadata => $this->inner->findFilter($moduleId, $filterId),
149            $this->ttl
150        );
151    }
152
153    /** {@inheritdoc} */
154    public function findModuleFilters(int $moduleId, PermissionContext $context): array
155    {
156        $actorKey = $context->isSuperuser ? 'su' : 'u' . $context->actorUserId;
157        $key      = self::KEY_PREFIX . 'mod_filters_' . $moduleId . '_' . $actorKey;
158
159        /** @var array<int, FilterMetadata> */
160        return $this->safeGetOrSet(
161            $this->cache,
162            $key,
163            fn(): array => $this->inner->findModuleFilters($moduleId, $context),
164            $this->ttl
165        );
166    }
167
168    /** {@inheritdoc} */
169    public function findAllModuleOptions(): array
170    {
171        $key = self::KEY_PREFIX . 'all_mod_opts';
172
173        /** @var array<int, array{id: int, name: string, label: string}> */
174        return $this->safeGetOrSet(
175            $this->cache,
176            $key,
177            fn(): array => $this->inner->findAllModuleOptions(),
178            $this->ttl
179        );
180    }
181
182    /** {@inheritdoc} */
183    public function findFacetedFilterOptions(
184        ModuleMetadata    $module,
185        array             $fields,
186        PermissionContext $context
187    ): array {
188        $actorKey = $context->isSuperuser ? 'su' : 'u' . $context->actorUserId;
189        $key = self::KEY_PREFIX . 'faceted_' . $module->id . '_' . $actorKey;
190
191        /** @var array<string, array<int, array{id: int|string, label: string, is_active?: int}>> */
192        return $this->safeGetOrSet(
193            $this->cache,
194            $key,
195            fn(): array => $this->inner->findFacetedFilterOptions($module, $fields, $context),
196            $this->ttl
197        );
198    }
199
200    /** {@inheritdoc} */
201    public function findAllUserOptions(?string $tablePrefix = null): array
202    {
203        $key = self::KEY_PREFIX . 'all_usr_opts_' . ($tablePrefix ?? 'def');
204
205        /** @var array<int, array{id: int, label: string, is_active?: int}> */
206        return $this->safeGetOrSet(
207            $this->cache,
208            $key,
209            fn(): array => $this->inner->findAllUserOptions($tablePrefix),
210            $this->ttl
211        );
212    }
213
214    /** {@inheritdoc} */
215    public function findAllStructureOptions(?string $tablePrefix = null): array
216    {
217        $key = self::KEY_PREFIX . 'all_str_opts_' . ($tablePrefix ?? 'def');
218
219        /** @var array<int, array{id: int, label: string, is_active?: int}> */
220        return $this->safeGetOrSet(
221            $this->cache,
222            $key,
223            fn(): array => $this->inner->findAllStructureOptions($tablePrefix),
224            $this->ttl
225        );
226    }
227
228    /** {@inheritdoc} */
229    public function findAllPicklistOptions(?int $moduleId = null): array
230    {
231        $key = self::KEY_PREFIX . 'all_pkl_opts_' . ($moduleId ?? 'all');
232
233        /** @var array<int, array<string, mixed>> */
234        return $this->safeGetOrSet(
235            $this->cache,
236            $key,
237            fn(): array => $this->inner->findAllPicklistOptions($moduleId),
238            $this->ttl
239        );
240    }
241
242    /** {@inheritdoc} */
243    public function findAllUitypeOptions(): array
244    {
245        $key = self::KEY_PREFIX . 'all_uitype_opts';
246
247        /** @var array<int, array{id: int, name: string, label: string}> */
248        return $this->safeGetOrSet(
249            $this->cache,
250            $key,
251            fn(): array => $this->inner->findAllUitypeOptions(),
252            $this->ttl
253        );
254    }
255
256    /** {@inheritdoc} */
257    public function findPicklistValues(int $picklistId): array
258    {
259        $key = self::KEY_PREFIX . 'pkl_vals_' . $picklistId;
260
261        /** @var array<int, array<string, mixed>> */
262        return $this->safeGetOrSet(
263            $this->cache,
264            $key,
265            fn(): array => $this->inner->findPicklistValues($picklistId),
266            $this->ttl
267        );
268    }
269
270    /** {@inheritdoc} */
271    public function findFilterGrid(int $moduleId, ?int $filterGridId): FilterGridMetadata
272    {
273        $key = self::KEY_PREFIX . 'flt_grid_' . $moduleId . '_' . ($filterGridId ?? 'default');
274
275        /** @var FilterGridMetadata */
276        return $this->safeGetOrSet(
277            $this->cache,
278            $key,
279            fn(): FilterGridMetadata => $this->inner->findFilterGrid($moduleId, $filterGridId),
280            $this->ttl
281        );
282    }
283
284    /** {@inheritdoc} */
285    public function findModuleFiltersGrid(int $moduleId, PermissionContext $context): array
286    {
287        $actorKey = $context->isSuperuser ? 'su' : 'u' . $context->actorUserId;
288        $key      = self::KEY_PREFIX . 'mod_flts_grid_' . $moduleId . '_' . $actorKey;
289
290        /** @var array<int, FilterGridMetadata> */
291        return $this->safeGetOrSet(
292            $this->cache,
293            $key,
294            fn(): array => $this->inner->findModuleFiltersGrid($moduleId, $context),
295            $this->ttl
296        );
297    }
298
299    /** {@inheritdoc} */
300    public function findWidget(int $widgetId): ?WidgetMetadata
301    {
302        $key = self::KEY_PREFIX . 'widget_id_' . $widgetId;
303
304        /** @var WidgetMetadata|null */
305        return $this->safeGetOrSet(
306            $this->cache,
307            $key,
308            fn(): ?WidgetMetadata => $this->inner->findWidget($widgetId),
309            $this->ttl
310        );
311    }
312
313    /** {@inheritdoc} */
314    public function findAllActiveWidgets(): array
315    {
316        $key = self::KEY_PREFIX . 'all_active_widgets';
317
318        /** @var array<int, WidgetMetadata> */
319        return $this->safeGetOrSet(
320            $this->cache,
321            $key,
322            fn(): array => $this->inner->findAllActiveWidgets(),
323            $this->ttl
324        );
325    }
326
327    /** {@inheritdoc} */
328    public function findGridRelationsByModule(int $moduleId, ?int $filterGridId = null): array
329    {
330        $key = self::KEY_PREFIX . 'grid_rels_mod_' . $moduleId . '_' . ($filterGridId ?? 'all');
331
332        /** @var array<int, RelationGridMetadata> */
333        return $this->safeGetOrSet(
334            $this->cache,
335            $key,
336            fn(): array => $this->inner->findGridRelationsByModule($moduleId, $filterGridId),
337            $this->ttl
338        );
339    }
340
341    /** {@inheritdoc} */
342    public function findRelationsBySourceModule(int $sourceModuleId): array
343    {
344        $key = self::KEY_PREFIX . 'relations_mod_' . $sourceModuleId;
345
346        /** @var array<int, Relation1mMetadata> */
347        return $this->safeGetOrSet(
348            $this->cache,
349            $key,
350            fn(): array => $this->inner->findRelationsBySourceModule($sourceModuleId),
351            $this->ttl
352        );
353    }
354
355    /** {@inheritdoc} */
356    public function findMmRelationsBySourceModule(int $sourceModuleId): array
357    {
358        $key = self::KEY_PREFIX . 'relations_mm_mod_' . $sourceModuleId;
359
360        /** @var array<int, RelationMmMetadata> */
361        return $this->safeGetOrSet(
362            $this->cache,
363            $key,
364            fn(): array => $this->inner->findMmRelationsBySourceModule($sourceModuleId),
365            $this->ttl
366        );
367    }
368
369    /** {@inheritdoc} */
370    public function clearCache(?int $moduleId = null): void
371    {
372        try {
373            if ($moduleId !== null) {
374                $this->cache->remove(self::KEY_PREFIX . 'fields_mod_' . $moduleId);
375                $this->cache->remove(self::KEY_PREFIX . 'search_fields_mod_' . $moduleId);
376                $this->cache->remove(self::KEY_PREFIX . 'mod_id_' . $moduleId);
377                $this->cache->remove(self::KEY_PREFIX . 'relations_mod_' . $moduleId);
378                $this->cache->remove(self::KEY_PREFIX . 'relations_mm_mod_' . $moduleId);
379                $this->cache->remove(self::KEY_PREFIX . 'all_active_mods');
380                $this->cache->remove(self::KEY_PREFIX . 'all_mod_opts');
381                $this->cache->remove(self::KEY_PREFIX . 'all_pkl_opts_' . $moduleId);
382                $this->cache->remove(self::KEY_PREFIX . 'all_pkl_opts_all');
383                $this->cache->remove(self::KEY_PREFIX . 'all_active_widgets');
384            } else {
385                $this->cache->clear();
386            }
387        } catch (\Throwable) {
388            // Cache invalidation best-effort
389        }
390
391        $this->inner->clearCache($moduleId);
392    }
393}