Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Phpdoc Parser Laravel Package

phpstan/phpdoc-parser

phpstan/phpdoc-parser parses PHPDocs into a typed AST so you can read and modify docblocks programmatically. It handles rich types like unions, generics, array shapes, callables, and conditional types, plus Doctrine annotations. Powers PHPStan.

View on GitHub
Deep Wiki
Context7

Getting Started

phpstan/phpdoc-parser is a low-level, framework-agnostic library: it turns PHPDoc comment strings into an AST and can print them back. It does not resolve class names, load classes, or enforce semantics — you feed it strings and get node objects. That is exactly why it is useful in Laravel apps: you can read @param, @return, @var, @template, array shapes and generics from docblocks and act on them (generate OpenAPI schemas, form rules, TypeScript types, custom linting, Rector/PHPStan rules).

Install and look here first:

composer require phpstan/phpdoc-parser
  1. README "Basic usage" — the five-object bootstrap (ParserConfig → Lexer → ConstExprParser → TypeParser → PhpDocParser).
  2. The API reference (https://phpstan.github.io/phpdoc-parser/2.3.x/namespace-PHPStan.PhpDocParser.html) — this is your real documentation. Browse the Ast\PhpDoc and Ast\Type namespaces; every node class you will instanceof-check lives there.
  3. PHPStan's PHPDoc Types page — it doubles as the grammar spec for what the parser accepts.

There is no service provider; bind the parser once in a container singleton so you don't rebuild it per file.

First use case — extract a typed parameter list from a method's docblock:

use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\{ConstExprParser, PhpDocParser, TokenIterator, TypeParser};
use PHPStan\PhpDocParser\ParserConfig;

// Register once (e.g. in a AppServiceProvider::register())
$config = new ParserConfig(usedAttributes: []); // no attributes => fastest, least memory
$lexer  = new Lexer($config);
$const  = new ConstExprParser($config);
$phpDoc = new PhpDocParser($config, new TypeParser($config, $const), $const);

$reflection = new ReflectionMethod(Invoice::class, 'store');
$doc = $reflection->getDocComment() ?: '/** */';

$tokens = new TokenIterator($lexer->tokenize($doc));
$node   = $phpDoc->parse($tokens); // PhpDocNode

foreach ($node->getParamTagValues() as $tag) {
    /** @var ParamTagValueNode $tag */
    echo $tag->parameterName;      // '$payload'
    echo (string) $tag->type;      // 'array{name: string}' — nodes are Stringable
    echo $tag->isVariadic ? '...' : '';
}

Every AST node implements __toString(), which is a normalized (whitespace-collapsed) rendering of the type — genuinely handy for comparisons and cache keys.

Implementation Patterns

1. Typed accessors beat hand-rolled preg_match. PhpDocNode exposes getParamTagValues(), getReturnTagValues(), getVarTagValues(), getMethodTagValues(), getTemplateTagValues(), getExtendsTagValues(), getImplementsTagValues(), getThrowsTagValues(), getPropertyTagValues(), getTypeAliasTagValues(). Anything unrecognised still lands in $node->getTagsByName('@foo') as an UnknownTagNode — never silently dropped.

2. Validate a bare type string (no surrounding tag). This is the pattern for "is Foo|Bar<int> a legal type?" inputs, form validation, and CLI tools:

$tokens = new TokenIterator($lexer->tokenize($typeString));
$type   = $typeParser->parse($tokens);
$tokens->consumeTokenType(Lexer::TOKEN_END); // throws if trailing garbage

3. Traverse and transform with visitors, not foreach recursion. NodeTraverser + AbstractNodeVisitor mirrors nikic/PHP-Parser, so anyone who has written a Rector or PHPStan rule already knows the shape. Return NodeTraverser::DONT_TRAVERSE_CHILDREN, STOP_TRAVERSAL, REMOVE_NODE, or DONT_TRAVERSE_CURRENT_AND_CHILDREN from enterNode()/leaveNode() to control flow. Typical transformations: rewrite bare array → array<mixed>, normalize Collection<T> variance, replace legacy @param with @phpstan-param.

4. Format-preserving rewrites. The killer feature. The sequence is always: parse with full attributes → deep-clone → mutate the clone → print against the original.

$config = new ParserConfig(usedAttributes: ['lines' => true, 'indexes' => true, 'comments' => true]);
// ... lexer/parsers built from that config ...

$tokens   = new TokenIterator($lexer->tokenize($originalDoc));
$original = $phpDocParser->parse($tokens);

[$modified] = (new NodeTraverser([new CloningVisitor()]))->traverse([$original]);
$modified->getParamTagValues()[0]->type = new IdentifierTypeNode('Ipsum');

echo (new Printer())->printFormatPreserving($modified, $original, $tokens);

Use this in Artisan commands, Rector rules, or a "docblock linter with autofix" CI job. It preserves alignment, spacing and untouched lines, so diffs stay reviewable.

5. Cache the AST, not the parse. __set_state() is implemented on all node classes (since 2.3.1), so var_export() of a PhpDocNode is re-loadable via require. Cache per file keyed by md5($path . $mtime . $docComment) and skip the lexer entirely on warm runs:

$file = storage_path("app/phpdoc/{$key}.php");
if (! is_file($file)) {
    file_put_contents($file, '<?php return ' . var_export($node, true) . ';');
}
$node = require $file;

Verify round-tripping after version bumps — the export format tracks node internals, not a stable schema.

6. Inside PHPStan or Rector, don't re-parse. Both already hand you the PhpDocNode via $scope->getPhpDocNode() / the Rector PhpDocInfo API. Re-parsing is pure overhead and can drift from their config. Reserve direct parser use for runtime code, codegen, and CI tooling.

7. Runtime codegen is where Laravel teams get the most value. Parse controller or DTO docblocks containing array{name: string, age: int, tags?: list<string>} and emit:

  • Laravel FormRequest rules (map node types to string/integer/array/sometimes),
  • OpenAPI/JSON Schema fragments (ArrayShapeNode maps almost 1:1),
  • TypeScript interfaces for an Inertia front end,
  • Blade-admin form definitions.

Walk the shape via ArrayShapeNode::$items, each ArrayShapeItemNode giving $keyName, $valueType, and $optional. Handle both sealed and open shapes (...) — $node->sealed tells you which.

8. Use it as a docblock contract test. Assert that public service methods have a @return and that no docblock contains unresolvable identifiers, or that @param names exactly match reflection parameter names. A single Artisan command plus a CI step catches docblock rot better than PHPStan alone.

Gotchas and Tips

  • 2.x requires ParserConfig as the first constructor argument; 1.x did not. Almost every StackOverflow snippet you will find is 1.x. If another dependency pins ^1.0, Composer will happily install the old API and your new Lexer($config) calls will be wrong. Check composer show phpstan/phpdoc-parser.
  • TokenIterator is stateful and consumed. After $phpDocParser->parse($tokens) the iterator sits at the end. Do not reuse it for a second parse, and do not try to read positions off it afterwards — re-tokenize instead. Keep the token iterator alive only when you need printFormatPreserving().
  • usedAttributes: [] vs. everything. Enabling lines/indexes/comments costs memory and time on every file. Only enable the ones you need: indexes + comments are effectively mandatory for the format-preserving printer, lines for error messages. Mixing a parser built with attributes off and a printer expecting them produces broken output or exceptions.
  • printFormatPreserving() argument order is (new, old, tokens) — a classic footgun. Passing only the modified node, or the modified node as "old", silently destroys formatting.
  • Catch ParserException per docblock. A single malformed comment in a third-party vendor file must not break your whole build. Wrap in try/catch, log the file and offset, fall back to the raw string. Also guard against invalid UTF-8 (e.g. legacy ISO-8859-1 source files) before tokenizing.
  • No name resolution, no validation. IdentifierTypeNode('User') stays User; Foo&Bar is accepted even if Foo and Bar are unrelated classes; generic arity is not checked. Implement FQCN resolution yourself with ReflectionClass/ReflectionMethod + the file's use statements, and treat everything the parser gives you as syntax-valid, not semantics-valid.
  • Type nodes are a wide class hierarchy. IdentifierTypeNode, GenericTypeNode, UnionTypeNode, IntersectionTypeNode, ArrayTypeNode, ArrayShapeNode, ObjectShapeNode, CallableTypeNode, ConditionalTypeNode, OffsetAccessTypeNode, ConstTypeNode, NullableTypeNode, ThisTypeNode, ConstExprIntegerNode… Do not regex the string form when instanceof is available; conversely, (string) $node is fine for logging and hashing but loses structure.
  • Array shapes have ConstExprParser in the loop. Default values like array{status: 'draft'|'sent'} and constant fetches (self::CONST, Foo::BAR) are const expressions, not types — you must build ConstExprParser before TypeParser and pass it in, or those shapes throw.
  • Extension point for custom tags: subclass PhpDocParser and override the protected parseTagValue(TokenIterator $tokens, PhpDocTagNode $tag), delegating to parent:: for everything you don't recognise. Return your own PhpDocTagValueNode subclass. This is how you support app-specific tags (@openapi, @tenantScope) with real typed nodes instead of string splitting UnknownTagNode values.
  • Doctrine annotations are parsed into Ast\PhpDoc\Doctrine — relevant if you use doctrine/orm entities in a Laravel app; don't write your own annotation scanner.
  • Performance reality check: PHPStan caches aggressively for a reason. Parsing every docblock in a 5k-file app/ on every request is a self-inflicted outage — do it in CI/Artisan codegen, or cache with the var_export trick and revalidate on mtime. Recent 2.3.x releases include token-offset optimizations, so pin a floor like ^2.3 rather than a wide range.
  • Testing tip: assert on (string) $node for normalized output and on instanceof + node properties for structure. For printer tests, compare the full resulting docblock string — that's the actual contract.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums