phpdocumentor/reflection-docblock
A DocBlock parser fully compatible with the PHPDoc standard. Use its DocBlockFactory to read summaries, descriptions, tags, and annotations from doc comments or reflection classes, so your library can extract embedded documentation and metadata.
Install it as a dev dependency — in a Laravel app this library belongs in tooling (artisan commands, generators, CI checks), not in the request path:
composer require --dev phpdocumentor/reflection-docblock:^6.0
Pin ^6.0.3 or later on PHP 8.4 projects (explicit nullable deprecations and escaped */ handling were fixed there).
Bind the factory once so it is injectable and swappable in tests:
// app/Providers/DocBlockServiceProvider.php
use phpDocumentor\Reflection\DocBlockFactory;
public function register(): void
{
$this->app->singleton(DocBlockFactory::class, fn () => DocBlockFactory::createInstance());
}
First use case — an artisan command that dumps what the parser sees for any class or method, which is also your debugging tool for the rest of the work:
use Illuminate\Console\Command;
use phpDocumentor\Reflection\DocBlockFactory;
class DocBlockInspect extends Command
{
protected $signature = 'docblock:inspect {class} {method?}';
public function handle(DocBlockFactory $factory): int
{
$reflection = $this->argument('method')
? new \ReflectionMethod($this->argument('class'), $this->argument('method'))
: new \ReflectionClass($this->argument('class'));
$comment = $reflection->getDocComment();
if ($comment === false) {
$this->warn('No DocBlock found.');
return self::FAILURE;
}
// create() also accepts any object exposing getDocComment()
$docblock = $factory->create($reflection);
$this->line('<info>Summary:</info> ' . $docblock->getSummary());
$this->line('<info>Description:</info> ' . $docblock->getDescription()->render());
foreach ($docblock->getTags() as $tag) {
$this->line(sprintf(' @%s → %s', $tag->getName(), (string) $tag));
}
return self::SUCCESS;
}
}
Then read vendor/phpdocumentor/reflection-docblock/docs/examples and src/DocBlockFactory.php — the factory signatures are the API surface you will actually touch, and they differ slightly between 4.x, 5.x and 6.x.
Annotation-style discovery (the main Laravel use case). Scan a directory, reflect each class, and turn custom tags into registrations inside a service provider. Keep the scan in a compiled/cached step, never per request:
$files = File::allFiles(app_path('Http/Controllers'));
foreach ($files as $file) {
$class = 'App\\' . str_replace(
['/', '.php'],
['\\', ''],
Str::after($file->getPathname(), app_path() . '/')
);
if (! class_exists($class)) {
continue;
}
$docblock = $factory->create(new \ReflectionClass($class));
foreach ($docblock->getTagsByName('tenant-billing') as $tag) {
$plan = trim($tag->getDescription()->render()); // Generic tag body
// register $class => $plan somewhere (config, container, route defaults)
}
}
Read unknown tags without writing tag classes. Any tag the StandardTagFactory does not recognise comes back as phpDocumentor\Reflection\DocBlock\Tags\Generic, which keeps getName() and the raw body. This is the lowest-friction path for a house-style annotation (@owner, @feature-flag, @internal-api). Only write a dedicated Tag class when you need structured parsing (a type, multiple arguments, validation).
Register real custom tags. DocBlockFactory::createInstance() takes an additional-tags map (tag name → class implementing Tag), and the underlying StandardTagFactory exposes tag-handler registration/extension points. Use this when you want typed accessors instead of string parsing — verify the exact signature against the installed version, it changed in 6.0.
Cache the extraction, not the object graph. DocBlock results are serializable again (fixed in 5.0.0-alpha9), so Cache::rememberForever($key, fn () => $factory->create($comment)) works. Safer in practice: cache a plain array of the fields you extracted, keyed by path + mtime + package version, so a package upgrade can never hydrate a stale class shape:
$key = sprintf('docblock:%s:%d:%s', $file->getPathname(), $file->getMTime(), Package::VERSION);
CI linting. A docblock:lint command that walks app/ and returns self::FAILURE when a tag is an InvalidTag (or when a required @param is missing) gives you a cheap, project-specific doc standard gate that PHPStan/Larastan won't enforce for you.
Generators. For OpenAPI/schema/IDE-helper style output, combine @param, @return and @property tags with phpdocumentor/type-resolver to get normalised type strings, then map them to Laravel validation rules or JSON Schema types. @property on Eloquent models is the most common real-world source you will be reading.
Prefer native attributes for new code. PHP 8 attributes (#[Route(...)]) are first-class in Laravel and cheaper; use this package for reading existing docblocks, for anything that must live alongside PHPDoc (generics, @mixin, @method), and for tooling that must also accept legacy annotations.
InvalidTag object in the result set rather than an exception. Always filter with instanceof InvalidTag (and inspect the wrapped exception) before trusting getTags() output.InvalidTag. Codebases written for Larastan/PHPStan are full of array<string, mixed> and similar; do not assume this parser round-trips them. Test one of your real controller docblocks through docblock:inspect before building on top of it.getDocComment() yourself. It returns false when absent; check it before calling create() rather than relying on the factory to be forgiving.. plus whitespace starts the description. Summaries containing abbreviations ("e.g. ") get truncated in surprising places.getDescription() is not on the Tag interface. It exists on Generic/BaseTag; if you only hold a Tag, use (string) $tag or render(). Do not type-hint getDescription() across arbitrary tags.getTagsByName('param'), not 'Param') and be consistent in your own annotations.phpdocumentor/type-resolver. 6.x requires type-resolver 2.x, which can conflict with other dev tooling that pins 1.x. Check composer why phpdocumentor/type-resolver before upgrading in a large app.*/. Both have caused parsing bugs historically; stay on a recent patch release if your repo is developed on mixed platforms.Generic tag body → additional-tags map on createInstance() → custom Tag class → custom handler on StandardTagFactory. Climb only as far as you need.render() on the DocBlock/Description reconstructs a docblock from the parsed model. If render() output doesn't match the source comment, the parser disagrees with you about the structure — a fast way to find where.How can I help you explore Laravel packages today?