# 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 $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 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 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 $data Input data * @param callable(T): bool $filter Filter callback * @return array * @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 // 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 // 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 ', // 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 => 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')); $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 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 $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 $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 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 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 (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(int $id) ``` ## Type Aliases and Imports Parse `@phpstan-type` and `@phpstan-import-type` for type aliases. ```php * @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 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 print($phpDocNode); // Output: // /** // * @param array $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 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.