1- # The PHPDoc Parser Component
1+ # PHPDoc Parser Component
22
33<primary-label ref =" phpdoc-component " />
44<show-structure for =" chapter " depth =" 2 " />
55
6- The printer package is responsible for visualizing the AST as string
7- formats.
6+ Most PHP code carries part of its type information not in the signature
7+ itself, but in the docblock above it — a ` @param ` narrowing an ` array `
8+ argument down to ` list<User> ` , a ` @throws ` PHP has no native syntax for at
9+ all, a ` @template ` turning a plain class into a generic one. The PHPDoc
10+ component reads that comment and turns it into a small, immutable object
11+ graph: one entry per line, each already parsed according to what that
12+ particular tag means.
13+
14+ Every type a tag carries — a ` @param ` 's argument type, a ` @return ` 's result,
15+ a ` @template ` 's bound — is parsed with the very same
16+ [ TypeLang grammar] ( introduction.md ) used everywhere else in this project.
17+ A type written inside a docblock is therefore described exactly as precisely
18+ as one written directly in PHP code; nothing is lost by moving it into a
19+ comment.
820
921## Installation
1022
@@ -15,22 +27,163 @@ formats.
1527 </p>
1628</tldr >
1729
30+ ** Requirements:**
31+ * ` PHP >= 8.4 `
32+ * ` ext-mbstring ` <sup >optional</sup >
33+
34+ <note >
35+ Every tag this component understands — including the generic
36+ (<code >@template </code >) and structural (<code >@param </code >,
37+ <code >@property </code >, ...) families — ships in this single package.
38+ Earlier releases split those out into separate
39+ <code >type-lang/phpdoc-standard-tags</code > and
40+ <code >type-lang/phpdoc-template-tags</code > extensions; both have since been
41+ folded back in, and installing <code >type-lang/phpdoc</code > alone is
42+ enough.
43+ </note >
44+
45+ ## Usage
46+
47+ Parsing a comment is a single call: hand ` DocBlockParser::parse() ` the raw
48+ ` /** ... */ ` text, and it hands back a ` DocBlock ` .
49+
50+ ``` php
51+ use TypeLang\PhpDoc\DocBlockParser;
52+
53+ $parser = new DocBlockParser();
54+
55+ $block = $parser->parse(<<<'PHPDOC'
56+ /**
57+ * Sends a notification to the given recipient.
58+ *
59+ * @see Mailer::send() The underlying transport.
60+ * @link https://example.com/docs Delivery documentation.
61+ * @return bool
62+ */
63+ PHPDOC);
64+ ```
65+
66+ The description — everything written before the first tag — is available as
67+ ` $block->description ` , and the tags themselves form an ordered, countable,
68+ iterable collection:
69+
70+ ``` php
71+ echo $block->description;
72+ // "Sends a notification to the given recipient."
73+
74+ echo count($block); // 3
75+
76+ foreach ($block as $tag) {
77+ echo $tag->name; // "see", "link", "return"
78+ }
79+
80+ $see = $block[0]; // SeeTag
81+ echo $see->reference; // "Mailer::send()"
82+ ```
83+
84+ A single malformed tag never brings the rest of the comment down with it.
85+ A tag whose name isn't recognized, or whose body doesn't match the grammar
86+ expected for its name, is not thrown as an exception — it is returned as an
87+ ` InvalidTag ` , carrying the failure reason alongside it, so that the other,
88+ well-formed tags around it are still parsed normally:
89+
90+ ``` php
91+ $block = $parser->parse(<<<'PHPDOC'
92+ /**
93+ * @param int $wellFormed
94+ * @param not a valid type here $because
95+ */
96+ PHPDOC);
97+
98+ $broken = $block[1]; // object(InvalidTag)
99+
100+ echo $broken->name; // "param"
101+
102+ // Malformed "@param" tag, expected:
103+ // <Type > <Variable > [ <Description > ]
104+ echo $broken->reason->getMessage();
105+ ```
106+
18107<tip >
19- In addition, extensions are available that complement the
20- functionality of the component.
21- <ul>
22- <li><a href="phpdoc-standard-tags.md">PHPDoc Standard Tags</a></li>
23- <li><a href="phpdoc-template-tags.md">PHPDoc Template Tags</a></li>
24- </ul>
108+ A <code >DocBlockParser</code > instance builds its internal tag and type
109+ grammar once, in its constructor, and is otherwise stateless. Construct it
110+ once and reuse (or share) it across an entire application, rather than
111+ building a new instance per docblock.
25112</tip >
26113
27- ** Requirements:**
28- * ` PHP >= 8.1 `
114+ ## How a Comment Is Read
29115
30- ## Usage
116+ Parsing happens in two passes that mirror the two logical parts of a
117+ comment: first the whole text is split into the leading description and one
118+ segment per tag line, and only then is each of those segments parsed on its
119+ own — the description checked for inline tags, each tag line checked against
120+ the grammar declared for its name.
121+
122+ ``` mermaid
123+ graph TD
124+ C["/** ... */ comment"] -- split --> D[Description segment]
125+ C -- split --> T1["@tag segment #1"]
126+ C -- split --> T2["@tag segment #2"]
127+ D -- parsed for inline tags --> DB[DocBlock]
128+ T1 -- parsed against its grammar --> DB
129+ T2 -- parsed against its grammar --> DB
130+ ```
131+
132+ ### DocBlock
133+
134+ A ` DocBlock ` is a read-only snapshot of the whole comment: an optional
135+ ` description ` and the ordered ` tags ` list. It additionally behaves as a
136+ collection of its own tags — countable, iterable, and indexable by integer
137+ offset — so ` count($block) ` , ` foreach ($block as $tag) ` and ` $block[0] ` all
138+ work directly on it without reaching for ` ->tags ` explicitly.
139+
140+ ### Description
141+
142+ Everything before the first tag becomes the description. Most of the time
143+ that is just text, represented by a plain ` Description ` object exposing a
144+ single ` $value ` string. When the text additionally contains one or more
145+ ** inline tags** — a ` {@tag ...} ` sequence with balanced braces, such as
146+ ` {@see Mailer::send()} ` written in the middle of a sentence — it is
147+ represented as a ` TaggedDescription ` instead: an ordered mix of plain-text
148+ fragments and the nested tags found among them.
149+
150+ ```
151+ /**
152+ * Hello world {@see Mailer::send()} and more text after it.
153+ └────┬────┘ └─────────┬─────────┘ └──────────┬──────────┘
154+ text nested tag text
155+ */
156+ ```
157+
158+ Only a handful of tags — [ @see ] ( see-tag.md ) , [ @link ] ( link-tag.md ) ,
159+ [ @internal ] ( internal-tag.md ) and [ @inheritdoc ] ( inheritdoc-tag.md ) — are
160+ ever recognized this way. A ` {@param} ` written in running text, for instance, is never lifted
161+ out: ` @param ` only makes sense as a whole tag line, so the braces around it
162+ stay exactly as written, as plain text.
163+
164+ ### Tag
165+
166+ Every tag, whether it stood on its own line or was found nested inside a
167+ description, carries at minimum a ` $name ` (without the leading ` @ ` ) and an
168+ optional ` $description ` (whatever text follows its own body). A tag
169+ recognized by name exposes further, tag-specific parts on top of that — a
170+ ` ParamTag ` additionally exposes the argument's ` $type ` and ` $variable ` , a
171+ ` SeeTag ` exposes what it ` $reference ` s, and so on. A tag whose name has no
172+ registered definition at all — including every tag still marked "Not Implemented"
173+ in the sidebar — falls back to a plain ` Tag ` , with its entire suffix folded
174+ unparsed into the description.
175+
176+ ## Where to Go Next
31177
32- <secondary-label ref =" wip " />
178+ Every tag this component recognizes has its own page in the sidebar,
179+ grouped by where it comes from: Standard, Advanced, phpDocumentor, or a
180+ specific static analyzer (Psalm, PHPStan, Phan, PhpStorm, PHP
181+ CodeSniffer) for the ones not yet implemented.
33182
34- <warning >
35- WIP: Documentation is not complete
36- </warning >
183+ <deflist >
184+ <def title="Extending">
185+ How a tag's own grammar is declared, how to add a tag of your own,
186+ and how to write a new grammar building block (a "combinator") for
187+ one. See <a href="custom-tags.md">Extending</a>.
188+ </def>
189+ </deflist >
0 commit comments