$implementationPaths * @param list $neutralTerms * @param list $retainedProviderSemantics */ public function __construct( public readonly string $key, public readonly ProviderBoundaryOwner $owner, public readonly string $description, public readonly array $implementationPaths, public readonly array $neutralTerms, public readonly array $retainedProviderSemantics, public readonly string $followUpAction, ) { $this->validate(); } /** * @param array{ * owner?: string, * description?: string, * implementation_paths?: list, * neutral_terms?: list, * retained_provider_semantics?: list, * follow_up_action?: string * } $attributes */ public static function fromConfig(string $key, array $attributes): self { $owner = ProviderBoundaryOwner::tryFrom((string) ($attributes['owner'] ?? '')); if (! $owner instanceof ProviderBoundaryOwner) { throw new InvalidArgumentException("Provider boundary seam [{$key}] has an invalid owner."); } return new self( key: $key, owner: $owner, description: (string) ($attributes['description'] ?? ''), implementationPaths: self::stringList($attributes['implementation_paths'] ?? []), neutralTerms: self::stringList($attributes['neutral_terms'] ?? []), retainedProviderSemantics: self::stringList($attributes['retained_provider_semantics'] ?? []), followUpAction: (string) ($attributes['follow_up_action'] ?? self::FOLLOW_UP_NONE), ); } public function isProviderOwned(): bool { return $this->owner === ProviderBoundaryOwner::ProviderOwned; } public function isPlatformCore(): bool { return $this->owner === ProviderBoundaryOwner::PlatformCore; } public function retainsProviderSemantics(): bool { return $this->retainedProviderSemantics !== []; } public function documentsProviderSemantic(string $term): bool { return in_array($term, $this->retainedProviderSemantics, true); } public function coversPath(string $path): bool { $normalizedPath = $this->normalizePath($path); foreach ($this->implementationPaths as $implementationPath) { if ($normalizedPath === $this->normalizePath($implementationPath)) { return true; } } return false; } /** * @param array $values * @return list */ private static function stringList(array $values): array { return array_values(array_filter( array_map(static fn (mixed $value): string => trim((string) $value), $values), static fn (string $value): bool => $value !== '', )); } private function validate(): void { if (trim($this->key) === '') { throw new InvalidArgumentException('Provider boundary seam key cannot be empty.'); } if (trim($this->description) === '') { throw new InvalidArgumentException("Provider boundary seam [{$this->key}] must include a description."); } if ($this->implementationPaths === []) { throw new InvalidArgumentException("Provider boundary seam [{$this->key}] must include implementation paths."); } if ($this->isPlatformCore() && $this->neutralTerms === []) { throw new InvalidArgumentException("Platform-core provider boundary seam [{$this->key}] must include neutral terms."); } if ($this->retainsProviderSemantics() && $this->followUpAction === self::FOLLOW_UP_NONE) { throw new InvalidArgumentException("Provider boundary seam [{$this->key}] retains provider semantics without a follow-up action."); } if (! in_array($this->followUpAction, $this->validFollowUpActions(), true)) { throw new InvalidArgumentException("Provider boundary seam [{$this->key}] has an invalid follow-up action."); } } /** * @return list */ private function validFollowUpActions(): array { return [ self::FOLLOW_UP_NONE, self::FOLLOW_UP_DOCUMENT_IN_FEATURE, self::FOLLOW_UP_SPEC, ]; } private function normalizePath(string $path): string { return trim(str_replace('\\', '/', $path), '/'); } }