Some Laravel apps need the same change made to every Blade file at once, like renaming a component or normalising a directive. A
sed
one-liner replaces every match, including the ones inside strings, comments, and attributes you did not want changed. Forte, written by
John Koster
, parses
.blade.php
into a typed syntax tree, lets you query the tree, and renders a rewritten template back out.
Koster also wrote
prettier-plugin-blade
. Version 3 of that formatter runs on Forte.
Use it when you need to find out what your views contain, apply one edit across hundreds of them, or fail a build when a template breaks one of your team's conventions.
安装
Forte needs PHP 8.2, the
dom
extension, and Laravel 10 through 13:
作曲家
要求
fortephp/forte
The service provider registers itself, so the
Forte
facade is available immediately.
解析
Forte::parse()
takes a string,
Forte::parseFile()
takes a path:
使用
Forte\Facades\Forte
;$doc
=
Forte
::
解析
(
'<div class="mt-4">Hello, {{ $name }}!</div>'
(英文):$doc
=
Forte
::
parseFile
(
'resources/views/welcome.blade.php'
(英文):
Both go through a lexer, which turns the source into tokens, and a tree builder, which assembles the nodes. A
Document
holds the result.
Given an unclosed tag or a
@if
with no
@endif
, the parser records a diagnostic on the document and returns a partial tree. You can query and rewrite that tree like any other. Two broken views in a four-hundred-view app produce two diagnostics, and the other 398 parse normally.
Parse a file and render it back without changes and you get the same bytes, whitespace included.
Querying the Tree
The query methods return lazy collections, so you can filter and map them the way you would any other Laravel collection:
$forms
=
$doc
->
queryElements
(
'形式'
(英文):$conditionals
=
$doc
->
queryBlockDirectives
([
'if'
,
'unless'
]);$components
=
$doc
->
queryComponents
([
'x-alert'
,
'livewire:*'
]);
For a single known node there are direct lookups:
导航
=
$doc
->
elementById
(
'primary-navigation'
(英文):$hasHead
=
$doc
->
hasElement
(
'head'
(英文):
isDynamic()
tells you whether the value came from a bound attribute, and
attributeTokens('class')
splits a class list into an array:
表单
=
$doc
->
firstElement
(
'形式'
(英文):方法
=
表单
?->
属性
(
'方法'
(英文):$isDynamic
=
方法
?->
isDynamic
()
??
错误的
;$classes
=
表单
?->
attributeTokens
(
'班级'
)
??
[];
Forte builds a
DOMDocument
from the tree and runs the expression through PHP's
DOMXPath
, which is what the
ext-dom
requirement covers. Blade constructs become elements in a
forte
namespace, so
@if
blocks are
forte:if
and echoes are
forte:echo
:
$divs
=
$doc
->
xpath
(
'//div[@class]'
)
->
得到
();$conditionals
=
$doc
->
xpath
(
'//forte:if'
)
->
得到
();
Matches come back as Forte nodes rather than
DOMElement
objects, so you can pass a query result to a rewrite. Finding every
<a>
inside a
<nav>
that has no
href
is one expression instead of a recursive walk.
Rewriting
apply()
,
rewrite()
, 和
rewriteWith()
each return a new
Document
and leave the original alone, so you can keep both and compare them.
rewriteWith()
handles one-off changes with a closure. The callback receives a
NodePath
rather than the node itself:
使用
Forte\Rewriting\NodePath
;$newDoc
=
$doc
->
rewriteWith
(
功能
(
NodePath
$path) {
如果
($path
->
isTag
(
'一个'
)
&&
str_starts_with
($path
->
获取属性
(
'href'
)
??
“”
,
'http'
)){$路径
->
设置属性
(
'目标'
,
'_blank'
(英文):$路径
->
设置属性
(
'rel'
,
'noopener noreferrer'
(英文):}});回声
$newDoc
->
使成为
();
A
NodePath
exposes the parent, siblings, ancestors, and depth of the node it points at, along with
getAttribute()
,
setAttribute()
,
removeAttribute()
,
addClass()
,
renameTag()
,
replaceWith()
,
remove()
,
insertBefore()
, 和
insertAfter()
。
skipChildren()
和
stopTraversal()
end the traversal early once you have found the node you want.
Forte queues the edits rather than applying them one at a time, so a pass over a large template produces one new document instead of one per edit.
For anything longer than a closure, write a visitor:
使用
Forte\Rewriting\Visitor
;使用
Forte\Rewriting\NodePath
;班级
NormalizeAlerts
延伸
Visitor{
民众
功能
进入
(
NodePath
$path)
:
空白{
如果
(
!
$路径
->
isTag
(
'div'
)
||
!
$路径
->
hasAttribute
(
'data-alert'
)){
返回
;}$level
=
$路径
->
获取属性
(
'data-alert'
)
??
'信息'
;$路径
->
renameTag
(
'x-alert'
(英文):$路径
->
设置属性
(
'类型'
,$level);$路径
->
removeAttribute
(
'data-alert'
(英文):}}
enter()
runs before the node's children are visited, and
leave()
runs after. Most passes only need
enter()
. Use
leave()
when the change depends on what happened to the children, such as unwrapping an element once its contents have been rewritten.
还有一个
RewriteBuilder
for the declarative version: select nodes by XPath, then queue the mutations to apply to the matches.
Building New Nodes
Rewrites often need a new node to put in place.
Builder
makes one:
使用
Forte\Rewriting\Builders\Builder
;建造者
::
元素
(
'div'
)
->
班级
(
'wrapper'
)
->
文本
(
'你好'
(英文):建造者
::
指示
(
'if'
,
'($show)'
(英文):建造者
::
回声
(
'$name'
(英文):
Pass the result to
replaceWith()
,
insertBefore()
, 或者
insertAfter()
。
Auditing, Codemods, and CI Checks
Finding Out What Your Views Contain
Before deleting a component, find every view that still renders it:
使用
Forte\Facades\Forte
;使用
Illuminate\Support\Facades\File
;foreach
(
文件
::
所有文件
(
resource_path
(
“观点”
))
作为
$file) {
如果
(
!
str_ends_with
($file
->
getFilename
(),
'.blade.php'
)){
继续
;}$uses
=
Forte
::
parseFile
($file
->
获取路径名
())
->
queryComponents
([
'x-alert'
])
->
数数
();
如果
($uses
>
0
){
回声
"{
$文件
->
getRelativePathname
()}: {
$uses
}
\n
“
;}}
grep -rc 'x-alert' resources/views
answers a similar question in one line, and it counts the mentions in comments, in
@php
strings, and in a
class="x-alert-icon"
attribute alongside the real ones. The count above is the number of times the component is rendered.
Making the Same Edit in Hundreds of Views
添加
loading="lazy"
to every
<img>
that has no
loading
attribute is a pass you can run, review as a diff, and re-run after you adjust it:
使用
Forte\Facades\Forte
;使用
Forte\Rewriting\NodePath
;使用
Illuminate\Support\Facades\File
;foreach
(
文件
::
所有文件
(
resource_path
(
“观点”
))
作为
$file) {
如果
(
!
str_ends_with
($file
->
getFilename
(),
'.blade.php'
)){
继续
;}$doc
=
Forte
::
parseFile
($file
->
获取路径名
());$updated
=
$doc
->
rewriteWith
(
功能
(
NodePath
$path) {
如果
($path
->
isTag
(
'img'
)
&&
!
$路径
->
hasAttribute
(
'loading'
)){$路径
->
设置属性
(
'loading'
,
'lazy'
(英文):}});
文件输出内容
($file
->
获取路径名
(), $updated
->
使成为
());}
一个
<img>
written inside a comment, a string, or a
@php
block parses as a different node kind, so
isTag('img')
is false for it. Getting that right with a regex takes more care than the rest of the job.
If the pass was wrong, fix the script, run
git restore resources/views
, and try again.
Failing the Build on a Broken Convention
A
POST
form with no
@csrf
is one expression, so the check fits inside a test:
使用
Forte\Facades\Forte
;使用
Illuminate\Support\Facades\File
;测试
(
'every POST form has a CSRF token'
,
功能
(){$offenders
=
[];
foreach
(
文件
::
所有文件
(
resource_path
(
“观点”
))
作为
$file) {
如果
(
!
str_ends_with
($file
->
getFilename
(),
'.blade.php'
)){
继续
;}缺失
=
Forte
::
parseFile
($file
->
获取路径名
())
->
xpath
(
'//form[@method="POST"][not(.//forte:csrf)]'
)
->
数数
();
如果
($missing
>
0
){$offenders[]
=
$文件
->
getRelativePathname
();}}
预计
($offenders)
->
为空
();});
That expression reads left to right: every
<form>
和
method="POST"
, keeping the ones with no
@csrf
anywhere inside them. The
.//
is the "anywhere inside" part, so a form whose
@csrf
is in a sibling form still counts as an offender.
A
@foreach
whose first child has no
wire:key
is also one expression:
$missingKeys
=
$doc
->
xpath
(
'//forte:foreach[*[1][not(@*[name()="wire:key"])]]'
)
->
数数
();
//forte:foreach
matches every
@foreach
block.
*[1]
is the first child element of that block.
not(@*[name()="wire:key"])
keeps the blocks whose first child has no
wire:key
attribute. The
@*[name()="..."]
form is there because XPath reads the colon in
wire:key
as a namespace separator.
Both expressions are tests in Forte's own suite. The second one depends on nesting and on which child comes first.
When a Regex Is Enough
For one rename across thirty views,
sed
or your IDE's structural search plus a careful read of the diff is less work than writing a visitor. The same goes for an edit you make once and never check again.
Forte is worth the setup in these cases:
- The rule depends on structure: "inside a form", "the first child of", "nested in a
@foreach". A grep matches lines, so it cannot express these. - The edit touches so many files that reading the whole diff by hand takes longer than writing the pass.
- The check runs on every commit, where a match inside a comment or a string fails the build for no reason.
Chisel and Reload
Two other packages are built on Forte, and you can use both without writing a visitor. Chisel is
prettier-plugin-blade
v3, rewritten against the new parser; the project reports it formatting complex real-world templates 140 times faster than the previous version. It needs Node 18 or later:
npm
我
-D
更漂亮
prettier-plugin-blade@^3
Blade formatting has more options than it used to, including Laravel Pint's own Blade support 和 format-on-save in PhpStorm 。
Reload is a Vite plugin that patches Blade changes into the page without a full refresh. Its docs call it experimental, and it falls back to a full reload after
max_patches_before_reload
incremental patches:
作曲家
要求
fortephp/reload
--dev
It watches
resources/views/**/*.blade.php
and instruments elements, components, directives, and includes. The
refresh
选项
laravel-vite-plugin
已经
reloads the page when a Blade file changes
. Reload patches the DOM instead.
Forte is MIT licensed and currently at v1.1.0. The source is on GitHub , the documentation and an interactive playground are at fortephp.com 。







