Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.83% covered (success)
95.83%
46 / 48
94.74% covered (success)
94.74%
18 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
CachedAccessRepository
95.74% covered (success)
95.74%
45 / 47
94.74% covered (success)
94.74%
18 / 19
26
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
 getModuleLevel
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAllModuleLevels
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setModuleLevel
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 findRulesByModule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 countRulesByModule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findRuleById
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 saveRule
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 deleteRule
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 findMatchingRules
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canAccessNonCrudModule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 recompileModule
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 recompileUser
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 recompileAll
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasSharedOwnerAccess
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadAllLevels
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
4.06
 invalidateCache
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 serializeLevels
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 deserializeLevels
100.00% covered (success)
100.00%
5 / 5
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\Access\Infrastructure\Cache;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Access\Domain\Model\AccessRule;
12use App\Core\Access\Domain\Model\ModuleAccessLevel;
13use App\Core\Access\Domain\Repository\AccessRepositoryInterface;
14use Yiisoft\Cache\CacheInterface;
15
16/**
17 * High-performance Cached Decorator for AccessRepositoryInterface.
18 *
19 * Implements two-tier caching (in-memory request cache and optional persistent CacheInterface)
20 * for module access tiers to eliminate SQL queries during standard user navigation.
21 * Automatically invalidates cache on mutations and rule recompilations.
22 *
23 * @package App\Core\Access\Infrastructure\Cache
24 */
25final class CachedAccessRepository implements AccessRepositoryInterface
26{
27    private const string CACHE_KEY_LEVELS = 'ammonly_access_module_levels';
28
29    /**
30     * In-memory request-level cache of configured module access levels.
31     *
32     * @var array<string, ModuleAccessLevel>|null
33     */
34    private ?array $memoryLevels = null;
35
36    /**
37     * CachedAccessRepository constructor.
38     *
39     * @param AccessRepositoryInterface $inner Concrete access repository (e.g. SqlAccessRepository).
40     * @param CacheInterface|null       $cache Persistent cache interface (optional).
41     */
42    public function __construct(
43        private readonly AccessRepositoryInterface $inner,
44        private readonly ?CacheInterface           $cache = null,
45    ) {
46    }
47
48    /**
49     * {@inheritdoc}
50     */
51    public function getModuleLevel(string $moduleName): ModuleAccessLevel
52    {
53        $levels = $this->loadAllLevels();
54
55        return $levels[$moduleName] ?? ModuleAccessLevel::PRIVATE;
56    }
57
58    /**
59     * {@inheritdoc}
60     */
61    public function getAllModuleLevels(): array
62    {
63        return $this->loadAllLevels();
64    }
65
66    /**
67     * {@inheritdoc}
68     */
69    public function setModuleLevel(string $moduleName, ModuleAccessLevel $level, ?int $updatedBy = null): void
70    {
71        $this->invalidateCache();
72        $this->inner->setModuleLevel($moduleName, $level, $updatedBy);
73    }
74
75    /**
76     * {@inheritdoc}
77     */
78    public function findRulesByModule(string $moduleName): array
79    {
80        return $this->inner->findRulesByModule($moduleName);
81    }
82
83    /**
84     * {@inheritdoc}
85     */
86    public function countRulesByModule(): array
87    {
88        return $this->inner->countRulesByModule();
89    }
90
91    /**
92     * {@inheritdoc}
93     */
94    public function findRuleById(int $id): ?AccessRule
95    {
96        return $this->inner->findRuleById($id);
97    }
98
99    /**
100     * {@inheritdoc}
101     */
102    public function saveRule(AccessRule $rule): int
103    {
104        $this->invalidateCache();
105
106        return $this->inner->saveRule($rule);
107    }
108
109    /**
110     * {@inheritdoc}
111     */
112    public function deleteRule(int $id): bool
113    {
114        $this->invalidateCache();
115
116        return $this->inner->deleteRule($id);
117    }
118
119    /**
120     * {@inheritdoc}
121     */
122    public function findMatchingRules(string $moduleName, int $actorUserId, array $actorStructureIds = []): array
123    {
124        return $this->inner->findMatchingRules($moduleName, $actorUserId, $actorStructureIds);
125    }
126
127    /**
128     * {@inheritdoc}
129     */
130    public function canAccessNonCrudModule(string $moduleName, int $actorUserId, array $actorStructureIds = []): bool
131    {
132        return $this->inner->canAccessNonCrudModule($moduleName, $actorUserId, $actorStructureIds);
133    }
134
135    /**
136     * {@inheritdoc}
137     */
138    public function recompileModule(string $moduleName): void
139    {
140        $this->invalidateCache();
141        $this->inner->recompileModule($moduleName);
142    }
143
144    /**
145     * {@inheritdoc}
146     */
147    public function recompileUser(int $userId): void
148    {
149        $this->invalidateCache();
150        $this->inner->recompileUser($userId);
151    }
152
153    /**
154     * {@inheritdoc}
155     */
156    public function recompileAll(): void
157    {
158        $this->invalidateCache();
159        $this->inner->recompileAll();
160    }
161
162    /**
163     * {@inheritdoc}
164     */
165    public function hasSharedOwnerAccess(
166        string $moduleName,
167        int    $actorUserId,
168        string $ownerType,
169        int    $ownerId,
170        string $action = 'read'
171    ): bool {
172        return $this->inner->hasSharedOwnerAccess($moduleName, $actorUserId, $ownerType, $ownerId, $action);
173    }
174
175    /**
176     * Loads all module levels from memory, persistent cache, or inner repository.
177     *
178     * @return array<string, ModuleAccessLevel>
179     */
180    private function loadAllLevels(): array
181    {
182        if ($this->memoryLevels !== null) {
183            return $this->memoryLevels;
184        }
185
186        if ($this->cache !== null) {
187            /** @var mixed $cachedRaw */
188            $cachedRaw = $this->cache->getOrSet(
189                self::CACHE_KEY_LEVELS,
190                fn(): array => $this->serializeLevels($this->inner->getAllModuleLevels()),
191                3600
192            );
193
194            if (is_array($cachedRaw)) {
195                $this->memoryLevels = $this->deserializeLevels($cachedRaw);
196                return $this->memoryLevels;
197            }
198        }
199
200        $this->memoryLevels = $this->inner->getAllModuleLevels();
201
202        return $this->memoryLevels;
203    }
204
205    /**
206     * Invalidates in-memory and persistent levels cache.
207     */
208    private function invalidateCache(): void
209    {
210        $this->memoryLevels = null;
211        if ($this->cache !== null) {
212            $this->cache->remove(self::CACHE_KEY_LEVELS);
213        }
214    }
215
216    /**
217     * Serializes module levels array into raw string representation for persistent cache.
218     *
219     * @param array<string, ModuleAccessLevel> $levels
220     * @return array<string, string>
221     */
222    private function serializeLevels(array $levels): array
223    {
224        $raw = [];
225        foreach ($levels as $mod => $lvl) {
226            $raw[$mod] = $lvl->value;
227        }
228
229        return $raw;
230    }
231
232    /**
233     * Deserializes raw string representation from persistent cache into ModuleAccessLevel enum map.
234     *
235     * @param array<string, mixed> $raw
236     * @return array<string, ModuleAccessLevel>
237     */
238    private function deserializeLevels(array $raw): array
239    {
240        $levels = [];
241        foreach ($raw as $mod => $val) {
242            if (is_string($val)) {
243                $levels[$mod] = ModuleAccessLevel::tryFrom($val) ?? ModuleAccessLevel::PRIVATE;
244            }
245        }
246
247        return $levels;
248    }
249}