Menu

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.

Edit this page on GitHub

© 2016–2024 Ondřej Mirtes