From 8763a32c56a8f225a263fcd69d29f43e4fde1b81 Mon Sep 17 00:00:00 2001 From: Valentin Udaltsov Date: Wed, 2 Oct 2024 04:09:27 +0300 Subject: [PATCH] Update README.md --- README.md | 319 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 318 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 54e7c9b..f086daf 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,325 @@ [![Code Coverage](https://codecov.io/gh/typhoon-php/type/branch/0.4.x/graph/badge.svg)](https://codecov.io/gh/typhoon-php/type/tree/0.4.x) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Ftyphoon-php%2Ftype%2F0.4.x)](https://dashboard.stryker-mutator.io/reports/github.com/typhoon-php/type/0.4.x) +Typhoon Type is an object abstraction over the modern PHP type system. Use this library to build tools that work with +sophisticated types. + +Here are some examples of potential use-cases: + +```php +use Typhoon\Type\types; + +$data = (new MyAwesomeJsonDecoder())->decode( + json: '[1, 0.5, "213"]', + type: types::list(types::numeric), +); + +var_dump($data); +``` + +``` +array(3) { + [0] => int(1) + [1] => float(0.5) + [2] => string(3) "213" +} +``` + +Or: + +```php +use Typhoon\Type\types; + +final readonly class GetUserResponse +{ + /** + * @param non-empty-string $name + * @param 'user'|'admin' $group + */ + public function __construct( + public Uuid $id, + public string $name, + public string $group, + ) {} +} + +echo (new MyAwesomeOpenApiGenerator())->generateSchema(types::object(GetUserResponse::class)); +``` + +```yaml +GetUserResponse: + type: object + properties: + id: + type: string + format: uuid + example: b609e6a9-bba6-4599-9faa-cc9977353bb4 + name: + type: string + example: Hello world! + group: + type: string + enum: [ user, admin ] +``` + ## Installation -```shell +``` composer require typhoon/type ``` + +## Constructing types + +Typhoon types can be constructed via the `Typhoon\Type\types` static factory. Let's express this monstrous type via +the Typhoon DSL: + +``` +array{ + a: non-empty-string, + b?: int|float, + c: Traversable, + d: callable(PDO::*, TSend#Generator=, scalar...): void, + ... +} +``` + +```php +use Typhoon\Type\types; + +$type = types::unsealedArrayShape([ + 'a' => types::nonEmptyString, + 'b' => types::optional(types::union(types::int, types::float)), + 'c' => types::object(Traversable::class, [types::numericString, types::false]), + 'd' => types::callable( + parameters: [ + types::classConstantMask(PDO::class), + types::param(types::classTemplate(Generator::class, 'TSend'), hasDefault: true), + types::param(types::scalar, variadic: true), + ], + return: types::void, + ), +]); +``` + +As you can see, creating types in Typhoon is a lot of fun, especially if you work in IDE with autocompletion 😉 + +## Design + +Unlike other solutions, Typhoon Type does not expose concrete type classes in its API. Instead, it provides only +a [common `Type` interface](../src/Type/Type.php), a [type factory `types`](../src/Type/types.php), and +a [`TypeVisitor` with destructurization](../src/Type/TypeVisitor.php). +This approach gives several advantages: + +1. The visitor has only a minimal subset of type methods that must be implemented when describing a type algebra. + Complexity of the other types is hidden and can be completely ignored. +2. Memory efficient enums can be used for all atomic types and for aliases of commonly used compound types. +3. Using of downcasting via the `instanceof` operator is automatically discouraged, since all `Type` implementations are + `@internal` ( + see [PHPStan: Why Is instanceof *Type Wrong and Getting Deprecated?](https://phpstan.org/blog/why-is-instanceof-type-wrong-and-getting-deprecated)). + +## Printing types + +To cast any type to string, use the `Typhoon\Type\stringify()` function: + +```php +use Typhoon\Type\types; +use function Typhoon\Type\stringify; + +var_dump( + stringify( + types::Generator( + key: types::nonNegativeInt, + value: types::classTemplate(Foo::class, 'T'), + send: types::scalar, + ), + ), +); // Generator, T#Foo, scalar, mixed> +``` + +### Comparing types + +Typhoon team is currently working on a type comparator. Until it is released, you can +use [DefaultTypeVisitor](../src/Type/DefaultTypeVisitor.php) for simple checks: + +```php +use Typhoon\Type\Type; +use Typhoon\Type\types; +use Typhoon\Type\Visitor\DefaultTypeVisitor; + +/** + * @extends DefaultTypeVisitor + */ +final class BasicIntChecker extends DefaultTypeVisitor +{ + public function int(Type $type, Type $minType, Type $maxType): mixed + { + return true; + } + + public function intValue(Type $type, int $value): mixed + { + return true; + } + + public function intMask(Type $type, Type $ofType): bool + { + return true; + } + + protected function default(Type $type): bool + { + return false; + } +} + +var_dump(types::positiveInt->accept(new BasicIntChecker())); // true +var_dump(types::callableString()->accept(new BasicIntChecker())); // false +``` + +## Compatibility with Psalm and PHPStan + +### Native PHP types + +| PHPStan | Psalm | Typhoon | +|-------------------------|-------------------------|-------------------------------------------------------------------------------------------| +| `null` | `null` | `types::null`, `types::value(null)` | +| `void` | `void` | `types::void` | +| `never` | `never` | `types::never` | +| `true` | `true` | `types::true`, `types::value(true)` | +| `false` | `false` | `types::false`, `types::value(false)` | +| `bool`, `boolean` | `bool` | `types::bool` | +| `int`, `integer` | `int` | `types::int` | +| `float`, `double` | `float` | `types::float` | +| `string` | `string` | `types::string` | +| `resource` | `resource` | `types::resource` | +| `array` | `array` | `types::array` | +| `iterable` | `iterable` | `types::iterable` | +| `object` | `object` | `types::object` | +| `Foo` | `Foo` | `types::object(Foo::class)` | +| `Closure` | `Closure` | `types::Closure` (an alias for `types::object(Closure::class)`) | +| `Generator` | `Generator` | `types::Generator()` (an alias for `types::object(Generator::class)`) | +| `self` | `self` | `types::self()` | +| `parent` | `parent` | `types::parent()` | +| `static` | `static` | `types::static()` | +| `callable` | `callable` | `types::callable` | +| `?string` | `?string` | `types::nullable(types::string)` | +| `int\|string` | `int\|string` | `types::union(types::int, types::string)` | +| `Countable&Traversable` | `Countable&Traversable` | `types::intersection(types::object(Countable::class), types::object(Traversable::class))` | +| `mixed` | `mixed` | `types::mixed` | + +### Numbers + +| PHPStan | Psalm | Typhoon | +|---------------------------|------------------------------|-----------------------------------------------------------------| +| `literal-int` | `literal-int` | `types::literalInt` | +| `123` | `123` | `types::int(123)`, `types::value(123)` | +| `positive-int` | `positive-int` | `types::positiveInt` | +| `negative-int` | `negative-int` | `types::negativeInt` | +| `non-positive-int` | `non-positive-int` | `types::nonPositiveInt` | +| `non-negative-int` | `non-negative-int` | `types::nonNegativeInt` | +| `non-zero-int` | `negative-int\|positive-int` | `types::nonZeroInt` | +| `int<-5, 6>` | `int<-5, 6>` | `types::intRange(-5, 6)` | +| `int` | `int` | `types::intRange(max: 6)` | +| `int<-5, max>` | `int<-5, max>` | `types::intRange(min: -5)` | +| `int-mask<1, 2, 4>` | `int-mask<1, 2, 4>` | `types::intMask(1, 2, 4)` | +| `int-mask-of` | `int-mask-of` | `types::intMaskOf(types::classConstantMask(Foo::class, 'INT_')` | +| ❌ | ❌ | `types::literalFloat` | +| ❌ | ❌ | `types::floatRange(-0.001, 2.344)` | +| `12.5` | `12.5` | `types::float(12.5)`, `types::value(12.5)` | +| `numeric` | `numeric` | `types::numeric` | + +### Strings + +| PHPStan | Psalm | Typhoon | +|-------------------------------------|-------------------------------------|-------------------------------------------------| +| `non-empty-string` | `non-empty-string` | `types::nonEmptyString` | +| `literal-string` | `literal-string` | `types::literalString` | +| `'abc'` | `'abc'` | `types::string('abc')`, `types::value('abc')` | +| `truthy-string`, `non-falsy-string` | `truthy-string`, `non-falsy-string` | `types::truthyString`, `types::nonFalsyString` | +| `numeric-string` | `numeric-string` | `types::numericString` | +| `callable-string` | `callable-string` | `types::callableString()` | +| `class-string` | `class-string` | `types::classString(types::object(Foo::class))` | +| `Foo::class` | `Foo::class` | `types::class(Foo::class)` | +| `class-string` | `class-string` | `types::classString` | +| ❌ | `interface-string` | ❌ | +| ❌ | `trait-string` | ❌ | +| ❌ | `enum-string` | ❌ | +| ❌ | `lowercase-string` | ❌ | + +### Constants + +| PHPStan | Psalm | Typhoon | +|---------------|---------------|-------------------------------------------------------------------------------------------| +| `PHP_INT_MAX` | `PHP_INT_MAX` | `types::constant('PHP_INT_MAX')` | +| `Foo::BAR` | `Foo::BAR` | `types::classConstant(Foo::class, 'BAR')` | +| `Foo::IS_*` | `Foo::IS_*` | `types::classConstant(Foo::class, 'IS_*')`, `types::classConstantMask(Foo::class, 'IS_')` | + +### Arrays and iterables + +| PHPStan | Psalm | Typhoon | +|------------------------------------------------------------------|-------------------------------------------|---------------------------------------------------------------------------------------------------------------------| +| `array-key` | `array-key` | `types::arrayKey` | +| `Foo[]` | `Foo[]` | `types::array(value: types::object(Foo::class))` | +| `list` | `list` | `types::list(types::string)` | +| `non-empty-list` | `non-empty-list` | `types::nonEmptyList(types::string)` | +| `list{int, string}` | `list{int, string}` | `types::listShape([types::int, types::string])` | +| `list{int, 1?: string}` | `list{int, 1?: string}` | `types::listShape([types::int, types::optional(types::string)])` | +| `list{int, ...}` | `list{int, ...}` | `types::unsealedListShape([types::int])` | +| ❌ ([issue](https://github.com/phpstan/phpdoc-parser/issues/245)) | `list{int, ...}` | `types::unsealedListShape([types::int], types::string)` | +| `array` | `array` | `types::array(value: types::string)` | +| `array` | `array` | `types::array(types::int, types::string)` | +| `non-empty-array` | `non-empty-array` | `types::nonEmptyArray(types::arrayKey, types::string)` | +| `array{}` | `array{}` | `types::array()` | +| `array{int, string}` | `array{int, string}` | `types::arrayShape([types::int, types::string])` | +| `array{int, a?: string}` | `array{int, a?: string}` | `types::arrayShape([types::int, 'a' => types::optional(types::string)])` | +| `array{int, ...}` | `array{int, ...}` | `types::unsealedArrayShape([types::int])` | +| ❌ ([issue](https://github.com/phpstan/phpdoc-parser/issues/245)) | `array{float, ...}` | `types::unsealedArrayShape([types::float], types::int, types::string)` | +| `key-of` | `key-of` | `types::keyOf(types::classConstant(Foo::class, 'ARRAY'))` | +| `value-of` | `value-of` | `types::valueOf(types::classConstant(Foo::class, 'ARRAY'))` | +| `TArray[TKey]` | `TArray[TKey]` | `types::offset($arrayType, $keyType)` | +| `iterable` | `iterable` | `types::iterable(types::object, types::string)` | +| `iterable` | `iterable` | `types::iterable(value: types::string)` | +| `Generator` | `Generator` | `types::object(Generator::class, [$key, $value, $send, $return])`, `types::Generator($key, $value, $send, $return)` | +| `callable&array` | `callable-array` | `types::callableArray()` | + +### Objects + +| PHPStan | Psalm | Typhoon | +|-------------------------|-------------------------|-------------------------------------------------------------| +| `Foo` | `Foo` | `types::object(Foo::class, [types::string, types::float])` | +| `self` | `self` | `types::self([types::string, types::float])` | +| `parent` | `parent` | `types::parent([types::string, types::float])` | +| `static` | `static` | `types::static([types::string, types::float])` | +| `object{prop: string}` | `object{prop: string}` | `types::object(['prop' => types::string])` | +| `object{prop?: string}` | `object{prop?: string}` | `types::object(['prop' => types::optional(types::string)])` | + +### Callables + +| PHPStan | Psalm | Typhoon | +|------------------------------|------------------------------|---------------------------------------------------------------------| +| `callable-string` | `callable-string` | `types::callableString()` | +| `callable&array` | `callable-array` | `types::callableArray()` | +| `callable(string): void` | `callable(string): void` | `types::callable([types::string], types::void)` | +| `callable(string=): mixed` | `callable(string=): mixed` | `types::callable([types::param(types::string, hasDefault: true)])` | +| `callable(...string): mixed` | `callable(...string): mixed` | `types::callable([types::param(types::string, variadic: true)])` | +| `callable(&string): mixed` | `callable(&string): mixed` | `types::callable([types::param(types::string, byReference: true)])` | +| `Closure(string): void` | `Closure(string): void` | `types::Closure([types::string], types::void)` | +| `Closure(string=): mixed` | `Closure(string=): mixed` | `types::Closure([types::param(types::string, hasDefault: true)])` | +| `Closure(...string): mixed` | `Closure(...string): mixed` | `types::Closure([types::param(types::string, variadic: true)])` | +| `Closure(&string): mixed` | `Closure(&string): mixed` | `types::Closure([types::param(types::string, byReference: true)])` | +| `pure-callable` | `pure-callable` | ❌ | + +### Other + +| PHPStan | Psalm | Typhoon | +|-------------------------------------|-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| `scalar` | `scalar` | `types::scalar` | +| Template `T` | Template `T` | `types::functionTemplate('foo', 'T')`, `types::classTemplate(Foo::class, 'T')`, `types::methodTemplate(Foo::class, 'bar', 'T')` | +| Alias `X` | Alias `X` | `types::classAlias(Foo::class, 'X')` | +| `(T is string ? true : false)` | `(T is string ? true : false)` | `types::conditional($type, types::string, types::true, types::false)` | +| `($return is true ? string : void)` | `($return is true ? string : void)` | `types::conditional(types::functionArg('var_export', 'return'), types::true, types::string, types::void)` | +| `!null` (only in assertions) | `!null` (only in assertions) | `types::not(types::null)` | +| ❌ | `properties-of` | ❌ | +| ❌ | `class-string-map` | ❌ | +| `open-resource` | `open-resource` | ❌ | +| `closed-resource` | `closed-resource` | ❌ |