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

#See also

  • Traversal: Alternative traversal methods without XPath
  • Documents: Parse templates and access the document API
  • Elements: HTML element nodes returned by XPath queries