Try Live
Add Docs
Rankings
Pricing
Docs
Install
Install
Docs
Pricing
More...
More...
Try Live
Rankings
Enterprise
Create API Key
Add Docs
PHPDoc Parser
https://github.com/phpstan/phpdoc-parser
Admin
A library for parsing and modifying PHPDocs with an Abstract Syntax Tree (AST), also supporting
...
Tokens:
12,199
Snippets:
73
Trust Score:
9.4
Update:
2 months ago
Context
Skills
Chat
Benchmark
73.6
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# PHPStan PHPDoc Parser PHPStan's PHPDoc parser is a powerful PHP library that parses PHPDoc comments into an Abstract Syntax Tree (AST). It provides complete support for parsing PHPDoc tags (`@param`, `@return`, `@var`, etc.), complex type expressions (union types, intersection types, generics, callables, array shapes), and Doctrine annotations. The library is designed to be the foundation for static analysis tools, IDE plugins, and documentation generators that need to understand PHP type annotations. The parser follows a modular architecture with a lexer that tokenizes PHPDoc strings, specialized parsers for types, constant expressions, and full PHPDoc blocks, and an AST printer that can reproduce PHPDoc strings from the parsed tree. It supports format-preserving modifications, allowing you to parse a PHPDoc, modify parts of the AST, and print it back while preserving the original formatting of unchanged sections. ## Installation ```bash composer require phpstan/phpdoc-parser ``` ## Basic Parser Setup Create and configure the lexer and parsers using ParserConfig to parse PHPDoc strings into AST nodes. ```php <?php require_once __DIR__ . '/vendor/autoload.php'; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; // Create configuration - empty array for basic usage $config = new ParserConfig(usedAttributes: []); // Initialize the lexer $lexer = new Lexer($config); // Create parsers - TypeParser needs ConstExprParser for constant expressions $constExprParser = new ConstExprParser($config); $typeParser = new TypeParser($config, $constExprParser); $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); // Parse a PHPDoc string $phpDoc = '/** @param array<string, int> $items The items to process */'; $tokens = new TokenIterator($lexer->tokenize($phpDoc)); $phpDocNode = $phpDocParser->parse($tokens); // Access parsed data $paramTags = $phpDocNode->getParamTagValues(); echo $paramTags[0]->parameterName; // Output: $items echo $paramTags[0]->type; // Output: array<string, int> echo $paramTags[0]->description; // Output: The items to process ``` ## Parsing PHPDoc with Location Attributes Enable line and index tracking for precise source location information on AST nodes. ```php <?php use PHPStan\PhpDocParser\Ast\Attribute; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; // Enable location tracking with usedAttributes $config = new ParserConfig(usedAttributes: [ 'lines' => true, // Track start/end lines 'indexes' => true, // Track start/end token indexes 'comments' => true // Track comments within types ]); $lexer = new Lexer($config); $constExprParser = new ConstExprParser($config); $typeParser = new TypeParser($config, $constExprParser); $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); $phpDoc = '/** * @param string $name User name * @return bool */'; $tokens = new TokenIterator($lexer->tokenize($phpDoc)); $phpDocNode = $phpDocParser->parse($tokens); // Access location attributes on nodes foreach ($phpDocNode->getTags() as $tag) { $startLine = $tag->getAttribute(Attribute::START_LINE); $endLine = $tag->getAttribute(Attribute::END_LINE); $startIndex = $tag->getAttribute(Attribute::START_INDEX); $endIndex = $tag->getAttribute(Attribute::END_INDEX); echo "Tag {$tag->name} spans lines {$startLine}-{$endLine}\n"; } // Output: // Tag @param spans lines 2-2 // Tag @return spans lines 3-3 ``` ## Extracting Tag Values Use convenience methods on PhpDocNode to extract specific tag types. ```php <?php use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; $config = new ParserConfig(usedAttributes: []); $lexer = new Lexer($config); $constExprParser = new ConstExprParser($config); $typeParser = new TypeParser($config, $constExprParser); $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); $phpDoc = '/** * Process user data and return result. * * @template T of object * @param array<string, T> $data Input data * @param callable(T): bool $filter Filter callback * @return array<string, T> * @throws InvalidArgumentException When data is invalid * @deprecated Use processUserDataV2() instead */'; $tokens = new TokenIterator($lexer->tokenize($phpDoc)); $node = $phpDocParser->parse($tokens); // Get @param tags foreach ($node->getParamTagValues() as $param) { echo "Parameter: {$param->parameterName} of type {$param->type}\n"; } // Output: // Parameter: $data of type array<string, T> // Parameter: $filter of type callable(T): bool // Get @return tag $returns = $node->getReturnTagValues(); if (count($returns) > 0) { echo "Returns: {$returns[0]->type}\n"; } // Output: Returns: array<string, T> // Get @throws tags foreach ($node->getThrowsTagValues() as $throws) { echo "Throws: {$throws->type}\n"; } // Output: Throws: InvalidArgumentException // Get @template tags foreach ($node->getTemplateTagValues() as $template) { echo "Template: {$template->name}"; if ($template->bound !== null) { echo " of {$template->bound}"; } echo "\n"; } // Output: Template: T of object // Get @deprecated tag $deprecated = $node->getDeprecatedTagValues(); if (count($deprecated) > 0) { echo "Deprecated: {$deprecated[0]->description}\n"; } // Output: Deprecated: Use processUserDataV2() instead // Get all tags by name $customTags = $node->getTagsByName('@phpstan-param'); ``` ## Parsing Type Expressions Parse standalone type expressions using the TypeParser directly. ```php <?php use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode; use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode; use PHPStan\PhpDocParser\Ast\Type\NullableTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; $config = new ParserConfig(usedAttributes: []); $lexer = new Lexer($config); $typeParser = new TypeParser($config, new ConstExprParser($config)); // Parse various type expressions $types = [ 'string', // IdentifierTypeNode '?int', // NullableTypeNode 'string|int|null', // UnionTypeNode 'Countable&Traversable', // IntersectionTypeNode 'array<string, mixed>', // GenericTypeNode 'int[]', // ArrayTypeNode 'callable(string, int): bool', // CallableTypeNode 'array{name: string, age?: int}', // ArrayShapeNode 'Closure(int $x): void', // CallableTypeNode ]; foreach ($types as $typeString) { $tokens = new TokenIterator($lexer->tokenize($typeString)); $typeNode = $typeParser->parse($tokens); echo "{$typeString} => " . get_class($typeNode) . "\n"; } // Output: // string => PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode // ?int => PHPStan\PhpDocParser\Ast\Type\NullableTypeNode // string|int|null => PHPStan\PhpDocParser\Ast\Type\UnionTypeNode // Countable&Traversable => PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode // array<string, mixed> => PHPStan\PhpDocParser\Ast\Type\GenericTypeNode // int[] => PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode // callable(string, int): bool => PHPStan\PhpDocParser\Ast\Type\CallableTypeNode // array{name: string, age?: int} => PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode // Closure(int $x): void => PHPStan\PhpDocParser\Ast\Type\CallableTypeNode // Inspect a generic type $tokens = new TokenIterator($lexer->tokenize('array<string, int>')); $type = $typeParser->parse($tokens); if ($type instanceof GenericTypeNode) { echo "Base type: {$type->type->name}\n"; // array echo "Key type: {$type->genericTypes[0]}\n"; // string echo "Value type: {$type->genericTypes[1]}\n"; // int } // Inspect an array shape $tokens = new TokenIterator($lexer->tokenize('array{id: int, name: string, email?: string}')); $type = $typeParser->parse($tokens); if ($type instanceof ArrayShapeNode) { foreach ($type->items as $item) { $optional = $item->optional ? '?' : ''; echo "Key: {$item->keyName}{$optional} => {$item->valueType}\n"; } } // Output: // Key: id => int // Key: name => string // Key: email? => string ``` ## AST Traversal and Modification Use NodeTraverser with visitors to traverse and modify AST nodes. ```php <?php use PHPStan\PhpDocParser\Ast\AbstractNodeVisitor; use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Ast\NodeTraverser; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; $config = new ParserConfig(usedAttributes: []); $lexer = new Lexer($config); $constExprParser = new ConstExprParser($config); $typeParser = new TypeParser($config, $constExprParser); $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); // Create a visitor that collects all type nodes class TypeCollectorVisitor extends AbstractNodeVisitor { /** @var IdentifierTypeNode[] */ public array $types = []; public function enterNode(Node $node) { if ($node instanceof IdentifierTypeNode) { $this->types[] = $node; } return null; // Continue traversal } } // Create a visitor that renames types class TypeRenameVisitor extends AbstractNodeVisitor { private array $renames; public function __construct(array $renames) { $this->renames = $renames; } public function enterNode(Node $node) { if ($node instanceof IdentifierTypeNode) { if (isset($this->renames[$node->name])) { return new IdentifierTypeNode($this->renames[$node->name]); } } return null; } } $phpDoc = '/** @param array<int, User> $users */'; $tokens = new TokenIterator($lexer->tokenize($phpDoc)); $phpDocNode = $phpDocParser->parse($tokens); // Collect all types $collector = new TypeCollectorVisitor(); $traverser = new NodeTraverser([$collector]); $traverser->traverse([$phpDocNode]); echo "Found types:\n"; foreach ($collector->types as $type) { echo " - {$type->name}\n"; } // Output: // Found types: // - array // - int // - User // Rename types $renamer = new TypeRenameVisitor(['User' => 'App\\Entity\\User']); $traverser = new NodeTraverser([$renamer]); $modified = $traverser->traverse([$phpDocNode]); echo (string) $modified[0]; // Output: /** @param array<int, App\Entity\User> $users */ // Use NodeTraverser constants for flow control class StopOnFirstVisitor extends AbstractNodeVisitor { public ?IdentifierTypeNode $found = null; public function enterNode(Node $node) { if ($node instanceof IdentifierTypeNode && $node->name === 'User') { $this->found = $node; return NodeTraverser::STOP_TRAVERSAL; } return null; } } ``` ## Format-Preserving Printing Modify AST nodes while preserving the original formatting using CloningVisitor and Printer. ```php <?php use PHPStan\PhpDocParser\Ast\NodeTraverser; use PHPStan\PhpDocParser\Ast\NodeVisitor\CloningVisitor; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; use PHPStan\PhpDocParser\Printer\Printer; // Enable all attributes for format-preserving printing $config = new ParserConfig(usedAttributes: [ 'lines' => true, 'indexes' => true, 'comments' => true ]); $lexer = new Lexer($config); $constExprParser = new ConstExprParser($config); $typeParser = new TypeParser($config, $constExprParser); $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); $originalPhpDoc = '/** * @param string $name User name * @param int $age * @return bool */'; $tokens = new TokenIterator($lexer->tokenize($originalPhpDoc)); $originalNode = $phpDocParser->parse($tokens); // Clone the AST to preserve original nodes for comparison $cloningTraverser = new NodeTraverser([new CloningVisitor()]); /** @var PhpDocNode $modifiedNode */ [$modifiedNode] = $cloningTraverser->traverse([$originalNode]); // Modify only the first parameter's type $modifiedNode->getParamTagValues()[0]->type = new IdentifierTypeNode('non-empty-string'); // Print with format preservation $printer = new Printer(); $tokens = new TokenIterator($lexer->tokenize($originalPhpDoc)); $phpDocParser->parse($tokens); // Re-parse to get fresh tokens $tokens = new TokenIterator($lexer->tokenize($originalPhpDoc)); $phpDocParser->parse($tokens); $result = $printer->printFormatPreserving($modifiedNode, $originalNode, $tokens); echo $result; // Output (preserves spacing): // /** // * @param non-empty-string $name User name // * @param int $age // * @return bool // */ // Regular printing (without format preservation) echo $printer->print($modifiedNode); // Output (normalized spacing): // /** // * @param non-empty-string $name User name // * @param int $age // * @return bool // */ ``` ## Parsing Doctrine Annotations Parse Doctrine-style annotations embedded in PHPDoc comments. ```php <?php use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineAnnotation; use PHPStan\PhpDocParser\Ast\PhpDoc\Doctrine\DoctrineTagValueNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; $config = new ParserConfig(usedAttributes: []); $lexer = new Lexer($config); $constExprParser = new ConstExprParser($config); $typeParser = new TypeParser($config, $constExprParser); $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); $phpDoc = '/** * @ORM\Entity(repositoryClass="App\Repository\UserRepository") * @ORM\Table(name="users", indexes={ * @ORM\Index(name="email_idx", columns={"email"}) * }) * @Assert\NotBlank(message="Name is required") */'; $tokens = new TokenIterator($lexer->tokenize($phpDoc)); $node = $phpDocParser->parse($tokens); foreach ($node->getTags() as $tag) { echo "Tag: {$tag->name}\n"; if ($tag->value instanceof DoctrineTagValueNode) { $annotation = $tag->value->annotation; echo " Annotation: {$annotation->name}\n"; foreach ($annotation->arguments as $arg) { $key = $arg->key !== null ? "{$arg->key} = " : ""; echo " Argument: {$key}{$arg->value}\n"; } } } // Output: // Tag: @ORM\Entity // Annotation: @ORM\Entity // Argument: repositoryClass = "App\Repository\UserRepository" // Tag: @ORM\Table // Annotation: @ORM\Table // Argument: name = "users" // Argument: indexes = {@ORM\Index(name="email_idx", columns={"email"})} // Tag: @Assert\NotBlank // Annotation: @Assert\NotBlank // Argument: message = "Name is required" ``` ## Working with Property and Method Tags Parse `@property`, `@property-read`, `@property-write`, and `@method` tags for magic properties and methods. ```php <?php use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; $config = new ParserConfig(usedAttributes: []); $lexer = new Lexer($config); $constExprParser = new ConstExprParser($config); $typeParser = new TypeParser($config, $constExprParser); $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); $phpDoc = '/** * @property string $name * @property-read int $id * @property-write bool $active * @method string getName() * @method void setName(string $name) * @method static User create(string $name, int $age = 0) * @method T|null find<T of Entity>(int $id) */'; $tokens = new TokenIterator($lexer->tokenize($phpDoc)); $node = $phpDocParser->parse($tokens); // Get @property tags echo "Properties:\n"; foreach ($node->getPropertyTagValues() as $prop) { echo " {$prop->propertyName}: {$prop->type}\n"; } // Output: // Properties: // $name: string // Get @property-read tags echo "Read-only properties:\n"; foreach ($node->getPropertyReadTagValues() as $prop) { echo " {$prop->propertyName}: {$prop->type}\n"; } // Output: // Read-only properties: // $id: int // Get @property-write tags echo "Write-only properties:\n"; foreach ($node->getPropertyWriteTagValues() as $prop) { echo " {$prop->propertyName}: {$prop->type}\n"; } // Output: // Write-only properties: // $active: bool // Get @method tags echo "Methods:\n"; foreach ($node->getMethodTagValues() as $method) { $static = $method->isStatic ? 'static ' : ''; $return = $method->returnType !== null ? "{$method->returnType} " : ''; $params = implode(', ', array_map(function($p) { $type = $p->type !== null ? "{$p->type} " : ''; $default = $p->defaultValue !== null ? " = {$p->defaultValue}" : ''; return "{$type}{$p->parameterName}{$default}"; }, $method->parameters)); // Template types $templates = ''; if (count($method->templateTypes) > 0) { $templates = '<' . implode(', ', array_map(function($t) { $bound = $t->bound !== null ? " of {$t->bound}" : ''; return "{$t->name}{$bound}"; }, $method->templateTypes)) . '>'; } echo " {$static}{$return}{$method->methodName}{$templates}({$params})\n"; } // Output: // Methods: // string getName() // void setName(string $name) // static User create(string $name, int $age = 0) // T|null find<T of Entity>(int $id) ``` ## Type Aliases and Imports Parse `@phpstan-type` and `@phpstan-import-type` for type aliases. ```php <?php use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; $config = new ParserConfig(usedAttributes: []); $lexer = new Lexer($config); $constExprParser = new ConstExprParser($config); $typeParser = new TypeParser($config, $constExprParser); $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); $phpDoc = '/** * @phpstan-type UserId int<1, max> * @phpstan-type UserData array{id: UserId, name: string, email: string} * @phpstan-import-type Address from Customer * @phpstan-import-type OrderId from Order as OrderIdentifier */'; $tokens = new TokenIterator($lexer->tokenize($phpDoc)); $node = $phpDocParser->parse($tokens); // Get type aliases echo "Type Aliases:\n"; foreach ($node->getTypeAliasTagValues() as $alias) { echo " {$alias->alias} = {$alias->type}\n"; } // Output: // Type Aliases: // UserId = int<1, max> // UserData = array{id: UserId, name: string, email: string} // Get type imports echo "Type Imports:\n"; foreach ($node->getTypeAliasImportTagValues() as $import) { $as = $import->importedAs !== null ? " as {$import->importedAs}" : ''; echo " {$import->importedAlias} from {$import->importedFrom}{$as}\n"; } // Output: // Type Imports: // Address from Customer // OrderId from Order as OrderIdentifier ``` ## Assert Tags and Type Narrowing Parse `@phpstan-assert` tags for type narrowing in conditional contexts. ```php <?php use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagMethodValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagPropertyValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\AssertTagValueNode; use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; $config = new ParserConfig(usedAttributes: []); $lexer = new Lexer($config); $constExprParser = new ConstExprParser($config); $typeParser = new TypeParser($config, $constExprParser); $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); $phpDoc = '/** * @phpstan-assert string $value * @phpstan-assert !null $param * @phpstan-assert-if-true non-empty-string $text * @phpstan-assert-if-false null $this->property * @phpstan-assert int $obj->getValue() */'; $tokens = new TokenIterator($lexer->tokenize($phpDoc)); $node = $phpDocParser->parse($tokens); // Get basic assert tags foreach ($node->getAssertTagValues() as $assert) { $negated = $assert->isNegated ? '!' : ''; echo "Assert: {$negated}{$assert->type} {$assert->parameter}\n"; } // Output: // Assert: string $value // Assert: !null $param // Get all assert tags including if-true/if-false variants $allAsserts = array_merge( $node->getAssertTagValues('@phpstan-assert'), $node->getAssertTagValues('@phpstan-assert-if-true'), $node->getAssertTagValues('@phpstan-assert-if-false') ); // Get property assertions foreach ($node->getAssertPropertyTagValues('@phpstan-assert-if-false') as $assert) { echo "Assert property: {$assert->type} {$assert->parameter}->{$assert->property}\n"; } // Output: // Assert property: null $this->property // Get method assertions foreach ($node->getAssertMethodTagValues('@phpstan-assert') as $assert) { echo "Assert method: {$assert->type} {$assert->parameter}->{$assert->method}()\n"; } // Output: // Assert method: int $obj->getValue() ``` ## Printing AST Nodes Use the Printer class to convert AST nodes back to string format. ```php <?php use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeItemNode; use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode; use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode; use PHPStan\PhpDocParser\Printer\Printer; $printer = new Printer(); // Build a PHPDoc node programmatically $paramType = new GenericTypeNode( new IdentifierTypeNode('array'), [new IdentifierTypeNode('string'), new IdentifierTypeNode('mixed')] ); $returnType = new UnionTypeNode([ new IdentifierTypeNode('bool'), new IdentifierTypeNode('null') ]); $phpDocNode = new PhpDocNode([ new PhpDocTagNode('@param', new ParamTagValueNode( $paramType, false, // isVariadic '$data', // parameterName 'Input data', // description false // isReference )), new PhpDocTagNode('@return', new ReturnTagValueNode( $returnType, 'Success status' )) ]); echo $printer->print($phpDocNode); // Output: // /** // * @param array<string, mixed> $data Input data // * @return bool|null Success status // */ // Print individual type nodes $arrayShape = ArrayShapeNode::createSealed([ new ArrayShapeItemNode( new IdentifierTypeNode('id'), false, new IdentifierTypeNode('int') ), new ArrayShapeItemNode( new IdentifierTypeNode('name'), true, // optional new IdentifierTypeNode('string') ), ]); echo $printer->print($arrayShape); // Output: array{id: int, name?: string} // Using __toString() method on nodes echo (string) $arrayShape; // Output: array{id: int, name?: string} ``` ## Handling Parser Exceptions Handle parsing errors gracefully with ParserException. ```php <?php use PHPStan\PhpDocParser\Lexer\Lexer; use PHPStan\PhpDocParser\ParserConfig; use PHPStan\PhpDocParser\Parser\ConstExprParser; use PHPStan\PhpDocParser\Parser\ParserException; use PHPStan\PhpDocParser\Parser\PhpDocParser; use PHPStan\PhpDocParser\Parser\TokenIterator; use PHPStan\PhpDocParser\Parser\TypeParser; $config = new ParserConfig(usedAttributes: []); $lexer = new Lexer($config); $constExprParser = new ConstExprParser($config); $typeParser = new TypeParser($config, $constExprParser); $phpDocParser = new PhpDocParser($config, $typeParser, $constExprParser); // Invalid type syntax - will be captured as InvalidTagValueNode $invalidPhpDoc = '/** @param array{invalid: */'; $tokens = new TokenIterator($lexer->tokenize($invalidPhpDoc)); $node = $phpDocParser->parse($tokens); $paramTags = $node->getParamTagValues(); // Empty array - invalid syntax becomes InvalidTagValueNode instead // Check for invalid tags foreach ($node->getTags() as $tag) { if ($tag->value instanceof \PHPStan\PhpDocParser\Ast\PhpDoc\InvalidTagValueNode) { echo "Invalid tag {$tag->name}: {$tag->value->value}\n"; echo "Error: {$tag->value->exception->getMessage()}\n"; } } // Output: // Invalid tag @param: array{invalid: // Error: Unexpected token "*/", expected type at offset 22 on line 1 // Direct type parsing throws exceptions try { $tokens = new TokenIterator($lexer->tokenize('array{broken')); $typeParser->parse($tokens); } catch (ParserException $e) { echo "Parse error at offset {$e->getCurrentOffset()}, line {$e->getCurrentLine()}\n"; echo "Expected: {$e->getExpectedTokenType()}\n"; echo "Got: {$e->getCurrentTokenValue()}\n"; } // Output: // Parse error at offset 12, line 1 // Expected: 7 (TOKEN_CLOSE_CURLY_BRACKET) // Got: ``` ## Summary PHPStan's PHPDoc parser provides a comprehensive solution for parsing and manipulating PHPDoc comments in PHP code. Key use cases include static analysis tools that need to understand type annotations, IDE plugins providing autocompletion and type hints, documentation generators that extract API information from PHPDoc blocks, and code transformation tools that need to modify PHPDoc comments while preserving formatting. The library handles all standard PHPDoc tags plus PHPStan/Psalm-specific extensions like `@phpstan-type`, `@phpstan-assert`, and `@template`. The parser integrates seamlessly into existing PHP projects through Composer and provides a well-designed API with clear separation between lexing, parsing, and printing phases. The AST-based approach enables powerful traversal and modification capabilities through the visitor pattern, while format-preserving printing ensures that automated code modifications maintain the original code style. With support for Doctrine annotations and comprehensive error handling, the library serves as a robust foundation for any PHP tooling that needs to work with type information in PHPDoc comments.