*/ private array $definitions; public function __construct() { $this->definitions = []; $this->register(new SettingDefinition( domain: 'backup', key: 'retention_keep_last_default', type: 'int', systemDefault: 30, rules: ['required', 'integer', 'min:1', 'max:365'], normalizer: static fn (mixed $value): int => (int) $value, )); $this->register(new SettingDefinition( domain: 'backup', key: 'retention_min_floor', type: 'int', systemDefault: 1, rules: ['required', 'integer', 'min:1', 'max:365'], normalizer: static fn (mixed $value): int => (int) $value, )); $this->register(new SettingDefinition( domain: 'drift', key: 'severity_mapping', type: 'json', systemDefault: [], rules: [ 'required', 'array', static function (string $attribute, mixed $value, \Closure $fail): void { if (! is_array($value)) { $fail('The severity mapping must be a JSON object.'); return; } foreach ($value as $findingType => $severity) { if (! is_string($findingType) || trim($findingType) === '') { $fail('Each severity mapping key must be a non-empty string.'); return; } if (! is_string($severity)) { $fail(sprintf('Severity for "%s" must be a string.', $findingType)); return; } $normalizedSeverity = strtolower($severity); if (! in_array($normalizedSeverity, self::supportedFindingSeverities(), true)) { $fail(sprintf( 'Severity for "%s" must be one of: %s.', $findingType, implode(', ', self::supportedFindingSeverities()), )); return; } } }, ], normalizer: static fn (mixed $value): array => self::normalizeSeverityMapping($value), )); $this->register(new SettingDefinition( domain: 'operations', key: 'operation_run_retention_days', type: 'int', systemDefault: 90, rules: ['required', 'integer', 'min:7', 'max:3650'], normalizer: static fn (mixed $value): int => (int) $value, )); $this->register(new SettingDefinition( domain: 'operations', key: 'stuck_run_threshold_minutes', type: 'int', systemDefault: 0, rules: ['required', 'integer', 'min:0', 'max:10080'], normalizer: static fn (mixed $value): int => (int) $value, )); } /** * @return array */ public function all(): array { return $this->definitions; } public function find(string $domain, string $key): ?SettingDefinition { return $this->definitions[$this->cacheKey($domain, $key)] ?? null; } public function require(string $domain, string $key): SettingDefinition { $definition = $this->find($domain, $key); if ($definition instanceof SettingDefinition) { return $definition; } throw new \InvalidArgumentException(sprintf('Unknown setting key: %s.%s', $domain, $key)); } private function register(SettingDefinition $definition): void { $this->definitions[$this->cacheKey($definition->domain, $definition->key)] = $definition; } private function cacheKey(string $domain, string $key): string { return $domain.'.'.$key; } /** * @return array */ private static function supportedFindingSeverities(): array { return [ Finding::SEVERITY_LOW, Finding::SEVERITY_MEDIUM, Finding::SEVERITY_HIGH, Finding::SEVERITY_CRITICAL, ]; } /** * @return array */ private static function normalizeSeverityMapping(mixed $value): array { if (! is_array($value)) { return []; } $normalized = []; foreach ($value as $findingType => $severity) { if (! is_string($findingType) || trim($findingType) === '' || ! is_string($severity)) { continue; } $normalized[$findingType] = strtolower($severity); } ksort($normalized); return $normalized; } }