diff --git a/src/Psl/Internal/Loader.php b/src/Psl/Internal/Loader.php index 39e400d1..d8b78fbf 100644 --- a/src/Psl/Internal/Loader.php +++ b/src/Psl/Internal/Loader.php @@ -344,6 +344,7 @@ final class Loader 'Psl\\Type\\scalar' => 'Psl/Type/scalar.php', 'Psl\\Type\\shape' => 'Psl/Type/shape.php', 'Psl\\Type\\uint' => 'Psl/Type/uint.php', + 'Psl\\Type\\class_string' => 'Psl/Type/class_string.php', 'Psl\\Type\\u32' => 'Psl/Type/u32.php', 'Psl\\Type\\u16' => 'Psl/Type/u16.php', 'Psl\\Type\\u8' => 'Psl/Type/u8.php', diff --git a/src/Psl/Type/Internal/ClassStringType.php b/src/Psl/Type/Internal/ClassStringType.php new file mode 100644 index 00000000..91b30321 --- /dev/null +++ b/src/Psl/Type/Internal/ClassStringType.php @@ -0,0 +1,76 @@ +> + * + * @internal + */ +final class ClassStringType extends Type +{ + /** + * @var class-string $classname + */ + private string $classname; + + /** + * @param class-string $classname + */ + public function __construct( + string $classname + ) { + $this->classname = $classname; + } + + /** + * @psalm-assert-if-true class-string $value + */ + public function matches(mixed $value): bool + { + return is_string($value) && is_a($value, $this->classname, true); + } + + /** + * @throws CoercionException + * + * @return class-string + */ + public function coerce(mixed $value): string + { + if (is_string($value) && is_a($value, $this->classname, true)) { + return $value; + } + + throw CoercionException::withValue($value, $this->toString(), $this->getTrace()); + } + + /** + * @throws AssertException + * + * @return class-string + * + * @psalm-assert class-string $value + */ + public function assert(mixed $value): string + { + if (is_string($value) && is_a($value, $this->classname, true)) { + return $value; + } + + throw AssertException::withValue($value, $this->toString(), $this->getTrace()); + } + + public function toString(): string + { + return 'class-string<' . $this->classname . '>'; + } +} diff --git a/src/Psl/Type/class_string.php b/src/Psl/Type/class_string.php new file mode 100644 index 00000000..207c1fc7 --- /dev/null +++ b/src/Psl/Type/class_string.php @@ -0,0 +1,17 @@ + $classname + * + * @return TypeInterface> + */ +function class_string(string $classname): TypeInterface +{ + return new Internal\ClassStringType($classname); +} diff --git a/tests/static-analysis/Type/class_string.php b/tests/static-analysis/Type/class_string.php new file mode 100644 index 00000000..0343a03f --- /dev/null +++ b/tests/static-analysis/Type/class_string.php @@ -0,0 +1,23 @@ + $_foo + */ +function take_collection_classname(string $_foo): void +{ +} + +/** + * @throws Psl\Type\Exception\AssertException + */ +function tests(): void +{ + take_collection_classname(Type\class_string(Psl\Collection\CollectionInterface::class)->assert('foo')); +} diff --git a/tests/unit/Type/ClassStringTypeTest.php b/tests/unit/Type/ClassStringTypeTest.php new file mode 100644 index 00000000..762befbc --- /dev/null +++ b/tests/unit/Type/ClassStringTypeTest.php @@ -0,0 +1,42 @@ +stringable('foo')]; + yield [new class { + }]; + } + + public function getToStringExamples(): iterable + { + yield [Type\class_string(Collection\MapInterface::class), 'class-string']; + yield [Type\class_string(Collection\VectorInterface::class), 'class-string']; + yield [Type\class_string(Collection\Vector::class), 'class-string']; + yield [Type\class_string(Collection\Map::class), 'class-string']; + } +}