PHP TypeLang Help

PHPDoc Parser Component

Most PHP code carries part of its type information not in the signature itself, but in the docblock above it — a @param narrowing an array argument down to list<User>, a @throws PHP has no native syntax for at all, a @template turning a plain class into a generic one. The PHPDoc component reads that comment and turns it into a small, immutable object graph: one entry per line, each already parsed according to what that particular tag means.

Every type a tag carries — a @param's argument type, a @return's result, a @template's bound — is parsed with the very same TypeLang grammar used everywhere else in this project. A type written inside a docblock is therefore described exactly as precisely as one written directly in PHP code; nothing is lost by moving it into a comment.

Installation

Requirements:

  • PHP >= 8.4

  • ext-mbstring optional

Usage

Parsing a comment is a single call: hand DocBlockParser::parse() the raw /** ... */ text, and it hands back a DocBlock.

use TypeLang\PhpDoc\DocBlockParser; $parser = DocBlockParser::createDefault(); $block = $parser->parse(<<<'PHPDOC' /** * Sends a notification to the given recipient. * * @see Mailer::send() The underlying transport. * @link https://example.com/docs Delivery documentation. * @return bool */ PHPDOC); var_dump($block);
TypeLang\PhpDoc\DocBlock\DocBlock { +tags: array:3 [ 0 => TypeLang\PhpDoc\DocBlock\Tag\SeeTag\SeeTag { +name: "see" +description: TypeLang\PhpDoc\DocBlock\Description\Description { +value: "The underlying transport." } +reference: TypeLang\PhpDoc\DocBlock\Reference\ClassMethodReference { +isExternal: false +class: "Mailer" +name: "send" } } 1 => TypeLang\PhpDoc\DocBlock\Tag\LinkTag\LinkTag { +name: "link" +description: TypeLang\PhpDoc\DocBlock\Description\Description { +value: "Delivery documentation." } +reference: TypeLang\PhpDoc\DocBlock\Reference\UriReference { +isExternal: true +uri: "https://example.com/docs" } } 2 => TypeLang\PhpDoc\DocBlock\Tag\ReturnTag\ReturnTag { +name: "return" +description: null +type: TypeLang\Type\NamedTypeNode { +name: TypeLang\Type\Name { +segments: array:1 [ 0 => TypeLang\Type\Identifier { +value: "bool" } ] +isFullyQualified: false } +arguments: null +fields: null } } ] +description: TypeLang\PhpDoc\DocBlock\Description\Description { +value: "Sends a notification to the given recipient.\n" } }

The description — everything written before the first tag — is available as $block->description, and the tags themselves form an ordered, countable, iterable collection:

echo $block->description; // "Sends a notification to the given recipient." echo count($block); // 3 foreach ($block as $tag) { echo $tag->name; // "see", "link", "return" } $see = $block[0]; // SeeTag echo $see->reference; // "Mailer::send()"

A single malformed tag never brings the rest of the comment down with it. A tag whose name isn't recognized, or whose body doesn't match the grammar expected for its name, is not thrown as an exception — it is returned as an InvalidTag, carrying the failure reason alongside it, so that the other, well-formed tags around it are still parsed normally:

$block = $parser->parse(<<<'PHPDOC' /** * @param int $wellFormed * @param not a valid type here $because */ PHPDOC); $broken = $block[1]; // object(InvalidTag) echo $broken->name; // "param" // Malformed "@param" tag, expected: // <Type> <Variable> [ <Description> ] echo $broken->reason->getMessage();

How a Comment Is Read

Parsing happens in two passes that mirror the two logical parts of a comment: first the whole text is split into the leading description and one segment per tag line, and only then is each of those segments parsed on its own — the description checked for inline tags, each tag line checked against the grammar declared for its name.

split

split

split

parsed for inline tags

parsed against its grammar

parsed against its grammar

/** ... */ comment

Description segment

@tag segment #1

@tag segment #2

DocBlock

DocBlock

A DocBlock is a read-only snapshot of the whole comment: an optional description and the ordered tags list. It additionally behaves as a collection of its own tags — countable, iterable, and indexable by integer offset — so count($block), foreach ($block as $tag) and $block[0] all work directly on it without reaching for ->tags explicitly.

Description

Everything before the first tag becomes the description. Most of the time that is just text, represented by a plain Description object exposing a single $value string. When the text additionally contains one or more inline tags — a {@tag ...} sequence with balanced braces, such as {@see Mailer::send()} written in the middle of a sentence — it is represented as a TaggedDescription instead: an ordered mix of plain-text fragments and the nested tags found among them.

/** * Hello world {@see Mailer::send()} and more text after it. └────┬────┘ └─────────┬─────────┘ └──────────┬──────────┘ text nested tag text */

Only a handful of tags — @see, @link, @internal, @inheritdoc, @example and @source — are ever recognized this way. Whether a tag may be lifted out of running text is declared by its placement; a {@param}, for instance, is never lifted out: @param is a block-only tag, so the braces around it stay exactly as written, as plain text.

Tag

Every tag, whether it stood on its own line or was found nested inside a description, carries at minimum a $name (without the leading @) and an optional $description (whatever text follows its own body). A tag recognized by name exposes further, tag-specific parts on top of that — a ParamTag additionally exposes the argument's $type and $variable, a SeeTag exposes what it $references, and so on. A tag whose name has no registered definition at all — including every tag still marked "Not Implemented" in the sidebar — falls back to a plain Tag, with its entire suffix folded unparsed into the description.

Where to Go Next

Every tag this component recognizes has its own page in the sidebar, grouped by where it comes from: Standard, Advanced, phpDocumentor, or a specific static analyzer (Psalm, PHPStan, Phan, PhpStorm, PHP CodeSniffer) for the ones not yet implemented.

Extending

How a tag's own grammar is declared, how to add a tag of your own, and how to write a new grammar building block (a "combinator") for one. See Extending.

08 July 2026