uuf6429 / phpstan-phpdoc-type-resolver
Resolve (fully qualify) types from PHPStan's PHPDoc parser
Package info
github.com/uuf6429/phpstan-phpdoc-type-resolver
pkg:composer/uuf6429/phpstan-phpdoc-type-resolver
Requires
- php: ^8
- phpstan/phpdoc-parser: ^2
Requires (Dev)
- ergebnis/composer-normalize: ^2.7
- friendsofphp/php-cs-fixer: ^3.53
- phpstan/phpstan: ^2.1
- phpstan/phpstan-phpunit: ^2.0
- phpstan/phpstan-strict-rules: ^2.0
- phpunit/phpunit: ^9.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Resolve (fully qualify) types from PHPStan's PHPDoc parser.
ðū Installation
Install via Composer:
composer require uuf6429/phpstan-phpdoc-type-resolver
Consider using --dev if you intend to use this library during development only.
ðĪ Why?
Because phpstan/phpdoc-parser doesn't resolve types (it's not its responsibility) and phpdocument/type-resolver
currently has some major limitations.
ð Usage
In principle, the resolver needs two things:
- The PHPStan-PHPDoc type (an instance of
TypeNode). - 'Scope' information of where that type occurred.
There are two ways to retrieve that information, as shown below.
Important: The resolver will always convert some specific PHPStan types into something else as follows:
- *
ThisTypeNodeis converted intoIdentifierTypeNodefor the current class. - *
GenericTypeNodeto eitherConcreteGenericTypeNodeorTemplateGenericTypeNodebased on if the received instance contains unresolved generic/template types. - PHPStan locally defined or imported types, a
TypeDefTypeNodewill be provided (instead of anIdentifierTypeNodewith just the type name).
(*) conversion is mandatory, failures will trigger some sort of exception (meaning: the original type should never be returned).
ð Via Reflection
Here's how we can resolve the Greeter::greet() method's return type:
// Reflect our class method $reflector = new \ReflectionMethod(\uuf6429\PHPStanPHPDocTypeResolverTests\Fixtures\Greeter::class, 'greet'); // Use the provided factory to easily parse the PHPDoc, which additionally automatically resolves the types $docBlock = \uuf6429\PHPStanPHPDocTypeResolver\PhpDoc\Factory::createInstance() ->createFromReflector($reflector); // And finally, retrieve the resolved type of the param tag $paramTag = $docBlock->getTags('@param')[0]; assert((string)$paramTag->type === '(object{name: string} | uuf6429\PHPStanPHPDocTypeResolverTests\Fixtures\Person)');
ð Without Factory/DocBlock Wrapper
Here's the longer way to resolve the return type of the Greeter::greet() method:
// Reflect our class method $reflector = new \ReflectionMethod(\uuf6429\PHPStanPHPDocTypeResolverTests\Fixtures\Greeter::class, 'greet'); // Use the scope resolver to get information about that method $phpDocResolverFactory = new \uuf6429\PHPStanPHPDocTypeResolver\PhpDoc\Factory(); $genericsExtractor = new \uuf6429\PHPStanPHPDocTypeResolver\PhpDoc\Generics\Extractor($phpDocResolverFactory); $scopeResolver = new \uuf6429\PHPStanPHPDocTypeResolver\PhpDoc\ReflectorScopeResolver($genericsExtractor); $scope = $scopeResolver->resolve($reflector); // Parse the PHPDoc block with PHPStan PHPDoc parser $parserConfig = new \PHPStan\PhpDocParser\ParserConfig([]); $lexer = new \PHPStan\PhpDocParser\Lexer\Lexer($parserConfig); $constExprParser = new \PHPStan\PhpDocParser\Parser\ConstExprParser($parserConfig); $typeParser = new \PHPStan\PhpDocParser\Parser\TypeParser($parserConfig, $constExprParser); $parser = new \PHPStan\PhpDocParser\Parser\PhpDocParser($parserConfig, $typeParser, $constExprParser); $docBlock = $parser->parse( new \PHPStan\PhpDocParser\Parser\TokenIterator( $lexer->tokenize($scope->comment) // ð note that the scope resolver also retrieves the PHPDoc block for us ) ); // Finally, we initialize the type resolver and resolve the param type of the first param $typeResolver = new \uuf6429\PHPStanPHPDocTypeResolver\TypeResolver( $genericsExtractor, new \uuf6429\PHPStanPHPDocTypeResolver\PhpImports\Resolver(), ); $paramType = $typeResolver->resolve($scope, $docBlock->getParamTagValues()[0]->type); assert((string)$paramType === '(object{name: string} | uuf6429\PHPStanPHPDocTypeResolverTests\Fixtures\Person)');
ðĪŠ Via Source Strings
It's also possible to resolve the type without actually loading the PHP source code (which is a requirement for reflection). However, this will take more work â the main difference is that you will need to set up the scope yourself.
Let's assume we want to resolve a type in a PHP source code string:
$source = <<<'PHP' <?php namespace My\Project\Services; use uuf6429\PHPStanPHPDocTypeResolverTests\Fixtures\Person as PersonEntity; class Greeter { /** * @param PersonEntity|object{name: string} $person */ public function greet($person): void { echo "Hello, {$person->name}!"; } } PHP; // Construct the scope manually - automating this will take some work $scope = new \uuf6429\PHPStanPHPDocTypeResolver\PhpDoc\Scope( // In-memory file; you could also use php memory streams etc file: 'data:application/x-httpd-php;base64,' . base64_encode($source), // Approximate line where the type occurred line: 9, // The class within which the type occurred class: 'My\Project\Services\Greeter', // The actualy PHPDoc block containing the type we're interested in comment: "/**\n * @param PersonEntity|object{name: string} \$person\n */", genericsResolver: new \uuf6429\PHPStanPHPDocTypeResolver\PhpDoc\Generics\GenericTypeMap(), ); // The factory can also be used with a custom scope $docBlock = \uuf6429\PHPStanPHPDocTypeResolver\PhpDoc\Factory::createInstance() ->createFromScope($scope); // And as before, retrieve the resolved type of the param tag $paramTag = $docBlock->getTags('@param')[0]; assert((string)$paramTag->type === '(uuf6429\PHPStanPHPDocTypeResolverTests\Fixtures\Person | object{name: string})');