|
| 1 | +--- |
| 2 | +title: Custom PHPDoc Types |
| 3 | +--- |
| 4 | + |
| 5 | +PHPStan lets you override how it converts [PHPDoc Type AST coming from its phpdoc-parser library](https://apiref.phpstan.org/1.9.x/PHPStan.PhpDocParser.Ast.Type.TypeNode.html) into [its type system representation](/developing-extensions/type-system). This can be used to introduce custom utility types - a popular feature known [from other languages like TypeScript](https://www.typescriptlang.org/docs/handbook/utility-types.html). |
| 6 | + |
| 7 | +The implementation is all about applying the [core concepts](/developing-extensions/core-concepts) like [the type system](/developing-extensions/type-system) so check out that guide first and then continue here. |
| 8 | + |
| 9 | +The conversion is done by a class called [`TypeNodeResolver`](https://apiref.phpstan.org/1.9.x/PHPStan.PhpDoc.TypeNodeResolver.html). That's why the interface to implement in this extension type is called [`TypeNodeResolverExtension`](https://apiref.phpstan.org/1.9.x/PHPStan.PhpDoc.TypeNodeResolverExtension.html): |
| 10 | + |
| 11 | +```php |
| 12 | +namespace PHPStan\PhpDoc; |
| 13 | + |
| 14 | +use PHPStan\Analyser\NameScope; |
| 15 | +use PHPStan\PhpDocParser\Ast\Type\TypeNode; |
| 16 | +use PHPStan\Type\Type; |
| 17 | + |
| 18 | +interface TypeNodeResolverExtension |
| 19 | +{ |
| 20 | + |
| 21 | + public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type; |
| 22 | + |
| 23 | +} |
| 24 | +``` |
| 25 | + |
| 26 | +The implementation needs to be registered in your [configuration file](/config-reference): |
| 27 | + |
| 28 | +```yaml |
| 29 | +services: |
| 30 | + - |
| 31 | + class: MyApp\PHPStan\MyTypeNodeResolverExtension |
| 32 | + tags: |
| 33 | + - phpstan.phpDoc.typeNodeResolverExtension |
| 34 | +``` |
| 35 | + |
| 36 | +TypeNodeResolverExtension cannot have [`TypeNodeResolver`](https://apiref.phpstan.org/1.9.x/PHPStan.PhpDoc.TypeNodeResolver.html) injected in the constructor due to circular reference issue, but the extensions can implement [`TypeNodeResolverAwareExtension`](https://apiref.phpstan.org/1.9.x/PHPStan.PhpDoc.TypeNodeResolverAwareExtension.html) interface to obtain `TypeNodeResolver` via a setter. |
| 37 | + |
| 38 | +An example |
| 39 | +---------------- |
| 40 | + |
| 41 | +Let's say we want to implement the [`Pick` utility type](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys) from TypeScript. It will allow us to achieve the code in the following example: |
| 42 | + |
| 43 | +```php |
| 44 | +/** |
| 45 | + * @phpstan-type Address array{name: string, surname: string, street: string, city: string, country: Country} |
| 46 | + */ |
| 47 | +class Foo |
| 48 | +{ |
| 49 | + |
| 50 | + /** |
| 51 | + * @param Pick<Address, 'name' | 'surname'> $personalDetails |
| 52 | + */ |
| 53 | + public function doFoo(array $personalDetails): void |
| 54 | + { |
| 55 | + \PHPStan\dumpType($personalDetails); // array{name: string, surname: string} |
| 56 | + } |
| 57 | + |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +This is how we'd be able to achieve that in our own `TypeNodeResolverExtension`: |
| 62 | + |
| 63 | +```php |
| 64 | +namespace MyApp\PHPStan; |
| 65 | + |
| 66 | +use PHPStan\Analyser\NameScope; |
| 67 | +use PHPStan\PhpDoc\TypeNodeResolver; |
| 68 | +use PHPStan\PhpDoc\TypeNodeResolverAwareExtension; |
| 69 | +use PHPStan\PhpDoc\TypeNodeResolverExtension; |
| 70 | +use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; |
| 71 | +use PHPStan\PhpDocParser\Ast\Type\TypeNode; |
| 72 | +use PHPStan\Type\Constant\ConstantArrayTypeBuilder; |
| 73 | +use PHPStan\Type\Type; |
| 74 | +use PHPStan\Type\TypeCombinator; |
| 75 | + |
| 76 | +class MyTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension |
| 77 | +{ |
| 78 | + |
| 79 | + private TypeNodeResolver $typeNodeResolver; |
| 80 | + |
| 81 | + public function setTypeNodeResolver(TypeNodeResolver $typeNodeResolver): void |
| 82 | + { |
| 83 | + $this->typeNodeResolver = $typeNodeResolver; |
| 84 | + } |
| 85 | + |
| 86 | + public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type |
| 87 | + { |
| 88 | + if (!$typeNode instanceof GenericTypeNode) { |
| 89 | + // returning null means this extension is not interested in this node |
| 90 | + return null; |
| 91 | + } |
| 92 | + |
| 93 | + $typeName = $typeNode->type; |
| 94 | + if ($typeName->name !== 'Pick') { |
| 95 | + return null; |
| 96 | + } |
| 97 | + |
| 98 | + $arguments = $typeNode->genericTypes; |
| 99 | + if (count($arguments) !== 2) { |
| 100 | + return null; |
| 101 | + } |
| 102 | + |
| 103 | + $arrayType = $this->typeNodeResolver->resolve($arguments[0], $nameScope); |
| 104 | + $keysType = $this->typeNodeResolver->resolve($arguments[1], $nameScope); |
| 105 | + |
| 106 | + $constantArrays = $arrayType->getConstantArrays(); |
| 107 | + if (count($constantArrays) === 0) { |
| 108 | + return null; |
| 109 | + } |
| 110 | + |
| 111 | + $newTypes = []; |
| 112 | + foreach ($constantArrays as $constantArray) { |
| 113 | + $newTypeBuilder = ConstantArrayTypeBuilder::createEmpty(); |
| 114 | + foreach ($constantArray->getKeyTypes() as $i => $keyType) { |
| 115 | + if (!$keysType->isSuperTypeOf($keyType)->yes()) { |
| 116 | + // eliminate keys that aren't in the Pick type |
| 117 | + continue; |
| 118 | + } |
| 119 | + |
| 120 | + $valueType = $constantArray->getValueTypes()[$i]; |
| 121 | + $newTypeBuilder->setOffsetValueType( |
| 122 | + $keyType, |
| 123 | + $valueType, |
| 124 | + $constantArray->isOptionalKey($i), |
| 125 | + ); |
| 126 | + } |
| 127 | + |
| 128 | + $newTypes[] = $newTypeBuilder->getArray(); |
| 129 | + } |
| 130 | + |
| 131 | + return TypeCombinator::union(...$newTypes); |
| 132 | + } |
| 133 | + |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +One example of TypeNodeResolverExtension usage is in the [phpstan-phpunit](https://github.com/phpstan/phpstan-phpunit) extension. Before [intersection types](https://phpstan.org/blog/union-types-vs-intersection-types) picked up the pace and were largely unknown to PHP community, developers often written `Foo|MockObject` when they actually meant `Foo&MockObject`. So [the extension actually fixed it for them](https://github.com/phpstan/phpstan-phpunit/blob/1.1.x/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php) and made PHPStan interpret `Foo|MockObject` as an intersection type. |
0 commit comments