$policyTypes */ public function __construct( public readonly array $policyTypes = [], ) {} /** * Create from the scope_jsonb column value. * * @param array|null $scopeJsonb */ public static function fromJsonb(?array $scopeJsonb): self { if ($scopeJsonb === null) { return new self; } $policyTypes = $scopeJsonb['policy_types'] ?? []; return new self( policyTypes: is_array($policyTypes) ? array_values(array_filter($policyTypes, 'is_string')) : [], ); } /** * Normalize the effective scope by intersecting profile scope with an optional override. * * Override can only narrow the profile scope (subset enforcement). * If the profile scope is empty (all types), the override becomes the effective scope. * If the override is empty or null, the profile scope is used as-is. */ public static function effective(self $profileScope, ?self $overrideScope): self { if ($overrideScope === null || $overrideScope->isEmpty()) { return $profileScope; } if ($profileScope->isEmpty()) { return $overrideScope; } $intersected = array_values(array_intersect($profileScope->policyTypes, $overrideScope->policyTypes)); return new self(policyTypes: $intersected); } /** * An empty scope means "all types". */ public function isEmpty(): bool { return $this->policyTypes === []; } /** * Check if a policy type is included in this scope. */ public function includes(string $policyType): bool { if ($this->isEmpty()) { return true; } return in_array($policyType, $this->policyTypes, true); } /** * Validate that override is a subset of the profile scope. */ public static function isValidOverride(self $profileScope, self $overrideScope): bool { if ($overrideScope->isEmpty()) { return true; } if ($profileScope->isEmpty()) { return true; } foreach ($overrideScope->policyTypes as $type) { if (! in_array($type, $profileScope->policyTypes, true)) { return false; } } return true; } /** * @return array */ public function toJsonb(): array { return [ 'policy_types' => $this->policyTypes, ]; } }