php-di/phpdoc-reader
PhpDocReader parses @var and @param docblock types to resolve class names, supporting fully qualified, imported, relative, and aliased names with the same rules as PHP. Primitive types are ignored; only valid class names are returned. Used by PHP-DI.
Install with Composer (no framework integration required, zero runtime dependencies):
composer require php-di/phpdoc-reader
The entry point is a single class, PhpDocReader\PhpDocReader. It takes a ReflectionProperty or ReflectionParameter and returns the resolved fully-qualified class name as a string, or null when the annotation is absent or is a primitive type.
use PhpDocReader\PhpDocReader;
$reader = new PhpDocReader();
// @var on a property
$property = new ReflectionProperty(Cache::class, 'backend');
$type = $reader->getPropertyClass($property); // "My\Cache\Backend" or null
// @param on a constructor/method parameter
$param = new ReflectionParameter([Cache::class, '__construct'], 'backend');
$type = $reader->getParameterClass($param); // "My\Cache\Backend" or null
Where to look first:
PhpDocReader::getPropertyClass() and getParameterClass() in src/PhpDocReader.php — read the regex/resolution logic once so you know exactly what it does and does not match.tests/ directory — it is the best specification of import-resolution edge cases (aliases, relative names, fully-qualified names).The canonical first use case is container autowiring: you have a constructor parameter with no native type hint, but a docblock says @param Backend $backend. Call getParameterClass() to recover My\Cache\Backend, then recursively resolve that class from the container.
Wrap it behind your own type resolver. Do not sprinkle PhpDocReader calls through your codebase. Define an interface like TypeResolver::resolveParameter(ReflectionParameter): ?string and implement it with this package. That keeps the 2020-era dependency swappable and lets you layer native reflection types on top.
Native types first, phpdoc second. The reader knows nothing about PHP 7.4+ typed properties or PHP 8 union types. Your resolver should check $parameter->getType() / $property->getType() first and only fall back to phpdoc when there is no native type. This is exactly the strategy PHP-DI itself uses.
Cache aggressively. Reflection plus docblock parsing is not free, and you will call this once per constructor parameter on every container build. Memoize by a stable key such as $declaringClass . '::' . $memberName:
final class CachedTypeResolver implements TypeResolver
{
private array $cache = [];
public function __construct(private PhpDocReader $reader) {}
public function resolveParameter(ReflectionParameter $p): ?string
{
$key = $p->getDeclaringClass()->getName() . '::' . $p->getName();
return $this->cache[$key] ??= $this->reader->getParameterClass($p);
}
}
Autowiring container integration. For each class you instantiate, loop over __construct parameters: use native type if present, otherwise getParameterClass(), otherwise throw a "cannot resolve" error. Then recurse into the container.
public function resolve(string $class): object
{
$ref = new ReflectionClass($class);
$ctor = $ref->getConstructor();
if ($ctor === null) {
return $ref->newInstance();
}
$args = [];
foreach ($ctor->getParameters() as $param) {
$type = $this->typeResolver->resolveParameter($param);
if ($type === null) {
if ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
continue;
}
throw new RuntimeException("Cannot autowire \${$param->getName()} of $class");
}
$args[] = $this->resolve($type);
}
return $ref->newInstanceArgs($args);
}
Property injection. For setter-less property injection, iterate getProperties(), call getPropertyClass(), and assign via reflection (setAccessible(true) is a no-op on PHP 8.1+, still needed on older versions). This pattern shows up in Zen-style containers.
Prefer the declaring class for inherited members. ReflectionProperty::getDeclaringClass() points at the class that declared the docblock, and therefore at the file whose use statements matter. Always let the reader resolve against $property->getDeclaringClass()'s file — which it does internally — rather than re-resolving imports yourself.
Testing pattern. Build fixture classes in tests/Fixtures/ covering each resolution rule (FQCN, imported, relative, aliased) and assert on the string output. Keep these tests when you wrap the reader so an upstream swap is provably safe.
null, not a string. @var string, @var int[], @var array all yield null. Treat null as "unknown", not as "no dependency".@var Collection<Foo> resolves to Collection, dropping the type argument. If you need generics you must parse the docblock yourself on top of the raw text.@var Foo|Bar, nullable @var ?Foo, and @var Foo|null predate PHP 8 and are not modelled. Do not rely on the reader for union types; prefer native reflection, or normalize the docblock yourself before passing it through.class_exists() will reject. Validate downstream and fail with a message that quotes the original annotation.\Foo\Bar), use imports, aliases (use Foo as Bar), and relative names (Sub\Class) all work — but only within the declaring file. Grouped use statements and PHP 8.0+ constructor-promoted property docblocks are not explicitly handled; test your PHP version.#[Inject] style metadata, this reader is the wrong tool. Treat it as legacy phpdoc-only infrastructure.^1.6 and keep a one-class adapter in front so you can fork or replace it without touching call sites.(new ReflectionProperty($class, $prop))->getDocComment() and the use lines from (new ReflectionClass($class))->getFileName(). Nine out of ten surprises are a missing use statement or a docblock written in a different file than the declaring class.PhpDocReader is not final and has no interface, so subclassing to override resolution is possible but brittle (private helpers). Prefer composition and a custom resolver interface over inheritance.How can I help you explore Laravel packages today?