*/ private array $subTypes = [], ) {} /** * @param list $subTypes */ public static function from(array $subTypes): self { $invalidSubTypes = array_filter( $subTypes, static fn (Type $type) => ! $type instanceof ObjectType && ! $type instanceof VacantType ); if ($invalidSubTypes !== []) { throw new InvalidClassStringElements($subTypes, $invalidSubTypes); } /** @var non-empty-list $subTypes */ return new self($subTypes); } public function accepts(mixed $value): bool { if (! is_string($value)) { return false; } if ($this->subTypes === []) { return Reflection::classOrInterfaceExists($value); } foreach ($this->subTypes as $type) { if ($type instanceof ObjectType && is_a($value, $type->className(), true)) { return true; } } return false; } public function compiledAccept(ComplianceNode $node): ComplianceNode { $condition = Node::functionCall('is_string', [$node]); if ($this->subTypes === []) { return $condition->and(Node::functionCall(Reflection::class . '::classOrInterfaceExists', [$node])); } $conditions = []; foreach ($this->subTypes as $type) { if ($type instanceof ObjectType) { $conditions[] = Node::functionCall('is_a', [ $node, Node::value($type->className()), Node::value(true), ]); } } return $condition->and(Node::logicalOr(...$conditions)->wrap()); } public function matches(Type $other): bool { if ($other instanceof NativeStringType || $other instanceof NonEmptyStringType || $other instanceof ScalarConcreteType || $other instanceof MixedType ) { return true; } if ($other instanceof UnionType) { return $other->isMatchedBy($this); } if ($other instanceof ArrayKeyType) { return $other->isMatchedBy($this); } if (! $other instanceof self) { return false; } foreach ($this->subTypes as $subType) { foreach ($other->subTypes as $otherSubType) { if (! $subType->matches($otherSubType)) { return false; } } } return true; } public function inferGenericsFrom(Type $other, Generics $generics): Generics { if (! $other instanceof self) { return $generics; } $selfTypes = UnionType::from(...$this->subTypes); $otherTypes = UnionType::from(...$other->subTypes); return $selfTypes->inferGenericsFrom($otherTypes, $generics); } public function canCast(mixed $value): bool { return (is_string($value) || $value instanceof Stringable) && $this->accepts((string)$value); } public function cast(mixed $value): string { assert($this->canCast($value)); return (string)$value; // @phpstan-ignore-line } public function errorMessage(): ErrorMessage { if ($this->subTypes === []) { return MessageBuilder::newError('Value {source_value} is not a valid class string.') ->withCode('invalid_class_string') ->build(); } return MessageBuilder::newError('Value {source_value} is not a valid class string of `{expected_class_type}`.') ->withCode('invalid_class_string') ->withParameter('expected_class_type', $this->subTypesSignature()) ->build(); } /** * @return list */ public function subTypes(): array { return $this->subTypes; } public function traverse(): array { return $this->subTypes; } public function replace(callable $callback): Type { return self::from(array_map($callback, $this->subTypes)); } public function nativeType(): NativeStringType { return NativeStringType::get(); } public function toString(): string { if ($this->subTypes === []) { return 'class-string'; } return 'class-string<' . $this->subTypesSignature() . '>'; } private function subTypesSignature(): string { return implode('|', array_map( static fn (Type $type) => $type->toString(), $this->subTypes, )); } }