$evidenceArchetypes */ public function __construct( public string $controlKey, public string $name, public string $domainKey, public string $subdomainKey, public string $controlClass, public string $summary, public string $operatorDescription, public DetectabilityClass $detectabilityClass, public EvaluationStrategy $evaluationStrategy, public array $evidenceArchetypes, public ArtifactSuitability $artifactSuitability, public string $historicalStatus = 'active', ) { foreach ([ 'control key' => $this->controlKey, 'name' => $this->name, 'domain key' => $this->domainKey, 'subdomain key' => $this->subdomainKey, 'control class' => $this->controlClass, 'summary' => $this->summary, 'operator description' => $this->operatorDescription, 'historical status' => $this->historicalStatus, ] as $label => $value) { if (trim($value) === '') { throw new InvalidArgumentException(sprintf('Canonical control definitions require a non-empty %s.', $label)); } } if ($this->controlKey !== mb_strtolower($this->controlKey) || preg_match('/^[a-z][a-z0-9_]*$/', $this->controlKey) !== 1) { throw new InvalidArgumentException(sprintf('Canonical control key [%s] must be a lowercase provider-neutral slug.', $this->controlKey)); } if (! in_array($this->historicalStatus, ['active', 'retired'], true)) { throw new InvalidArgumentException(sprintf('Canonical control [%s] has an unsupported historical status.', $this->controlKey)); } if ($this->evidenceArchetypes === []) { throw new InvalidArgumentException(sprintf('Canonical control [%s] must declare at least one evidence archetype.', $this->controlKey)); } } /** * @param array $data */ public static function fromArray(array $data): self { return new self( controlKey: (string) ($data['control_key'] ?? ''), name: (string) ($data['name'] ?? ''), domainKey: (string) ($data['domain_key'] ?? ''), subdomainKey: (string) ($data['subdomain_key'] ?? ''), controlClass: (string) ($data['control_class'] ?? ''), summary: (string) ($data['summary'] ?? ''), operatorDescription: (string) ($data['operator_description'] ?? ''), detectabilityClass: DetectabilityClass::from((string) ($data['detectability_class'] ?? '')), evaluationStrategy: EvaluationStrategy::from((string) ($data['evaluation_strategy'] ?? '')), evidenceArchetypes: self::evidenceArchetypes($data['evidence_archetypes'] ?? []), artifactSuitability: ArtifactSuitability::fromArray(is_array($data['artifact_suitability'] ?? null) ? $data['artifact_suitability'] : []), historicalStatus: (string) ($data['historical_status'] ?? 'active'), ); } /** * @return array{ * control_key: string, * name: string, * domain_key: string, * subdomain_key: string, * control_class: string, * summary: string, * operator_description: string, * detectability_class: string, * evaluation_strategy: string, * evidence_archetypes: list, * artifact_suitability: array{baseline: bool, drift: bool, finding: bool, exception: bool, evidence: bool, review: bool, report: bool}, * historical_status: string * } */ public function toArray(): array { return [ 'control_key' => $this->controlKey, 'name' => $this->name, 'domain_key' => $this->domainKey, 'subdomain_key' => $this->subdomainKey, 'control_class' => $this->controlClass, 'summary' => $this->summary, 'operator_description' => $this->operatorDescription, 'detectability_class' => $this->detectabilityClass->value, 'evaluation_strategy' => $this->evaluationStrategy->value, 'evidence_archetypes' => array_map( static fn (EvidenceArchetype $archetype): string => $archetype->value, $this->evidenceArchetypes, ), 'artifact_suitability' => $this->artifactSuitability->toArray(), 'historical_status' => $this->historicalStatus, ]; } public function isRetired(): bool { return $this->historicalStatus === 'retired'; } /** * @param iterable $values * @return list */ private static function evidenceArchetypes(iterable $values): array { return collect($values) ->filter(static fn (mixed $value): bool => is_string($value) && trim($value) !== '') ->map(static fn (string $value): EvidenceArchetype => EvidenceArchetype::from(trim($value))) ->values() ->all(); } }