Querying
XPath Queries
You may query Forte documents using XPath, which simplifies searching for specific patterns compared to manual document traversal. Forte handles converting your Blade template into a valid DOMDocument instance, allowing you to reuse existing knowledge and tooling.
#Quick Start
Here is a basic example. Given a parsed document, you can query it with any XPath 1.0 expression:
1<?php
2
3use Forte\Facades\Forte;
4
5$doc = Forte::parse('<div><span class="hi">Hello</span></div>');
6
7$span = $doc->xpath('//span')->first();
8$span->tagNameText(); // "span"
XPath always returns real AST nodes, the same objects you get from $doc->children(). You can count results, check for existence, or iterate over matches:
1// Count all @if directives
2$doc->xpath('//forte:if')->count();
3
4// Check if any <img> is missing alt
5$doc->xpath('//img[not(@alt)]')->exists();
You can XPath into Blade directives and continue working with the standard Node API on the results:
1$doc = Forte::parse('@if($show) <div>Visible</div> @endif');
2
3$div = $doc->xpath('//forte:if//div')->first();
4
5$div->tagNameText(); // "div"
6$div->children(); // child nodes
7$div->startOffset(); // byte offset in original template
8$div->getParent(); // the @if DirectiveNode
#The XPathWrapper
Calling $doc->xpath(string $expression) returns an XPathWrapper, a lazy, readonly object that evaluates the expression only when you ask for results.
The wrapper provides several methods for accessing query results:
| Method | Returns | Description |
|---|---|---|
->first() |
?Node |
First match, or null |
->get() |
NodeCollection |
All matches as a rich collection |
->all() |
array<Node> |
All matches as a plain array |
->exists() |
bool |
true if any match exists |
->count() |
int |
Number of matches |
->evaluate() |
mixed |
Raw XPath result (number, string, bool) |
The XPathWrapper also implements Countable and IteratorAggregate, so you can use it directly in foreach loops and count() calls:
1foreach ($doc->xpath('//li') as $node) {
2 echo $node->tagNameText();
3}
4
5$count = count($doc->xpath('//li'));
The get() method returns a NodeCollection, which provides additional filtering methods for working with query results:
1$collection = $doc->xpath('//div')->get();
2
3$collection->elements(); // only ElementNode instances
4$collection->directives(); // only DirectiveBlockNode/DirectiveNode
5$collection->onLine(5); // nodes on line 5
6$collection->ofType(EchoNode::class); // filter by type
#DOM Mapping Reference
Forte converts Blade constructs into a valid DOM structure so that XPath can query them. Understanding this mapping is the key to writing correct queries. Each Blade construct has a specific DOM representation with predictable attributes you can target.
#HTML Elements
HTML elements are preserved as-is in the DOM, with a data-forte-idx attribute linking each element back to its AST node:
1<div class="foo"> → <div class="foo" data-forte-idx="N">
2<input type="text"> → <input type="text" data-forte-idx="N">
You can query them with their normal tag names: //div, //input[@type="text"].
#Blade Directives
Blade directives become forte: namespaced elements in the DOM, letting you query them by directive name. Block directives and standalone directives share the same forte: prefix:
| Blade Syntax | XPath Selector | DOM Attributes |
|---|---|---|
@if($x)...@endif |
//forte:if |
args="($x)" |
@foreach($items as $item)...@endforeach |
//forte:foreach |
args="($items as $item)" |
@unless($x)...@endunless |
//forte:unless |
args="($x)" |
@isset($x)...@endisset |
//forte:isset |
args="($x)" |
@auth...@endauth |
//forte:auth |
args if present |
@guest...@endguest |
//forte:guest |
args if present |
@can("edit", $p)...@endcan |
//forte:can |
args="(\"edit\", $p)" |
@switch($s)...@endswitch |
//forte:switch |
child forte:case, forte:default |
@include("view") |
//forte:include |
args="(\"view\")" |
@yield("content") |
//forte:yield |
args |
@section("name")...@endsection |
//forte:section |
args |
@push("stack")...@endpush |
//forte:push |
args |
@stack("name") |
//forte:stack |
args |
@csrf |
//forte:csrf |
(none) |
#Intermediate Directives
Directives like @else, @elseif($x), @case("val"), and @default are child elements of their parent directive, marked with data-forte-intermediate="true":
1// Template:
2// @if($a)
3// <p>A</p>
4// @elseif($b)
5// <p>B</p>
6// @else
7// <p>C</p>
8// @endif
9
10$doc->xpath('//*[@data-forte-intermediate="true"]'); // 2 results (@elseif + @else)
11$doc->xpath('//forte:elseif[@data-forte-intermediate="true"]'); // just @elseif
For @switch blocks, you can query the individual branches directly:
1$doc->xpath('//forte:switch//forte:case'); // all @case branches
2$doc->xpath('//forte:switch//forte:default'); // the @default branch
#Echoes
Blade echo statements map to forte: namespaced elements. The original expression is available via the expression attribute, making it easy to find specific interpolations:
| Blade Syntax | XPath Selector | DOM Attribute |
|---|---|---|
{{ $expr }} |
//forte:echo |
expression="$expr" |
{!! $expr !!} |
//forte:raw-echo |
expression="$expr" |
{{{ $expr }}} |
//forte:triple-echo |
expression="$expr" |
#Comments and Special Constructs
Other Blade and PHP constructs are also mapped to forte: namespaced elements:
| Blade Syntax | XPath Selector | Key Attributes |
|---|---|---|
{{-- text --}} |
//forte:comment |
content="..." |
@php...@endphp |
//forte:php |
code="..." |
<?php ?> |
//forte:php-tag |
code, type="full" |
<?= ?> |
//forte:php-tag |
code, type="short" |
@verbatim...@endverbatim |
//forte:verbatim |
content="..." |
<!DOCTYPE html> |
//forte:doctype |
type="..." |
#Components
Blade components keep their original tag name in the DOM and gain metadata attributes that identify them as components:
1<x-button type="primary"> → <x-button data-forte-component="true"
2 data-forte-component-type="blade"
3 type="primary"
4 data-forte-idx="N">
The data-forte-component-type attribute indicates which component system the tag belongs to:
| Component Prefix | data-forte-component-type |
|---|---|
<x-...> |
"blade" |
<livewire:...> |
"livewire" |
<flux:...> |
"flux" |
Slots are identified by their own metadata attributes:
1// <x-card><x-slot:header>Title</x-slot:header>Body</x-card>
2
3$doc->xpath('//*[@data-forte-slot="true"]'); // all slots
4$doc->xpath('//*[@data-forte-slot-name="header"]'); // named slot
5$doc->xpath('//*[@data-forte-slot="true"][not(@data-forte-slot-name)]'); // default slot
#Attribute Mapping
Standard HTML attributes pass through unchanged. Blade-specific attribute syntaxes (bound, escaped, wire, etc.) are transformed so they can be queried while preserving their semantics:
| Blade Syntax | DOM Representation | XPath |
|---|---|---|
class="foo" |
class="foo" (unchanged) |
@class="foo" |
:class="$expr" |
forte:bind-class="$expr" |
@forte:bind-class |
::class="$expr" |
forte:escape-class="$expr" |
@forte:escape-class |
wire:model="x" |
wire:model="x" (unchanged) |
@*[name()="wire:model"] |
{{ $attrs }} (spread) |
data-forte-has-spread="true" |
@data-forte-has-spread |
#Dynamic Tags and Attributes
When a tag name or attribute name contains Blade expressions, the DOM uses metadata attributes to preserve the original source. For dynamic tag names, the sanitized name becomes the element name and data-forte-dynamic-tag stores the original expression:
1// <div-{{ $type }}>Content</div-{{ $type }}>
2// DOM: <div-__type__ data-forte-dynamic-tag="div-{{ $type }}">
3
4$doc->xpath('//*[@data-forte-dynamic-tag]'); // all dynamic tags
5$doc->xpath('//*[contains(@data-forte-dynamic-tag, "{{ $type }}")]'); // by expression
6$doc->xpath('//*[starts-with(@data-forte-dynamic-tag, "div-")]'); // by prefix
7$doc->xpath('//*[starts-with(name(), "card-")]'); // by sanitized name prefix
Dynamic attribute names follow a similar pattern, stored in data-forte-dynamic-attrs:
1// <div class-{{ $dynamic }}="thing">
2// DOM: <div data-forte-dynamic-attrs="class-{{ $dynamic }}">
3
4$doc->xpath('//*[@data-forte-dynamic-attrs]'); // all dynamic attrs
5$doc->xpath('//*[@data-forte-has-spread="true"]'); // spread attributes
#XPath Syntax Primer
This section provides a compact XPath 1.0 reference with Forte-specific examples.
#Axes
Axes control the direction of traversal through the document tree:
| Axis | Example | Finds |
|---|---|---|
// |
//div |
All <div> anywhere in the tree |
/ |
//ul/li |
Direct <li> children of <ul> |
ancestor:: |
//li/ancestor::div |
Any <div> ancestor of <li> |
following-sibling:: |
//span[1]/following-sibling::span |
Sibling <span> after the first |
preceding-sibling:: |
//li[3]/preceding-sibling::li |
Sibling <li> before the third |
parent::* |
//span/parent::* |
Parent of any <span> |
#Predicates
Predicates filter the selected nodes based on conditions:
| Pattern | Meaning |
|---|---|
[@attr] |
Has attribute attr |
[@attr="val"] |
Attribute equals value |
[not(@attr)] |
Does NOT have attribute |
[@a][@b] |
Has both a and b |
[1] |
First child of its kind |
[last()] |
Last child of its kind |
[not(node())] |
Empty (no children at all) |
#Functions
XPath 1.0 provides built-in functions for string matching, counting, and more:
| Function | Example | Description |
|---|---|---|
name() |
*[name()="wire:click"] |
Full element/attr name |
local-name() |
*[starts-with(local-name(), "include")] |
Name without namespace prefix |
starts-with() |
@*[starts-with(name(), "wire:model")] |
Attribute name prefix match |
contains() |
contains(@class, "card") |
Substring match |
text() |
button[text()] |
Has text content |
normalize-space() |
not(text()[normalize-space()]) |
No non-whitespace text |
not() |
img[not(@alt)] |
Negation |
count() |
count(//li) (via evaluate()) |
Count nodes |
position() |
li[position() > 1] |
Position-based filtering |
#Operators
Operators let you combine node sets or build compound conditions:
| Operator | Example | Description |
|---|---|---|
| |
//forte:echo | //forte:raw-echo |
Union of two node sets |
and |
[@type and @name] |
Logical AND |
or |
[@onclick or @onmouseover] |
Logical OR |
=, != |
[@type="text"] |
Equality, inequality |