AST
Traversal
Forte provides a variety of methods for navigating, querying, and iterating over nodes in a parsed document. Most traversal methods are available on both the Document and individual Node instances.
#Iterating Documents
You can iterate a document's root-level nodes with children (iterable) or getChildren (array):
1<?php
2
3use Forte\Facades\Forte;
4
5$doc = Forte::parse('<div>Hello</div><p>World</p>');
6
7foreach ($doc->children() as $child) {
8 // each root-level node
9}
10
11$children = $doc->getChildren(); // array<Node>
For quick access to the boundaries of the document:
1<?php
2
3$first = $doc->firstChild(); // ?Node
4$last = $doc->lastChild(); // ?Node
You can also check the size and content of the document:
1<?php
2
3$doc->childCount(); // int
4$doc->filled(); // true if document has non-whitespace content
5$doc->blank(); // true if document is empty or whitespace-only
#Walking the AST
Use walk for a depth-first traversal of the entire document tree. It calls the provided callback for each node and returns the document instance for chaining:
1<?php
2
3use Forte\Ast\Node;
4
5$doc->walk(function (Node $node) {
6 echo $node->kind() . "\n";
7})->render();
You may also call walk on any individual node to traverse only that subtree:
1<?php
2
3use Forte\Ast\Node;
4
5$element->walk(function (Node $node) {
6 // called for $element and all its descendants
7});
To collect all nodes below a given node without a callback, use descendants (lazy iterable) or getDescendants (array):
1<?php
2
3foreach ($node->descendants() as $descendant) {
4 // every node below $node
5}
6
7$all = $node->getDescendants(); // array<Node>
#Traversal Options
The walk, allOfType, and descendants methods accept a TraversalOptions parameter that controls which nodes are included. By default, internal nodes (like attributes and tag names) are excluded and synthetic nodes (created by rewriters) are included:
1<?php
2
3use Forte\Ast\TraversalOptions;
4
5// Default behavior: skip internal nodes, include synthetic nodes
6$options = TraversalOptions::defaults();
7
8// Include internal nodes (attributes, tag names, etc.)
9$deep = TraversalOptions::deep();
10
11$custom = new TraversalOptions(
12 includeInternal: true,
13 includeSynthetic: false,
14 includeTrivia: true,
15 maxDepth: 3,
16);
Pass options as the second argument to walk or allOfType. You can also pass true as a shorthand for TraversalOptions::deep():
1<?php
2
3use Forte\Ast\Node;
4
5// Walk including internal nodes
6$doc->walk(function (Node $node) {
7 // visits attributes, tag names, etc.
8}, TraversalOptions::deep());
9
10// Shorthand: pass true for deep traversal
11$doc->walk(function (Node $node) {
12 // same as above
13}, true);
#Finding Nodes
When you need a specific node rather than a full traversal, find returns the first match and findAll returns all matches:
1<?php
2
3use Forte\Ast\EchoNode;
4
5$firstEcho = $doc->find(fn(Node $n) => $n instanceof EchoNode); // ?Node
6$allEchoes = $doc->findAll(fn(Node $n) => $n instanceof EchoNode); // array<Node>
Dedicated finder methods are available for each major node type. Each has a singular variant (first match) and a plural variant (LazyCollection):
1<?php
2
3// Elements
4$doc->findElementByName('div'); // ?ElementNode
5$doc->findElementsByName('div'); // LazyCollection<ElementNode>
6
7// Components (full tag name including prefix)
8$doc->findComponentByName('x-alert'); // ?ComponentNode
9$doc->findComponentsByName('x-alert'); // LazyCollection<ComponentNode>
10
11// Standalone directives
12$doc->findDirectiveByName('include'); // ?DirectiveNode
13$doc->findDirectivesByName('include'); // LazyCollection<DirectiveNode>
14
15// Block directives
16$doc->findBlockDirectiveByName('if'); // ?DirectiveBlockNode
17$doc->findBlockDirectivesByName('if'); // LazyCollection<DirectiveBlockNode>
For editor integrations and cursor-aware tooling, you can locate the deepest node at a given source position:
1<?php
2
3$doc->findNodeAtOffset(42); // ?Node (by byte offset)
4$doc->findNodeAtPosition(3, 10); // ?Node (by line and column, 1-indexed)
#Lazy Collection Queries
For memory-efficient querying across the full document tree, Forte provides accessors that return LazyCollection instances for each node type:
1<?php
2
3$doc->elements; // all ElementNode instances
4$doc->components; // all ComponentNode instances
5$doc->directives; // all unpaired DirectiveNode instances
6$doc->blockDirectives; // all DirectiveBlockNode instances
7$doc->echoes; // all EchoNode instances
8$doc->rawEchoes; // raw echoes only ({!! !!})
9$doc->escapedEchoes; // escaped echoes only ({{ }})
10$doc->tripleEchoes; // triple echoes only ({{{ }}})
11$doc->comments; // all CommentNode and BladeCommentNode instances
12$doc->bladeComments; // Blade comments only ({{-- --}})
13$doc->htmlComments; // HTML comments only (<!-- -->)
14$doc->phpBlocks; // all PhpBlockNode instances
15$doc->phpTags; // all PhpTagNode instances
16$doc->text; // all TextNode instances
#Filtering
1<?php
2
3$divs = $doc->elements->filter(fn($el) => $el->is('div'));
4
5$includes = $doc->directives->filter(fn($d) => $d->nameText() === 'include');
6
7$matches = $doc->rawEchoes->filter(fn($e) => str_contains($e->expression(), '$html'));
#First Match
1<?php
2
3$firstDiv = $doc->elements->first(fn($el) => $el->is('div'));
4$firstIf = $doc->blockDirectives->first(fn($b) => $b->isIf());
#Counting
1<?php
2
3$elementCount = $doc->elements->count();
4$componentCount = $doc->components->count();
5$rawEchoCount = $doc->rawEchoes->count();
#Mapping
1<?php
2
3$tagNames = $doc->elements->map(fn($el) => $el->tagNameText())->all();
4// ['div', 'h1', 'p', 'span', ...]
5
6$directiveNames = $doc->directives->map(fn($d) => $d->nameText())->unique()->all();
7// ['include', 'csrf', 'props', ...]
#Existence Checks
1<?php
2
3$hasComponents = $doc->components->isNotEmpty();
4$hasRawEchoes = $doc->rawEchoes->isNotEmpty();
#Navigating the Tree
#Parent and Ancestor Navigation
Every node tracks its position in the tree. Use getParent to move up one level:
1<?php
2
3$parent = $node->getParent(); // ?Node
To walk the full ancestor chain from the immediate parent up to the root:
1<?php
2
3foreach ($node->ancestors() as $ancestor) {
4 // yields parent first, then grandparent, up to root
5}
6
7$ancestors = $node->getAncestors(); // array<Node> (parent-to-root order)
Ordering difference
The Node::ancestors() method yields in parent-to-root order. The NodePath::ancestors() method used in Rewriters returns an array in the opposite order (root-to-parent).
When you need the nearest ancestor matching a condition, use closest (predicate) or closestOfType (class name):
1<?php
2
3use Forte\Ast\Elements\ElementNode;
4use Forte\Ast\DirectiveBlockNode;
5
6$nearestDiv = $node->closest(fn($n) => $n instanceof ElementNode && $n->is('div'));
7$nearestBlock = $node->closestOfType(DirectiveBlockNode::class);
To check containment without retrieving the ancestor:
1<?php
2
3$isInsideForeach = $node->hasAncestorWhere(
4 fn($n) => $n instanceof DirectiveBlockNode && $n->isForeach()
5);
You can also check how deeply nested a node is (root-level nodes have depth 0):
1<?php
2
3$node->depth(); // int
#Sibling Navigation
Move horizontally through the tree with nextSibling and previousSibling:
1<?php
2
3$next = $node->nextSibling(); // ?Node
4$prev = $node->previousSibling(); // ?Node
To collect multiple siblings at once, use the directional or full sibling accessors:
1<?php
2
3$all = $node->getSiblings(); // array<Node>
4$after = $node->getNextSiblings(); // array<Node>
5$before = $node->getPreviousSiblings(); // array<Node>
You may also find siblings matching a predicate or of a specific type:
1<?php
2
3use Forte\Ast\Elements\ElementNode;
4
5$nextElement = $node->nextSiblingOfType(ElementNode::class); // ?Node
6$prevElement = $node->previousSiblingOfType(ElementNode::class); // ?Node
7
8$nextDiv = $node->nextSiblingWhere(
9 fn($n) => $n instanceof ElementNode && $n->is('div')
10);
Position predicates help identify boundary nodes:
1<?php
2
3$node->isFirstChild(); // bool
4$node->isLastChild(); // bool
5$node->isOnlyChild(); // bool
#Children Lookups
Any node with children supports predicate-based and type-based child lookups.
Use firstChildWhere and lastChildWhere to find children matching a predicate:
1<?php
2
3use Forte\Ast\Elements\ElementNode;
4
5$firstDiv = $node->firstChildWhere(
6 fn($n) => $n instanceof ElementNode && $n->is('div')
7);
8
9$lastDiv = $node->lastChildWhere(
10 fn($n) => $n instanceof ElementNode && $n->is('div')
11);
To collect all matching children:
1<?php
2
3$divs = $node->getChildrenWhere(
4 fn($n) => $n instanceof ElementNode && $n->is('div')
5);
You can also filter children by class name directly:
1<?php
2
3use Forte\Ast\EchoNode;
4
5$firstEcho = $node->firstChildOfType(EchoNode::class); // ?Node
6$allEchoes = $node->getChildrenOfType(EchoNode::class); // array<Node>
Shortcut methods are available for common node types:
1<?php
2
3$node->firstDirective(); // ?DirectiveNode
4$node->firstDirectiveBlock(); // ?DirectiveBlockNode
5$node->firstElement(); // ?ElementNode
6$node->firstText(); // ?TextNode
7$node->firstEcho(); // ?EchoNode
8
9$node->getDirectiveChildren(); // array<DirectiveNode>
10$node->getDirectiveBlockChildren(); // array<DirectiveBlockNode>
11$node->getElementChildren(); // array<ElementNode>
12$node->getTextChildren(); // array<TextNode>
13$node->getEchoChildren(); // array<EchoNode>
#Text Content
These methods extract plain text from a node's children. This is useful for reading the visible text content of elements:
Use textContent for the first text child, or trimmedTextContent for a whitespace-trimmed version:
1<?php
2
3$node->textContent(); // ?string
4$node->trimmedTextContent(); // ?string
To concatenate text from all text children:
1<?php
2
3$node->allTextContent(); // string
4$node->allTrimmedTextContent(); // string
#Tree Predicates
These methods help you understand a node's role in the tree structure. Use contains to check whether a node has another as a descendant:
1<?php
2
3$node->contains($otherNode); // bool
Leaf and root checks identify boundary positions:
1<?php
2
3$node->isLeaf(); // bool
4$node->isRoot(); // bool
To distinguish parsed nodes from those created by rewriters:
1<?php
2
3$node->isSynthetic(); // bool
#Node Collections
The nodes method on any node returns its children as a NodeCollection. The document's getRootNodes method does the same for root-level nodes. NodeCollection extends Laravel's Collection with node-specific filtering methods.
#Type Filtering
Filter a collection down to a specific node type:
1<?php
2
3$collection = $node->nodes();
4
5$collection->elements(); // ElementNode only
6$collection->directives(); // DirectiveNode only
7$collection->blockDirectives(); // DirectiveBlockNode only
8$collection->echoes(); // EchoNode only
9$collection->comments(); // CommentNode and BladeCommentNode
10$collection->components(); // ComponentNode only
11$collection->phpBlocks(); // PhpBlockNode only
12$collection->text(); // TextNode only
13$collection->ofType(EchoNode::class); // filter by any class
#Position Filtering
Narrow results to nodes at a specific source location. This is useful for editor integrations and range-based operations:
1<?php
2
3$collection->onLine(5); // nodes that span line 5
4$collection->startingOnLine(5); // nodes starting on line 5
5$collection->endingOnLine(5); // nodes ending on line 5
6$collection->containingOffset(42); // nodes containing byte offset 42
7$collection->betweenOffsets(10, 50); // nodes within an offset range
#Tree Filtering
Filter by structural role in the tree:
1<?php
2
3$collection->roots(); // root-level nodes only
4$collection->leaves(); // nodes with no children
5$collection->withChildren(); // nodes that have children
#Specific Queries
Query by directive or element name directly:
1<?php
2
3$collection->whereDirectiveName('include'); // directives named "include"
4$collection->whereElementIs('div'); // elements with tag name "div"
#Iterating Internal Children
Some nodes contain internal structural children that are not visited during a normal tree walk. These include element attributes, directive block structure, and component slots.
#Element Attributes
The attributes method on an ElementNode returns an Attributes collection. Individual attributes may be accessed by name:
1<?php
2
3use Forte\Ast\Elements\ElementNode;
4
5$element = $doc->findElementByName('div');
6
7$element->hasAttribute('class'); // bool
8$element->getAttribute('class'); // ?string
9$element->getClass(); // ?string (shorthand)
10$element->getId(); // ?string (shorthand)
11$element->getAttributes(); // array<Attribute>
You can filter attributes by type using the Attributes collection:
1<?php
2
3$attrs = $element->attributes();
4
5$attrs->static(); // standard HTML attributes
6$attrs->bound(); // Blade bound attributes (:name)
7$attrs->escaped(); // Blade escaped attributes (::name)
8$attrs->boolean(); // boolean attributes (no value)
9$attrs->complex(); // interpolated/expression attributes
10$attrs->simple(); // simple key-value attributes
11$attrs->bladeConstructs(); // embedded Blade constructs
12
13$attrs->whereNameIs('class'); // filter by exact name
14$attrs->whereNameMatches('/^data-/'); // filter by name pattern
15$attrs->exceptNames(['class', 'id']); // exclude specific names
16$attrs->onlyNames(['class', 'id']); // include only specific names
#Directive Block Structure
A DirectiveBlockNode contains an opening directive, optional intermediate directives, and a closing directive. These structural children are accessible directly:
1<?php
2
3use Forte\Ast\DirectiveBlockNode;
4
5$block = $doc->findBlockDirectiveByName('if');
6
7$block->startDirective(); // ?DirectiveNode (@if)
8$block->endDirective(); // ?DirectiveNode (@endif)
9$block->intermediateDirectives(); // iterable<DirectiveNode> (@elseif, @else)
10$block->hasIntermediates(); // bool
Block directives also expose type-check methods for common directive types:
1<?php
2
3$block->isIf(); // bool
4$block->isForeach(); // bool
5$block->isForelse(); // bool
6$block->isSection(); // bool
7$block->isSwitch(); // bool
8$block->isUnless(); // bool
9$block->isPush(); // bool
10$block->isOnce(); // bool
11$block->isVerbatim(); // bool
#Component Slots
A ComponentNode may contain named and default slots. The slot methods provide access to this internal structure:
1<?php
2
3use Forte\Ast\Components\ComponentNode;
4
5$component = $doc->findComponentByName('x-card');
6
7$component->hasSlots(); // bool
8$component->hasSlot('header'); // bool
9$component->hasDefaultSlot(); // bool
10$component->slot('header'); // ?SlotNode
11$component->getSlots(); // array<SlotNode>
12$component->getNamedSlots(); // array<SlotNode>
13$component->getDefaultSlot(); // array<Node> (non-slot children)
14$component->defaultSlotContent(); // string (rendered default slot)
Component identity metadata is also available:
1<?php
2
3$component->getComponentName(); // string (name without prefix)
4$component->getPrefix(); // string (e.g. "x-")
5$component->getType(); // string (e.g. "blade", "livewire")
6$component->isSlot(); // bool
7$component->getSlotName(); // ?string
#See also
- Documents: Parse templates and access the document API
- XPath Queries: Query documents with XPath expressions
- Rewriters: Transform nodes using the visitor pattern
- Basic Nodes: Node types and their APIs