phpDocumentor/TypeResolver: turning DocBlock type strings into PHP value objects
A PSR-5 based resolver of Class names, Types and Structural Element Names
At a glance
- What is it?
- A PSR-5 library that parses type expressions and structural element names from DocBlocks, expanding partial class names into fully qualified ones when you hand it a Context. Useful inside static analysers and documentation generators, not as a runtime validation layer.
- Who is it for?
- Adopt phpDocumentor/TypeResolver if you are writing a static analyser, a documentation generator, an IDE helper or any tool that reads DocBlocks and needs to turn '@var Foo\Bar' or '@see Classy::otherFunction()' into something structured. Do not adopt it as a runtime type validator: it never touches actual values, and it will happily resolve a type string that no argument could ever satisfy.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The problem: DocBlocks are strings, and your tool wants objects
PHP's own reflection tells you what a function signature declares. It tells you nothing about '@var string[]|null', '@return void', '@var (string|TypeResolver)[]' or '@see Classy::otherFunction()'. Those are text, and every tool that consumes them, from a static analyser to a documentation generator to an IDE plugin, ends up writing its own parser for the same grammar. The PSR-5 specification defines that grammar, including the rules for turning a partial class name into a fully qualified class name, and this package is an implementation of those rules. The README describes it as two resolvers: one that returns value objects for a type expression, and one that returns an FQSEN object for a structural element name. The intended audience is tool authors, not application developers. If you are writing a controller and want to know whether a variable is a string, this is the wrong library. If you are writing something that reads other people's DocBlocks, it is the piece you would otherwise spend a week reimplementing badly.
Two resolvers, one shared Context
The architecture is small and worth understanding before you call anything. TypeResolver::resolve() takes a type expression and returns a value object tree: 'string|integer' yields a Compound holding String_ and Integer, 'string[]' yields an Array_ wrapping String_, and a bare class name yields an Object_ (the README's own example resolves '@var TypeResolver' or '@var \phpDocumentor\Reflection\TypeResolver'). FqsenResolver::resolve() takes a structural element reference such as '\MyNamespace\MyClass::myMethod()' and returns an Fqsen. Both are stateless with respect to your project; the thing that makes partial names resolvable is the Context, a value object carrying a namespace and a map of aliases. The README is explicit that expanding partial names requires this additional Context, and that without it the resolvers cannot know which namespace the expression occurs in or which imports apply. The ContextFactory exists to build one from a Reflector or from namespace information, which is the path most tools take. The data flow is therefore: read a file, extract namespace and use statements, build a Context, feed expressions to the resolver, receive value objects, and let your analyser walk that tree. Nothing in that chain inspects runtime values.
Installing phpDocumentor/TypeResolver and resolving your first type
Installation is a single Composer command, and the README gives it directly:
composer require phpdocumentor/type-resolverAfter that, resolving a compound type is three lines. The resolver is instantiated with no arguments, and resolve() returns a value object rather than a string:
$typeResolver = new \phpDocumentor\Reflection\TypeResolver();
$type = $typeResolver->resolve('string|integer');According to the README you receive a \phpDocumentor\Reflection\Types\Compound with two elements, one String_ and one Integer. The next step is the one most people actually need: expanding a partial class name. That requires a Context holding the namespace and the aliases in play, and the README shows it built by hand:
$context = new \phpDocumentor\Reflection\Types\Context(
'\My\Example',
[ 'Types' => '\phpDocumentor\Reflection\Types']
);With that Context, a '@var Types\Context' inside namespace My\Example resolves to the fully qualified class rather than to a relative name. The repository ships an examples/ directory with six numbered files covering simple types, classes, all elements, and three ways of discovering the Context (class reflection, method reflection, file contents). Reading example 06 before writing your own file scanner is the fastest way to see what ContextFactory expects as input.
Nullable types come back wrapped, and that catches people out
PHP 7.1 introduced '?string', and the README documents the behaviour precisely: the resolver parses the underlying type as if the question mark were not there, then wraps the result in a \phpDocumentor\Reflection\Types\Nullable object, which has a method to fetch the actual type. This is a deliberate design choice rather than a quirk, and it is defensible, but it means your consuming code needs a branch for Nullable that it does not need for String_ or Compound. A tool that walks the returned tree and assumes every leaf is a primitive will silently mishandle nullable annotations. The other boundary to be aware of is scope: the resolver parses and expands names, and that is all. It does not verify that a class exists, does not autoload anything, and does not check a type against a value. A typo in a DocBlock resolves to an Object_ pointing at a class that was never written. That is correct behaviour for a parser, and wrong behaviour for anything you intend to use as a safety net.
How it differs from using PHP's own Reflection
The obvious alternative is to skip DocBlocks entirely and read native type declarations through Reflection, which needs no dependency and no parser. The difference in approach is what each one can see. Native reflection covers parameters, return types, and typed properties as the engine understands them; it cannot see '@var (string|TypeResolver)[]', cannot see a '@see' reference at all, and cannot tell you that a parameter is documented as accepting a union the signature does not express. TypeResolver reads the annotation layer, which is exactly the layer that exists because the signature could not say everything. The trade-off runs the other way too: Reflection is authoritative because the engine enforces it, while a DocBlock is a comment. If your tool only needs what the signature already states, adding this dependency buys you nothing. If your tool exists because DocBlocks carry information the signature does not, you are choosing between this package and writing the PSR-5 grammar yourself, and the grammar is not small.
Version 2.0.0, licensing and what a 2.x upgrade costs
The package is MIT licensed, which places essentially no conditions on how you use it inside a larger application, though the usual caveat applies that this is a description of the licence text and not legal advice. The release history shows a 2.0.0 on 2026-01-06 following a 1.12.0 in November 2025, so the 2.x line is the current one and the README's example URLs still reference the 1.x branch in their badges, which is a sign the documentation has not been fully retargeted. A major version bump on a parser library usually means changed return types or changed constructor signatures, and the value objects it returns are the public surface your code will couple to. The repository does not ship a changelog file at the top level, so the release notes on the tagged versions are the place to look before upgrading. Maintenance cost is low in normal use: the dependency has no service to run, no configuration file, and no state. The cost that does exist is coupling to the value object classes, which is why pinning matters more than tracking the branch.
Where TypeResolver is the wrong tool
Three cases stand out. First, runtime validation: if you want to reject a bad argument, use a validator or the engine's own type declarations, because this library never sees a value. Second, native-only codebases: if your project has full type declarations and no DocBlock annotations worth reading, Reflection is sufficient and a parser is dead weight. Third, code that needs to resolve names against a live autoloader: the resolver expands 'Types\Context' into a fully qualified name using the Context you supply, and it will do so whether or not that class is loadable. There is no check against the class map, so a stale 'use' statement produces a confident, wrong answer. If your tool needs to distinguish a real class from a typo, you have to add that check yourself after resolution. None of this is a defect; it is the boundary of a parser, and knowing where the boundary sits is what keeps you from building on it in the wrong place.
Editorial conclusion
Adopt phpDocumentor/TypeResolver if you are writing a static analyser, a documentation generator, an IDE helper or any tool that reads DocBlocks and needs to turn '@var Foo\Bar' or '@see Classy::otherFunction()' into something structured. Do not adopt it as a runtime type validator: it never touches actual values, and it will happily resolve a type string that no argument could ever satisfy. Do not adopt it either if you only need to read native PHP 7.4+ type declarations, since Reflection covers those without a parser. Before committing, verify two things in your own codebase: that the Context you build carries the namespace and the aliases of the file being analysed (the README shows ContextFactory as the way to derive that), and that you have handled the Nullable wrapper, because a '?string' does not come back as a String_ value object on its own. The last push to the default branch was on 2026-04-02, so the 2.x line is recent enough to build against, but pin a version and read the 2.0.0 release notes before upgrading from 1.x.
Frequently asked questions
How do I install phpDocumentor/TypeResolver?
The README gives a single Composer command, composer require phpdocumentor/type-resolver. There is no separate setup step, configuration file or service to start after that.
How does phpDocumentor/TypeResolver expand a partial class name into a fully qualified one?
It needs a \phpDocumentor\Reflection\Types\Context carrying the namespace and the aliases in play, which you either build by hand or derive with ContextFactory from a Reflector or from file contents. Without that Context the resolver cannot know which namespace the expression occurs in.
How does phpDocumentor/TypeResolver handle nullable types like ?string?
The README states that it resolves the underlying type as if the question mark were absent, then wraps the result in a \phpDocumentor\Reflection\Types\Nullable object, which exposes a method to fetch the actual type. Your walking code therefore needs a branch for Nullable.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/phpdocumentor-typeresolver)