Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
118 / 118
100.00% covered (success)
100.00%
24 / 24
CRAP
100.00% covered (success)
100.00%
1 / 1
RequestScopedMetadataCache
100.00% covered (success)
100.00%
117 / 117
100.00% covered (success)
100.00%
24 / 24
52
100.00% covered (success)
100.00%
1 / 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%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findModuleById
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 findAllActiveModules
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findFields
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findGlobalSearchFields
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findSections
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findFilter
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 findModuleFilters
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 findAllModuleOptions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findFacetedFilterOptions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 findAllPicklistOptions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 findAllUitypeOptions
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findPicklistValues
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findFilterGrid
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 findModuleFiltersGrid
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 findWidget
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 findAllActiveWidgets
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findGridRelationsByModule
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 findRelationsBySourceModule
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findMmRelationsBySourceModule
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 findAllUserOptions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 findAllStructureOptions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 clearCache
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
1 / 1
2
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\RelationGridMetadata;
17use App\Core\Engine\Domain\Model\RelationMmMetadata;
18use App\Core\Engine\Domain\Model\SectionMetadata;
19use App\Core\Engine\Domain\Model\WidgetMetadata;
20use App\Core\Engine\Domain\Repository\MetadataRepositoryInterface;
21
22/**
23 * Request-Scoped Metadata Cache.
24 *
25 * Decorator for MetadataRepositoryInterface that caches metadata results
26 * in PHP array memory for the duration of a single HTTP request.
27 * Prevents redundant DB queries when the same module/filter is accessed
28 * multiple times in a single request (e.g., list + schema calls).
29 *
30 * This is the ONLY approved caching mechanism for the engine.
31 * APCu, OPcache cross-request caching is NOT used per project rules.
32 *
33 * @package App\Core\Engine\Infrastructure\Cache
34 */
35final class RequestScopedMetadataCache implements MetadataRepositoryInterface // NOSONAR
36{
37    /**
38     * In-memory module metadata cache for this request.
39     *
40     * @var array<string, ModuleMetadata>
41     */
42    private array $moduleCache = [];
43
44    /**
45     * In-memory field metadata cache for this request keyed by module ID.
46     *
47     * @var array<int, array<int, FieldMetadata>>
48     */
49    private array $fieldCache = [];
50
51    /**
52     * In-memory global search field metadata cache for this request keyed by module ID.
53     *
54     * @var array<int, array<int, FieldMetadata>>
55     */
56    private array $globalSearchFieldsCache = [];
57
58    /**
59     * In-memory section metadata cache for this request keyed by module ID.
60     *
61     * @var array<int, array<int, SectionMetadata>>
62     */
63    private array $sectionCache = [];
64
65    /**
66     * In-memory filter metadata cache for this request.
67     *
68     * @var array<string, FilterMetadata>
69     */
70    private array $filterCache = [];
71
72    /**
73     * In-memory module options cache for this request.
74     *
75     * @var array<int, array{id: int, name: string, label: string}>|null
76     */
77    private ?array $moduleOptionsCache = null;
78
79    /**
80     * In-memory user options cache for this request.
81     *
82     * @var array<string, array<int, array{id: int, label: string, is_active?: int}>>
83     */
84    private array $userOptionsCache = [];
85
86    /**
87     * In-memory structure options cache for this request.
88     *
89     * @var array<string, array<int, array{id: int, label: string, is_active?: int}>>
90     */
91    private array $structureOptionsCache = [];
92
93    /**
94     * RequestScopedMetadataCache constructor.
95     *
96     * @param MetadataRepositoryInterface $inner Decorated SQL metadata repository.
97     */
98    public function __construct(private readonly MetadataRepositoryInterface $inner)
99    {
100    }
101
102    /**
103     * In-memory active modules map cache for this request.
104     *
105     * @var array<string, ModuleMetadata>|null
106     */
107    private ?array $activeModulesCache = null;
108
109    /** {@inheritdoc} */
110    public function findModule(string $moduleName): ModuleMetadata
111    {
112        if (!isset($this->moduleCache[$moduleName])) {
113            $this->moduleCache[$moduleName] = $this->inner->findModule($moduleName);
114        }
115
116        return $this->moduleCache[$moduleName];
117    }
118
119    /** {@inheritdoc} */
120    public function findModuleById(int $moduleId): ModuleMetadata
121    {
122        $cacheKey = "id:{$moduleId}";
123        if (!isset($this->moduleCache[$cacheKey])) {
124            $this->moduleCache[$cacheKey] = $this->inner->findModuleById($moduleId);
125        }
126
127        return $this->moduleCache[$cacheKey];
128    }
129
130    /** {@inheritdoc} */
131    public function findAllActiveModules(): array
132    {
133        if ($this->activeModulesCache === null) {
134            $this->activeModulesCache = $this->inner->findAllActiveModules();
135        }
136
137        return $this->activeModulesCache;
138    }
139
140    /** {@inheritdoc} */
141    public function findFields(int $moduleId): array
142    {
143        if (!isset($this->fieldCache[$moduleId])) {
144            $this->fieldCache[$moduleId] = $this->inner->findFields($moduleId);
145        }
146
147        return $this->fieldCache[$moduleId];
148    }
149
150    /** {@inheritdoc} */
151    public function findGlobalSearchFields(int $moduleId): array
152    {
153        if (!isset($this->globalSearchFieldsCache[$moduleId])) {
154            $this->globalSearchFieldsCache[$moduleId] = $this->inner->findGlobalSearchFields($moduleId);
155        }
156
157        return $this->globalSearchFieldsCache[$moduleId];
158    }
159
160    /** {@inheritdoc} */
161    public function findSections(int $moduleId): array
162    {
163        if (!isset($this->sectionCache[$moduleId])) {
164            $this->sectionCache[$moduleId] = $this->inner->findSections($moduleId);
165        }
166
167        return $this->sectionCache[$moduleId];
168    }
169
170    /**
171     * In-memory module filters list cache for this request.
172     *
173     * @var array<string, array<int, FilterMetadata>>
174     */
175    private array $moduleFiltersCache = [];
176
177    /** {@inheritdoc} */
178    public function findFilter(int $moduleId, ?int $filterId): FilterMetadata
179    {
180        $cacheKey = $moduleId . '_' . ($filterId ?? 'default');
181
182        if (!isset($this->filterCache[$cacheKey])) {
183            $this->filterCache[$cacheKey] = $this->inner->findFilter($moduleId, $filterId);
184        }
185
186        return $this->filterCache[$cacheKey];
187    }
188
189    /** {@inheritdoc} */
190    public function findModuleFilters(int $moduleId, PermissionContext $context): array
191    {
192        $actorKey = $context->isSuperuser ? 'su' : 'u' . $context->actorUserId;
193        $cacheKey = $moduleId . '_' . $actorKey;
194
195        if (!isset($this->moduleFiltersCache[$cacheKey])) {
196            $this->moduleFiltersCache[$cacheKey] = $this->inner->findModuleFilters($moduleId, $context);
197        }
198
199        return $this->moduleFiltersCache[$cacheKey];
200    }
201
202    /** {@inheritdoc} */
203    public function findAllModuleOptions(): array
204    {
205        if ($this->moduleOptionsCache === null) {
206            $this->moduleOptionsCache = $this->inner->findAllModuleOptions();
207        }
208
209        return $this->moduleOptionsCache;
210    }
211
212    /**
213     * In-memory faceted filter options cache for this request.
214     *
215     * @var array<string, array<string, array<int, array{id: int|string, label: string, is_active?: int}>>>
216     */
217    private array $facetedCache = [];
218
219    /** {@inheritdoc} */
220    public function findFacetedFilterOptions(
221        ModuleMetadata    $module,
222        array             $fields,
223        PermissionContext $context
224    ): array {
225        $actorKey = $context->isSuperuser ? 'all' : (string) $context->actorUserId;
226        $cacheKey = $module->id . '_' . $actorKey;
227
228        if (!isset($this->facetedCache[$cacheKey])) {
229            $this->facetedCache[$cacheKey] = $this->inner->findFacetedFilterOptions($module, $fields, $context);
230        }
231
232        return $this->facetedCache[$cacheKey];
233    }
234
235    /**
236     * In-memory picklist options cache for this request.
237     *
238     * @var array<string, array<int, array<string, mixed>>>
239     */
240    private array $picklistOptionsCache = [];
241
242    /**
243     * In-memory picklist values cache for this request.
244     *
245     * @var array<int, array<int, array<string, mixed>>>
246     */
247    private array $picklistValuesCache = [];
248
249    /** {@inheritdoc} */
250    public function findAllPicklistOptions(?int $moduleId = null): array
251    {
252        $cacheKey = $moduleId !== null ? (string) $moduleId : 'all';
253
254        if (!isset($this->picklistOptionsCache[$cacheKey])) {
255            $this->picklistOptionsCache[$cacheKey] = $this->inner->findAllPicklistOptions($moduleId);
256        }
257
258        return $this->picklistOptionsCache[$cacheKey];
259    }
260
261    /**
262     * In-memory uitype options cache for this request.
263     *
264     * @var array<int, array{id: int, name: string, label: string}>|null
265     */
266    private ?array $uitypeOptionsCache = null;
267
268    /** {@inheritdoc} */
269    public function findAllUitypeOptions(): array
270    {
271        if ($this->uitypeOptionsCache === null) {
272            $this->uitypeOptionsCache = $this->inner->findAllUitypeOptions();
273        }
274
275        return $this->uitypeOptionsCache;
276    }
277
278    /** {@inheritdoc} */
279    public function findPicklistValues(int $picklistId): array
280    {
281        if (!isset($this->picklistValuesCache[$picklistId])) {
282            $this->picklistValuesCache[$picklistId] = $this->inner->findPicklistValues($picklistId);
283        }
284
285        return $this->picklistValuesCache[$picklistId];
286    }
287
288    /**
289     * In-memory 1:M relations cache for this request keyed by source module ID.
290     *
291     * @var array<int, array<int, \App\Core\Engine\Domain\Model\Relation1mMetadata>>
292     */
293    private array $relationsCache = [];
294
295    /**
296     * In-memory M:M relations cache for this request keyed by source module ID.
297     *
298     * @var array<int, array<int, \App\Core\Engine\Domain\Model\RelationMmMetadata>>
299     */
300    private array $mmRelationsCache = [];
301
302    /**
303     * In-memory GRID filter cache for this request.
304     *
305     * @var array<string, FilterGridMetadata>
306     */
307    private array $filterGridCache = [];
308
309    /**
310     * In-memory module GRID filters list cache.
311     *
312     * @var array<string, array<int, FilterGridMetadata>>
313     */
314    private array $moduleFiltersGridCache = [];
315
316    /**
317     * In-memory widgets catalog cache.
318     *
319     * @var array<int, WidgetMetadata>
320     */
321    private array $widgetCache = [];
322
323    /**
324     * In-memory all active widgets cache.
325     *
326     * @var array<int, WidgetMetadata>|null
327     */
328    private ?array $activeWidgetsCache = null;
329
330    /**
331     * In-memory module GRID relations cache.
332     *
333     * @var array<string, array<int, RelationGridMetadata>>
334     */
335    private array $gridRelationsByModuleCache = [];
336
337    /** {@inheritdoc} */
338    public function findFilterGrid(int $moduleId, ?int $filterGridId): FilterGridMetadata
339    {
340        $cacheKey = "{$moduleId}:" . ($filterGridId ?? 'default');
341        if (!isset($this->filterGridCache[$cacheKey])) {
342            $this->filterGridCache[$cacheKey] = $this->inner->findFilterGrid($moduleId, $filterGridId);
343        }
344
345        return $this->filterGridCache[$cacheKey];
346    }
347
348    /** {@inheritdoc} */
349    public function findModuleFiltersGrid(int $moduleId, PermissionContext $context): array
350    {
351        $cacheKey = "{$moduleId}:{$context->actorUserId}:" . ($context->isSuperuser ? '1' : '0');
352        if (!isset($this->moduleFiltersGridCache[$cacheKey])) {
353            $this->moduleFiltersGridCache[$cacheKey] = $this->inner->findModuleFiltersGrid($moduleId, $context);
354        }
355
356        return $this->moduleFiltersGridCache[$cacheKey];
357    }
358
359    /** {@inheritdoc} */
360    public function findWidget(int $widgetId): ?WidgetMetadata
361    {
362        if (!isset($this->widgetCache[$widgetId])) {
363            $widget = $this->inner->findWidget($widgetId);
364            if ($widget !== null) {
365                $this->widgetCache[$widgetId] = $widget;
366            }
367            return $widget;
368        }
369
370        return $this->widgetCache[$widgetId];
371    }
372
373    /** {@inheritdoc} */
374    public function findAllActiveWidgets(): array
375    {
376        if ($this->activeWidgetsCache === null) {
377            $this->activeWidgetsCache = $this->inner->findAllActiveWidgets();
378        }
379
380        return $this->activeWidgetsCache;
381    }
382
383    /** {@inheritdoc} */
384    public function findGridRelationsByModule(int $moduleId, ?int $filterGridId = null): array
385    {
386        $cacheKey = "{$moduleId}:" . ($filterGridId ?? 'all');
387        if (!isset($this->gridRelationsByModuleCache[$cacheKey])) {
388            $this->gridRelationsByModuleCache[$cacheKey] = $this->inner->findGridRelationsByModule(
389                $moduleId,
390                $filterGridId
391            );
392        }
393
394        return $this->gridRelationsByModuleCache[$cacheKey];
395    }
396
397    /** {@inheritdoc} */
398    public function findRelationsBySourceModule(int $sourceModuleId): array
399    {
400        if (!isset($this->relationsCache[$sourceModuleId])) {
401            $this->relationsCache[$sourceModuleId] = $this->inner->findRelationsBySourceModule($sourceModuleId);
402        }
403
404        return $this->relationsCache[$sourceModuleId];
405    }
406
407    /** {@inheritdoc} */
408    public function findMmRelationsBySourceModule(int $sourceModuleId): array
409    {
410        if (!isset($this->mmRelationsCache[$sourceModuleId])) {
411            $this->mmRelationsCache[$sourceModuleId] = $this->inner->findMmRelationsBySourceModule($sourceModuleId);
412        }
413
414        return $this->mmRelationsCache[$sourceModuleId];
415    }
416
417    /** {@inheritdoc} */
418    public function findAllUserOptions(?string $tablePrefix = null): array
419    {
420        $key = $tablePrefix ?? 'default';
421        if (!isset($this->userOptionsCache[$key])) {
422            $this->userOptionsCache[$key] = $this->inner->findAllUserOptions($tablePrefix);
423        }
424
425        return $this->userOptionsCache[$key];
426    }
427
428    /** {@inheritdoc} */
429    public function findAllStructureOptions(?string $tablePrefix = null): array
430    {
431        $key = $tablePrefix ?? 'default';
432        if (!isset($this->structureOptionsCache[$key])) {
433            $this->structureOptionsCache[$key] = $this->inner->findAllStructureOptions($tablePrefix);
434        }
435
436        return $this->structureOptionsCache[$key];
437    }
438
439    /** {@inheritdoc} */
440    public function clearCache(?int $moduleId = null): void
441    {
442        if ($moduleId !== null) {
443            unset($this->fieldCache[$moduleId]);
444            unset($this->globalSearchFieldsCache[$moduleId]);
445            unset($this->relationsCache[$moduleId]);
446            unset($this->mmRelationsCache[$moduleId]);
447            unset($this->moduleCache["id:{$moduleId}"]);
448            $this->activeModulesCache = null;
449            $this->moduleOptionsCache = null;
450            $this->structureOptionsCache = [];
451            unset($this->picklistOptionsCache[(string) $moduleId]);
452            unset($this->picklistOptionsCache['all']);
453            $this->filterGridCache = [];
454            $this->moduleFiltersGridCache = [];
455            $this->gridRelationsByModuleCache = [];
456        } else {
457            $this->moduleCache = [];
458            $this->fieldCache = [];
459            $this->globalSearchFieldsCache = [];
460            $this->relationsCache = [];
461            $this->mmRelationsCache = [];
462            $this->filterCache = [];
463            $this->filterGridCache = [];
464            $this->moduleFiltersGridCache = [];
465            $this->widgetCache = [];
466            $this->activeWidgetsCache = null;
467            $this->gridRelationsByModuleCache = [];
468            $this->moduleOptionsCache = null;
469            $this->userOptionsCache = [];
470            $this->structureOptionsCache = [];
471            $this->activeModulesCache = null;
472            $this->picklistOptionsCache = [];
473            $this->picklistValuesCache = [];
474        }
475
476        $this->inner->clearCache($moduleId);
477    }
478}