Custom PHPDoc Types
PHPStan lets you override how it converts PHPDoc Type AST coming from its phpdoc-parser library into its type system representation. This can be used to introduce custom utility types - a popular feature known from other languages like TypeScript.
The implementation is all about applying the core concepts like the type system so check out that guide first and then continue here.
The conversion is done by a class called TypeNodeResolver
. That’s why the interface to implement in this extension type is called TypeNodeResolverExtension
:
namespace PHPStan\PhpDoc;
use PHPStan\Analyser\NameScope;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Type;
interface TypeNodeResolverExtension
{
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type;
}
The implementation needs to be registered in your configuration file:
services:
-
class: MyApp\PHPStan\MyTypeNodeResolverExtension
tags:
- phpstan.phpDoc.typeNodeResolverExtension
TypeNodeResolverExtension cannot have TypeNodeResolver
injected in the constructor due to circular reference issue, but the extensions can implement TypeNodeResolverAwareExtension
interface to obtain TypeNodeResolver
via a setter.
An example #
Let’s say we want to implement the Pick
utility type from TypeScript. It will allow us to achieve the code in the following example:
/**
* @phpstan-type Address array{name: string, surname: string, street: string, city: string, country: Country}
*/
class Foo
{
/**
* @param Pick<Address, 'name' | 'surname'> $personalDetails
*/
public function doFoo(array $personalDetails): void
{
\PHPStan\dumpType($personalDetails); // array{name: string, surname: string}
}
}
This is how we’d be able to achieve that in our own TypeNodeResolverExtension
:
namespace MyApp\PHPStan;
use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\TypeNodeResolver;
use PHPStan\PhpDoc\TypeNodeResolverAwareExtension;
use PHPStan\PhpDoc\TypeNodeResolverExtension;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
class MyTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension
{
private TypeNodeResolver $typeNodeResolver;
public function setTypeNodeResolver(TypeNodeResolver $typeNodeResolver): void
{
$this->typeNodeResolver = $typeNodeResolver;
}
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
{
if (!$typeNode instanceof GenericTypeNode) {
// returning null means this extension is not interested in this node
return null;
}
$typeName = $typeNode->type;
if ($typeName->name !== 'Pick') {
return null;
}
$arguments = $typeNode->genericTypes;
if (count($arguments) !== 2) {
return null;
}
$arrayType = $this->typeNodeResolver->resolve($arguments[0], $nameScope);
$keysType = $this->typeNodeResolver->resolve($arguments[1], $nameScope);
$constantArrays = $arrayType->getConstantArrays();
if (count($constantArrays) === 0) {
return null;
}
$newTypes = [];
foreach ($constantArrays as $constantArray) {
$newTypeBuilder = ConstantArrayTypeBuilder::createEmpty();
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
if (!$keysType->isSuperTypeOf($keyType)->yes()) {
// eliminate keys that aren't in the Pick type
continue;
}
$valueType = $constantArray->getValueTypes()[$i];
$newTypeBuilder->setOffsetValueType(
$keyType,
$valueType,
$constantArray->isOptionalKey($i),
);
}
$newTypes[] = $newTypeBuilder->getArray();
}
return TypeCombinator::union(...$newTypes);
}
}
One example of TypeNodeResolverExtension usage is in the phpstan-phpunit extension. Before 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 and made PHPStan interpret Foo|MockObject
as an intersection type.