Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
ConfigurationCascadeResolver
100.00% covered (success)
100.00%
48 / 48
100.00% covered (success)
100.00%
7 / 7
26
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
 resolve
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 resolveUserLevel
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 resolveDeviceScopedValue
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
7
 resolveStandardScopedValue
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 resolveModuleFallbackValue
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 resolveSystemLevel
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
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\Preference\Domain\Service;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Preference\Domain\Model\PreferenceScope;
12use App\Core\Preference\Domain\Repository\UserPreferenceRepositoryInterface;
13use App\Core\Settings\SettingsRepositoryInterface;
14
15/**
16 * Multi-layer Configuration Cascade Resolver.
17 *
18 * Implements the resolution hierarchy:
19 * 1. User + Device + Entity Scope
20 * 2. User + Device + Module Scope
21 * 3. User + Module Scope (Cross-Device)
22 * 4. User Global Scope
23 * 5. System Settings Repository Level
24 * 6. Hardcoded Fallback Default
25 *
26 * @package App\Core\Preference\Domain\Service
27 */
28final readonly class ConfigurationCascadeResolver implements ConfigurationCascadeResolverInterface
29{
30    /**
31     * ConfigurationCascadeResolver constructor.
32     *
33     * @param UserPreferenceRepositoryInterface $userPreferences User preferences persistence.
34     * @param SettingsRepositoryInterface|null  $systemSettings  Optional system settings repository.
35     */
36    public function __construct(
37        private UserPreferenceRepositoryInterface $userPreferences,
38        private ?SettingsRepositoryInterface $systemSettings = null,
39    ) {
40    }
41
42    /**
43     * @inheritDoc
44     */
45    public function resolve(PreferenceScope $scope, string $key, mixed $fallbackDefault = null): mixed
46    {
47        $userLevelValue = $this->resolveUserLevel($scope, $key);
48        if ($userLevelValue !== null) {
49            return $userLevelValue;
50        }
51
52        $systemLevelValue = $this->resolveSystemLevel($scope, $key);
53        if ($systemLevelValue !== null) {
54            return $systemLevelValue;
55        }
56
57        return $fallbackDefault;
58    }
59
60    /**
61     * Resolves value from user preference scopes (Device+Entity -> Device+Module -> Module -> Global).
62     *
63     * @param PreferenceScope $scope Scope coordinates.
64     * @param string          $key   Preference key.
65     * @return mixed Found value or null.
66     */
67    private function resolveUserLevel(PreferenceScope $scope, string $key): mixed
68    {
69        $deviceVal = $this->resolveDeviceScopedValue($scope, $key);
70        if ($deviceVal !== null) {
71            return $deviceVal;
72        }
73
74        return $this->resolveStandardScopedValue($scope, $key);
75    }
76
77    /**
78     * Resolves device-specific preference scopes.
79     */
80    private function resolveDeviceScopedValue(PreferenceScope $scope, string $key): mixed
81    {
82        if ($scope->deviceFingerprint !== null && $scope->entityType !== null && $scope->entityId !== null) {
83            $val = $this->userPreferences->findValue($scope, $key);
84            if ($val !== null) {
85                return $val;
86            }
87        }
88
89        if ($scope->deviceFingerprint !== null && $scope->moduleName !== null) {
90            $moduleDeviceScope = PreferenceScope::forModule(
91                $scope->userId,
92                $scope->moduleName,
93                $scope->deviceFingerprint
94            );
95            return $this->userPreferences->findValue($moduleDeviceScope, $key);
96        }
97
98        return null;
99    }
100
101    /**
102     * Resolves standard user-level module and global scopes.
103     */
104    private function resolveStandardScopedValue(PreferenceScope $scope, string $key): mixed
105    {
106        if ($scope->moduleName !== null) {
107            $moduleScope = PreferenceScope::forModule($scope->userId, $scope->moduleName);
108            $val = $this->userPreferences->findValue($moduleScope, $key);
109            if ($val !== null) {
110                return $val;
111            }
112        }
113
114        $userGlobalScope = PreferenceScope::forUser($scope->userId);
115        $globalVal = $this->userPreferences->findValue($userGlobalScope, $key);
116        if ($globalVal !== null) {
117            return $globalVal;
118        }
119
120        return $this->resolveModuleFallbackValue($scope, $key);
121    }
122
123    /**
124     * Resolves module fallback from all user module preferences.
125     */
126    private function resolveModuleFallbackValue(PreferenceScope $scope, string $key): mixed
127    {
128        if ($scope->moduleName !== null) {
129            $allModulePrefs = $this->userPreferences->findAllByUser($scope->userId, $scope->moduleName);
130            if (array_key_exists($key, $allModulePrefs) && $allModulePrefs[$key] !== null) {
131                return $allModulePrefs[$key];
132            }
133        }
134
135        return null;
136    }
137
138    /**
139     * Resolves value from system-level settings.
140     *
141     * @param PreferenceScope $scope Scope coordinates.
142     * @param string          $key   Preference key.
143     * @return mixed Found system setting or null.
144     */
145    private function resolveSystemLevel(PreferenceScope $scope, string $key): mixed
146    {
147        if ($this->systemSettings === null) {
148            return null;
149        }
150
151        if ($scope->moduleName !== null) {
152            $moduleKey = "{$scope->moduleName}.{$key}";
153            $modVal = $this->systemSettings->get($moduleKey);
154            if ($modVal !== '') {
155                return $modVal;
156            }
157        }
158
159        $globalVal = $this->systemSettings->get($key);
160        return $globalVal !== '' ? $globalVal : null;
161    }
162}