TenantAtlas/app/Services/Settings/SettingsResolver.php
2026-02-25 02:45:20 +01:00

159 lines
5.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Settings;
use App\Models\Tenant;
use App\Models\TenantSetting;
use App\Models\Workspace;
use App\Models\WorkspaceSetting;
use App\Support\Settings\SettingDefinition;
use App\Support\Settings\SettingsRegistry;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class SettingsResolver
{
/**
* @var array<string, array{domain: string, key: string, value: mixed, source: 'system_default'|'workspace_override'|'tenant_override', system_default: mixed, workspace_value: mixed, tenant_value: mixed}>
*/
private array $resolved = [];
public function __construct(private SettingsRegistry $registry) {}
/**
* @return array{domain: string, key: string, value: mixed, source: 'system_default'|'workspace_override'|'tenant_override', system_default: mixed, workspace_value: mixed, tenant_value: mixed}
*/
public function resolveDetailed(Workspace $workspace, string $domain, string $key, ?Tenant $tenant = null): array
{
if ($tenant instanceof Tenant) {
$this->assertTenantBelongsToWorkspace($workspace, $tenant);
}
$cacheKey = $this->cacheKey($workspace, $domain, $key, $tenant);
if (isset($this->resolved[$cacheKey])) {
return $this->resolved[$cacheKey];
}
$definition = $this->registry->require($domain, $key);
$workspaceValue = $this->workspaceOverrideValue($workspace, $domain, $key);
$tenantValue = $tenant instanceof Tenant
? $this->tenantOverrideValue($workspace, $tenant, $domain, $key)
: null;
$source = 'system_default';
$rawValue = $definition->systemDefault;
if ($workspaceValue !== null) {
$source = 'workspace_override';
$rawValue = $workspaceValue;
}
if ($tenantValue !== null) {
$source = 'tenant_override';
$rawValue = $tenantValue;
}
$effectiveValue = $this->mergeWithDefault($definition, $rawValue);
return $this->resolved[$cacheKey] = [
'domain' => $domain,
'key' => $key,
'value' => $effectiveValue,
'source' => $source,
'system_default' => $definition->systemDefault,
'workspace_value' => $workspaceValue,
'tenant_value' => $tenantValue,
];
}
public function resolveValue(Workspace $workspace, string $domain, string $key, ?Tenant $tenant = null): mixed
{
return $this->resolveDetailed($workspace, $domain, $key, $tenant)['value'];
}
public function clearCache(): void
{
$this->resolved = [];
}
/**
* For JSON settings that store partial overrides (e.g. SLA days with only
* some severities set), merge the stored partial with the system default
* so consumers always receive a complete value.
*/
private function mergeWithDefault(SettingDefinition $definition, mixed $value): mixed
{
if ($definition->type !== 'json') {
return $value;
}
if (! is_array($value) || ! is_array($definition->systemDefault)) {
return $value;
}
return array_replace($definition->systemDefault, $value);
}
private function workspaceOverrideValue(Workspace $workspace, string $domain, string $key): mixed
{
$setting = WorkspaceSetting::query()
->where('workspace_id', (int) $workspace->getKey())
->where('domain', $domain)
->where('key', $key)
->first(['value']);
if (! $setting instanceof WorkspaceSetting) {
return null;
}
return $this->decodeStoredValue($setting->getAttribute('value'));
}
private function tenantOverrideValue(Workspace $workspace, Tenant $tenant, string $domain, string $key): mixed
{
$setting = TenantSetting::query()
->where('workspace_id', (int) $workspace->getKey())
->where('tenant_id', (int) $tenant->getKey())
->where('domain', $domain)
->where('key', $key)
->first(['value']);
if (! $setting instanceof TenantSetting) {
return null;
}
return $this->decodeStoredValue($setting->getAttribute('value'));
}
private function decodeStoredValue(mixed $value): mixed
{
if (! is_string($value)) {
return $value;
}
$decoded = json_decode($value, true);
return json_last_error() === JSON_ERROR_NONE ? $decoded : $value;
}
private function assertTenantBelongsToWorkspace(Workspace $workspace, Tenant $tenant): void
{
if ((int) $tenant->workspace_id !== (int) $workspace->getKey()) {
throw new NotFoundHttpException('Tenant is outside the selected workspace scope.');
}
}
private function cacheKey(Workspace $workspace, string $domain, string $key, ?Tenant $tenant): string
{
return implode(':', [
(string) $workspace->getKey(),
(string) ($tenant?->getKey() ?? 0),
$domain,
$key,
]);
}
}