Forte: Parse and Rewrite Laravel Blade Templates with a Typed Syntax Tree
Laravel Composer Pacakge #Laravel #Blade #Parser #Codemod #PHP #Packages

Forte: Parse and Rewrite Laravel Blade Templates with a Typed Syntax Tree

4 min read Mohamed Said Mohamed Said

What Is Forte?

Forte (fortephp/forte) is a Laravel package written by John Koster that parses .blade.php files into a typed syntax tree. Instead of running a regex or a sed one-liner that blindly matches every occurrence — including ones inside comments, strings, and unrelated attributes — Forte gives you a structured document you can query and rewrite with precision.

Version 3 of prettier-plugin-blade (Chisel) is built on Forte, and the project reports it formats complex real-world templates 140 times faster than the previous version.

Installation

Forte requires PHP 8.2, the ext-dom extension, and Laravel 10–13:

composer require fortephp/forte

The service provider auto-registers, so the Forte facade is available immediately.

Parsing Blade Files

use Forte\Facades\Forte;

$doc = Forte::parse('<div class="mt-4">Hello, {{ $name }}!</div>');
$doc = Forte::parseFile('resources/views/welcome.blade.php');

Both methods run a lexer and a tree builder. If a template has an unclosed tag or an @if with no @endif, the parser records a diagnostic and returns a partial tree — the other files in your project still parse normally. Rendering a parsed file back without changes produces identical bytes, whitespace included.

Querying the Tree

Query methods return lazy Laravel collections:

$forms        = $doc->queryElements('form');
$conditionals = $doc->queryBlockDirectives(['if', 'unless']);
$components   = $doc->queryComponents(['x-alert', 'livewire:*']);

Forte builds a DOMDocument internally and exposes XPath queries. Blade constructs become elements in a forte namespace, so @if is forte:if and {{ }} echoes are forte:echo:

$divs        = $doc->xpath('//div[@class]')->get();
$conditionals = $doc->xpath('//forte:if')->get();

Matches come back as Forte nodes, not raw DOMElement objects, so you can pipe them straight into a rewrite.

Rewriting Templates

rewriteWith() accepts a closure that receives a NodePath — an object that exposes the node's parent, siblings, ancestors, depth, and mutation methods:

use Forte\Rewriting\NodePath;

$newDoc = $doc->rewriteWith(function (NodePath $path) {
    if ($path->isTag('a') && str_starts_with($path->getAttribute('href') ?? '', 'http')) {
        $path->setAttribute('target', '_blank');
        $path->setAttribute('rel', 'noopener noreferrer');
    }
});

echo $newDoc->render();

Edits are queued and applied in a single pass, so a large template produces one new document rather than one per mutation. For longer logic, write a Visitor class with enter() and leave() methods. A Builder helper creates new nodes to insert:

use Forte\Rewriting\Builders\Builder;

Builder::element('div')->class('wrapper')->text('Hello');
Builder::directive('if', '($show)');

Practical Use Cases

Auditing Views Before Deleting a Component

$uses = Forte::parseFile($file->getPathname())
    ->queryComponents(['x-alert'])
    ->count();

Unlike grep, this count excludes mentions inside comments, @php strings, and unrelated class attributes.

Bulk Codemods

Adding loading="lazy" to every <img> without that attribute across hundreds of views is a single rewriteWith() pass. An <img> inside a comment parses as a different node kind, so isTag('img') is false for it — no accidental matches.

CI Convention Checks

$missing = Forte::parseFile($file->getPathname())
    ->xpath('//form[@method="POST"][not(.//forte:csrf)]')
    ->count();

That XPath expression finds every POST form with no @csrf anywhere inside it. Drop it in a Pest or PHPUnit test to fail the build automatically.

Ecosystem: Chisel and Reload

  • Chisel (prettier-plugin-blade v3) — Blade formatter built on Forte; requires Node 18+.
  • Reload (fortephp/reload) — experimental Vite plugin that patches Blade changes into the page without a full refresh, falling back after a configurable number of incremental patches.

Key Takeaways

  • Forte parses Blade into a typed syntax tree, avoiding false positives that plague regex-based tools.
  • XPath queries use a forte: namespace for Blade-specific constructs (forte:if, forte:echo, forte:csrf, etc.).
  • Rewrites are non-destructive: rewriteWith() returns a new Document and leaves the original intact.
  • Partial parse results and diagnostics mean one broken template never blocks the rest of your codebase.
  • Best suited for structure-dependent rules, large-scale codemods, and automated CI checks — not for one-off renames where sed is faster.

Forte is MIT licensed and currently at v1.1.0. Source and docs: fortephp.com | GitHub.


Source: Forte: Parse and Rewrite Laravel Blade Templates — Laravel News

Found this useful?

Frequently Asked Questions

3 questions
Q01 How is Forte different from using grep or sed to find and replace Blade template content?
Forte parses templates into a typed syntax tree, so queries and rewrites only match real rendered nodes. A grep or sed command also matches occurrences inside comments, PHP strings, and unrelated attributes. Forte's `isTag()` and XPath queries are false for those node kinds, eliminating accidental matches.
Q02 Does Forte break if a Blade template has a syntax error?
No. When the parser encounters an unclosed tag or an unmatched directive like `@if` without `@endif`, it records a diagnostic on the document and returns a partial tree. The rest of your templates parse normally, and you can still query and rewrite the partial tree.
Q03 When should I use a Visitor class instead of a rewriteWith() closure?
Use a closure for short, self-contained changes. Write a Visitor when the logic is longer or needs both `enter()` (before children are visited) and `leave()` (after children are processed) hooks — for example, when a change depends on what happened to a node's children.

Continue reading

More Articles

View all